package tagwriter

import (
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"os"
	"strings"

	id3v2 "github.com/bogem/id3v2/v2"

	"yellowjacket/backend/fileutil"
)

// Sentinel errors for WAV RIFF operations.
var (
	errRF64NotSupported   = errors.New("RF64 files are not yet supported")
	errNotRIFF            = errors.New("not a RIFF file")
	errNotWAVE            = errors.New("not a WAVE file")
	errFileTooLargeForWAV = errors.New("file too large for WAV format (>4GB)")
)

// riffChunk holds a single RIFF sub-chunk (ID + raw data).
type riffChunk struct {
	id   [4]byte
	data []byte
}

// parseRIFF reads all RIFF sub-chunks from r.  It rejects RF64 files
// and non-WAVE containers with descriptive errors.  The parser is
// lenient on read: it tolerates missing padding bytes and ignores
// the declared RIFF size.
func parseRIFF(r io.ReadSeeker) ([]riffChunk, error) {
	// Read 4-byte container magic.
	var magic [4]byte
	if _, err := io.ReadFull(r, magic[:]); err != nil {
		return nil, fmt.Errorf("read RIFF magic: %w", err)
	}

	if string(magic[:]) == "RF64" {
		return nil, errRF64NotSupported
	}

	if string(magic[:]) != "RIFF" {
		return nil, fmt.Errorf("%w: got %q", errNotRIFF, magic)
	}

	// Read (and discard) RIFF size — lenient, do not enforce.
	var riffSize uint32
	if err := binary.Read(r, binary.LittleEndian, &riffSize); err != nil {
		return nil, fmt.Errorf("read RIFF size: %w", err)
	}

	// Read 4-byte form type.
	var form [4]byte
	if _, err := io.ReadFull(r, form[:]); err != nil {
		return nil, fmt.Errorf("read WAVE form type: %w", err)
	}

	if string(form[:]) != "WAVE" {
		return nil, fmt.Errorf("%w: got %q", errNotWAVE, form)
	}

	// Read sub-chunks until EOF.
	var chunks []riffChunk

	for {
		var chunkID [4]byte

		_, err := io.ReadFull(r, chunkID[:])
		if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
			break
		}

		if err != nil {
			return nil, fmt.Errorf("read chunk ID: %w", err)
		}

		var chunkSize uint32
		if err := binary.Read(r, binary.LittleEndian, &chunkSize); err != nil {
			return nil, fmt.Errorf("read chunk size for %q: %w", chunkID, err)
		}

		data := make([]byte, chunkSize)
		if _, err := io.ReadFull(r, data); err != nil {
			return nil, fmt.Errorf("read chunk data for %q: %w", chunkID, err)
		}

		chunks = append(chunks, riffChunk{id: chunkID, data: data})

		// Odd-length chunks have a padding byte.  Lenient: if the
		// read fails (e.g. EOF), just break rather than error.
		if chunkSize%2 != 0 {
			var pad [1]byte

			if _, err := r.Read(pad[:]); err != nil {
				break
			}
		}
	}

	return chunks, nil
}

// isID3ChunkID returns true if id represents an ID3v2 RIFF chunk.
// Both lowercase "id3 " and uppercase "ID3 " are accepted.
func isID3ChunkID(id [4]byte) bool {
	s := strings.ToLower(string(id[:3]))

	return s == "id3"
}

