package tagwriter

import (
	"fmt"
	"log/slog"
	"os"
	"strconv"
	"strings"

	"github.com/go-flac/flacpicture/v2"
	"github.com/go-flac/flacvorbis/v2"
	flac "github.com/go-flac/go-flac/v2"

	"yellowjacket/backend/fileutil"
)

// writeFlacTags writes metadata tags to a FLAC file using Vorbis Comments
// and PICTURE metadata blocks, integrated with AtomicWrite for crash safety.
func writeFlacTags(logger *slog.Logger, filePath string, changes TagChanges) error {
	// Check file size and warn for very large files.
	if info, err := os.Stat(filePath); err == nil {
		const largeSizeThreshold = 500 * 1024 * 1024 // 500 MB
		if info.Size() > largeSizeThreshold {
			logger.Warn("large FLAC file may use significant memory",
				slog.String("path", filePath),
				slog.Int64("size", info.Size()),
			)
		}
	}

	f, err := flac.ParseFile(filePath)
	if err != nil {
		return fmt.Errorf("parse flac: %w", err)
	}

	// Find existing Vorbis Comment block.
	var cmt *flacvorbis.MetaDataBlockVorbisComment

	cmtIdx := -1

	for idx, meta := range f.Meta {
		if meta.Type == flac.VorbisComment {
			cmt, err = flacvorbis.ParseFromMetaDataBlock(*meta)
			if err != nil {
				return fmt.Errorf("parse vorbis comments: %w", err)
			}

			cmtIdx = idx

			break
		}
	}

	if cmt == nil {
		cmt = flacvorbis.New()
	}

	// Apply text changes from the diff map.
	if err := applyFlacTextChanges(cmt, changes); err != nil {
		return fmt.Errorf("apply flac text changes: %w", err)
	}

	// Marshal Vorbis Comment block back and update f.Meta.
	cmtMeta := cmt.Marshal()
	if cmtIdx >= 0 {
		f.Meta[cmtIdx] = &cmtMeta
	} else {
		f.Meta = append(f.Meta, &cmtMeta)
	}

	// Handle cover art — PICTURE metadata block.
	if err := applyFlacCoverArt(f, changes); err != nil {
		return fmt.Errorf("apply flac cover art: %w", err)
	}

	// Write atomically via AtomicWrite.
	return fileutil.AtomicWrite(logger, filePath, func(tmp *os.File) error {
		_, writeErr := f.WriteTo(tmp)
		if writeErr != nil {
			return fmt.Errorf("write flac: %w", writeErr)
		}

		return nil
	})
}

// applyFlacTextChanges applies text field changes to a Vorbis Comment block.
func applyFlacTextChanges(cmt *flacvorbis.MetaDataBlockVorbisComment, changes TagChanges) error {
	type fieldMapping struct {
		key      string
		vorbisID string
		isInt    bool
	}

	mappings := []fieldMapping{
		{FieldTitle, flacvorbis.FIELD_TITLE, false},
		{FieldArtist, flacvorbis.FIELD_ARTIST, false},
		{FieldAlbum, flacvorbis.FIELD_ALBUM, false},
		{FieldAlbumArtist, "ALBUMARTIST", false},
		{FieldGenre, flacvorbis.FIELD_GENRE, false},
		{FieldYear, flacvorbis.FIELD_DATE, true},
		{FieldTrackNumber, flacvorbis.FIELD_TRACKNUMBER, true},
		{FieldDiscNumber, "DISCNUMBER", true},
		// TRACKTOTAL/DISCTOTAL and no other spelling: dhowden/tag's
		// Vorbis reader looks at exactly these two keys, so TOTALTRACKS
		// or a "1/12" inside TRACKNUMBER reads back as no total at all.
		{FieldTotalTracks, "TRACKTOTAL", true},
		{FieldTotalDiscs, "DISCTOTAL", true},
		{FieldComposer, "COMPOSER", false},
	}

	for _, m := range mappings {
		v, ok := changes[m.key]
		if !ok {
			continue
		}

		var val string

		if m.isInt {
			n, ok := asInt(v)
			if !ok {
				continue
			}

			val = strconv.Itoa(n)
		} else {
			s, ok := v.(string)
			if !ok {
				continue
			}

			val = s
		}

		replaceVorbisComment(cmt, m.vorbisID, val)
	}

	return nil
}

// replaceVorbisComment removes all existing entries for a field and adds a
// new value. Vorbis Comment field names are case-insensitive per spec.
func replaceVorbisComment(cmt *flacvorbis.MetaDataBlockVorbisComment, field string, value string) {
	// Remove all existing entries for this field (case-insensitive).
	prefix := strings.ToUpper(field) + "="
	filtered := make([]string, 0, len(cmt.Comments))

	for _, c := range cmt.Comments {
		if !strings.HasPrefix(strings.ToUpper(c), prefix) {
			filtered = append(filtered, c)
		}
	}

	cmt.Comments = filtered

	// Add the new value. Using the uppercase field name (Vorbis convention).
	_ = cmt.Add(strings.ToUpper(field), value)
}

// applyFlacCoverArt handles adding, replacing, or clearing PICTURE metadata
// blocks in a FLAC file.
func applyFlacCoverArt(f *flac.File, changes TagChanges) error {
	v, ok := changes[FieldCoverArt]
	if !ok {
		return nil
	}

	// Remove all existing PICTURE blocks.
	newMeta := make([]*flac.MetaDataBlock, 0, len(f.Meta))

	for _, meta := range f.Meta {
		if meta.Type != flac.Picture {
			newMeta = append(newMeta, meta)
		}
	}

	f.Meta = newMeta

	// If value is nil or empty, we've cleared the art — done.
	data, isBytes := asBytes(v)
	if !isBytes || len(data) == 0 {
		return nil
	}

	// Create new PICTURE block with the provided image data.
	pic, err := flacpicture.NewFromImageData(
		flacpicture.PictureTypeFrontCover,
		"Front cover",
		data,
		detectMIME(data),
	)
	if err != nil {
		return fmt.Errorf("create flac picture: %w", err)
	}

	picMeta := pic.Marshal()
	f.Meta = append(f.Meta, &picMeta)

	return nil
}
