package crypto

import (
	"cachefs/pkg/chunk"
	"crypto/cipher"
	"errors"
	"io"

	"golang.org/x/crypto/chacha20poly1305"
)

type cryptoWriter interface {
	io.Writer
	io.WriterAt
}

const lastChunkFlag = 0x01

func setLastChunkFlag(nonce *[NonceSize]byte) {
	nonce[len(nonce)-1] = lastChunkFlag
}

type writer struct {
	a         cipher.AEAD
	dst       cryptoWriter
	unwritten []byte // backed by buf
	buf       [FullSize]byte
	nonce     [NonceSize]byte
	err       error
	cm        map[int64]*chunkWriter
	last      *chunkWriter
	flush     bool
}

func newWriter(key []byte, dst cryptoWriter) (*writer, error) {
	aead, err := chacha20poly1305.New(key)
	if err != nil {
		return nil, err
	}
	w := &writer{
		a:   aead,
		dst: dst,
		cm:  make(map[int64]*chunkWriter),
	}
	w.unwritten = w.buf[:0]
	return w, nil
}

type chunkWriter struct {
	offset int64
	cn     int64
	w      *writer
	buf    [ChunkSize]byte
	nonce  [NonceSize]byte
	chunks chunk.Chunks
}

func (c *chunkWriter) writeAt(p []byte, offset int64) (n int, err error) {
	n = copy(c.buf[offset:], p)
	c.chunks.Add(offset, n)
	return
}

func (c *chunkWriter) full() bool {
	if len(c.chunks) == 1 {
		return c.chunks[0][1] == ChunkSize
	}
	return false
}

func (c *chunkWriter) flush(last bool) (n int, err error) {
	wb := c.buf[:0]
	if last {
		setLastChunkFlag(&c.nonce)
		s := c.chunks.Size()
		wb = c.buf[:s]
		n = int(s)
	} else {
		wb = c.buf[:]
		n = len(c.buf)
	}
	buf := c.w.a.Seal(wb[:0], c.nonce[:], wb, nil)
	_, err = c.w.dst.WriteAt(buf, c.offset)
	return
}

func (w *writer) Write(p []byte) (n int, err error) {
	// TODO: consider refactoring with a bytes.Buffer.
	if w.err != nil {
		return 0, w.err
	}
	if len(p) == 0 {
		return 0, nil
	}

	total := len(p)
	for len(p) > 0 {
		freeBuf := w.buf[len(w.unwritten):ChunkSize]
		n := copy(freeBuf, p)
		p = p[n:]
		w.unwritten = w.unwritten[:len(w.unwritten)+n]
		w.flush = true

		if len(w.unwritten) == ChunkSize && len(p) > 0 {
			if err := w.flushChunk(notLastChunk); err != nil {
				w.err = err
				return 0, err
			}
		}
	}
	return total, nil
}

func (w *writer) Close() error {
	if w.err != nil {
		return w.err
	}

	if w.last != nil {
		_, w.err = w.last.flush(lastChunk)
		if w.err != nil {
			return w.err
		}
		w.flush = false
	}

	if w.flush {
		w.err = w.flushChunk(lastChunk)
		if w.err != nil {
			return w.err
		}
	}

	w.err = errors.New("stream.Writer is already closed")
	return nil
}

const (
	lastChunk    = true
	notLastChunk = false
)

func (w *writer) flushChunk(last bool) error {
	if !last && len(w.unwritten) != ChunkSize {
		panic("stream: internal error: flush called with partial chunk")
	}

	if last {
		setLastChunkFlag(&w.nonce)
	}
	buf := w.a.Seal(w.buf[:0], w.nonce[:], w.unwritten, nil)
	_, err := w.dst.Write(buf)
	w.unwritten = w.buf[:0]
	incNonce(&w.nonce)
	return err
}