// writeRIFF writes a complete RIFF/WAVE container to w, preserving
// the given chunks in order and appending the id3Data as the final
// "id3 " chunk.  Returns errFileTooLargeForWAV if the result would
// exceed the 4 GB RIFF limit.
func writeRIFF(w io.Writer, chunks []riffChunk, id3Data []byte) error {
	// Calculate total RIFF payload size:
	//   4 bytes (WAVE form type)
	// + for each preserved chunk: 8 (header) + len(data) + padding
	// + id3 chunk: 8 + len(id3Data) + padding
	riffPayload := uint64(4)

	for _, c := range chunks {
		sz := uint64(len(c.data))
		riffPayload += 8 + sz

		if sz%2 != 0 {
			riffPayload++
		}
	}

	id3Len := uint64(len(id3Data))
	riffPayload += 8 + id3Len

	if id3Len%2 != 0 {
		riffPayload++
	}

	// The RIFF header itself is 8 bytes (magic + size), so the
	// total file size is riffPayload + 8.
	const maxRIFFSize = 0xFFFFFFFF
	if riffPayload > maxRIFFSize {
		return errFileTooLargeForWAV
	}

	// Write RIFF header: magic + uint32 LE size + WAVE.
	if _, err := w.Write([]byte("RIFF")); err != nil {
		return fmt.Errorf("write RIFF magic: %w", err)
	}

	if err := binary.Write(w, binary.LittleEndian, uint32(riffPayload)); err != nil {
		return fmt.Errorf("write RIFF size: %w", err)
	}

	if _, err := w.Write([]byte("WAVE")); err != nil {
		return fmt.Errorf("write WAVE form: %w", err)
	}

	// Write each preserved chunk.
	for _, c := range chunks {
		if err := writeChunk(w, c.id, c.data); err != nil {
			return err
		}
	}

	// Write id3 chunk last.
	var id3ID [4]byte

	copy(id3ID[:], "id3 ")

	return writeChunk(w, id3ID, id3Data)
}

// writeChunk writes a single RIFF sub-chunk (id + size + data + padding).
func writeChunk(w io.Writer, id [4]byte, data []byte) error {
	if _, err := w.Write(id[:]); err != nil {
		return fmt.Errorf("write chunk ID %q: %w", id, err)
	}

	if err := binary.Write(w, binary.LittleEndian, uint32(len(data))); err != nil {
		return fmt.Errorf("write chunk size %q: %w", id, err)
	}

	if _, err := w.Write(data); err != nil {
		return fmt.Errorf("write chunk data %q: %w", id, err)
	}

	// Pad odd-length chunks with a zero byte.
	if len(data)%2 != 0 {
		if _, err := w.Write([]byte{0}); err != nil {
			return fmt.Errorf("write chunk padding %q: %w", id, err)
		}
	}

	return nil
}

// writeWavTags applies the given TagChanges to a WAV file's ID3v2 tag
// embedded in a RIFF "id3 " chunk.  All non-ID3v2 chunks are
// preserved byte-for-byte in their original order.  The result is
// written atomically via fileutil.AtomicWrite.
func writeWavTags(
	logger *slog.Logger,
	filePath string,
	changes TagChanges,
) error {
	// Warn for very large files (same threshold as FLAC writer).
	if info, err := os.Stat(filePath); err == nil {
		const largeSizeThreshold = 500 * 1024 * 1024 // 500 MB

		if info.Size() > largeSizeThreshold {
			logger.Warn("large WAV file may use significant memory",
				slog.String("path", filePath),
				slog.Int64("size", info.Size()),
			)
		}
	}

	f, err := os.Open(filePath)
	if err != nil {
		return fmt.Errorf("open wav for reading: %w", err)
	}

	allChunks, err := parseRIFF(f)

	// Close immediately — we need the handle released before
	// AtomicWrite creates the replacement file.
	_ = f.Close()

	if err != nil {
		return fmt.Errorf("parse wav RIFF: %w", err)
	}

	// Separate preserved chunks from existing ID3 data.
	var (
		preserved   []riffChunk
		existingID3 []byte
	)

	for _, c := range allChunks {
		if isID3ChunkID(c.id) {
			existingID3 = c.data
		} else {
			preserved = append(preserved, c)
		}
	}

	// Build ID3v2 tag — merge with existing if present.
	var tag *id3v2.Tag

	if len(existingID3) > 0 {
		parsed, parseErr := id3v2.ParseReader(
			bytes.NewReader(existingID3),
			id3v2.Options{Parse: true},
		)
		if parseErr != nil {
			return fmt.Errorf("parse existing ID3v2 in WAV: %w", parseErr)
		}

		tag = parsed
	} else {
		tag = id3v2.NewEmptyTag()
		tag.SetDefaultEncoding(id3v2.EncodingUTF8)
	}

	applyTextChanges(tag, changes)
	applyCoverArtChanges(tag, changes)

	// Serialize tag to bytes.
	var id3Buf bytes.Buffer
	if _, err := tag.WriteTo(&id3Buf); err != nil {
		return fmt.Errorf("serialize ID3v2 tag: %w", err)
	}

	return fileutil.AtomicWrite(logger, filePath, func(tmp *os.File) error {
		return writeRIFF(tmp, preserved, id3Buf.Bytes())
	})
}
