package gobackend

import (
	"bytes"
	"encoding/binary"
	"fmt"
	stdimage "image"
	_ "image/gif"
	_ "image/jpeg"
	_ "image/png"
	"io"
	"math"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"

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

const artistTagModeSplitVorbis = "split_vorbis"

var artistTagSplitPattern = regexp.MustCompile(`\s*(?:,|&|\bx\b)\s*|\s+\b(?:feat(?:uring)?|ft|with)\.?\s*`)

func detectCoverMIME(coverPath string, coverData []byte) string {
	// Prefer magic-byte detection over file extension.
	// Some providers return non-JPEG data behind .jpg URLs.
	if len(coverData) >= 8 &&
		coverData[0] == 0x89 &&
		coverData[1] == 0x50 &&
		coverData[2] == 0x4E &&
		coverData[3] == 0x47 &&
		coverData[4] == 0x0D &&
		coverData[5] == 0x0A &&
		coverData[6] == 0x1A &&
		coverData[7] == 0x0A {
		return "image/png"
	}
	if len(coverData) >= 3 &&
		coverData[0] == 0xFF &&
		coverData[1] == 0xD8 &&
		coverData[2] == 0xFF {
		return "image/jpeg"
	}
	if len(coverData) >= 6 {
		header := string(coverData[:6])
		if header == "GIF87a" || header == "GIF89a" {
			return "image/gif"
		}
	}
	if len(coverData) >= 12 &&
		string(coverData[:4]) == "RIFF" &&
		string(coverData[8:12]) == "WEBP" {
		return "image/webp"
	}

	switch strings.ToLower(filepath.Ext(strings.TrimSpace(coverPath))) {
	case ".png":
		return "image/png"
	case ".jpg", ".jpeg":
		return "image/jpeg"
	case ".webp":
		return "image/webp"
	case ".gif":
		return "image/gif"
	}

	return "image/jpeg"
}

func buildPictureBlock(coverPath string, coverData []byte) (flac.MetaDataBlock, error) {
	if len(coverData) == 0 {
		return flac.MetaDataBlock{}, fmt.Errorf("empty cover data")
	}

	mime := detectCoverMIME(coverPath, coverData)
	picture := &flacpicture.MetadataBlockPicture{
		PictureType: flacpicture.PictureTypeFrontCover,
		MIME:        mime,
		Description: "Front Cover",
		ImageData:   coverData,
	}

	// Width/height/depth are optional in practice; keep zero when decode fails.
	if cfg, format, err := stdimage.DecodeConfig(bytes.NewReader(coverData)); err == nil {
		picture.Width = uint32(cfg.Width)
		picture.Height = uint32(cfg.Height)
		switch format {
		case "png":
			picture.ColorDepth = 32
		case "jpeg":
			picture.ColorDepth = 24
		default:
			picture.ColorDepth = 0
		}
	}

	return picture.Marshal(), nil
}

type Metadata struct {
	Title         string
	Artist        string
	Album         string
	AlbumArtist   string
	ArtistTagMode string
	Date          string
	TrackNumber   int
	TotalTracks   int
	DiscNumber    int
	TotalDiscs    int
	ISRC          string
	Description   string
	Lyrics        string
	Genre         string
	Label         string
	Copyright     string
	Composer      string
	Comment       string

	// ReplayGain fields (stored as Vorbis Comments in FLAC)
	ReplayGainTrackGain string // e.g. "-6.50 dB"
	ReplayGainTrackPeak string // e.g. "0.988831"
	ReplayGainAlbumGain string // e.g. "-7.20 dB"
	ReplayGainAlbumPeak string // e.g. "1.000000"
}

func EmbedMetadata(filePath string, metadata Metadata, coverPath string) error {
	f, err := flac.ParseFile(filePath)
	if err != nil {
		return fmt.Errorf("failed to parse FLAC file: %w", err)
	}

	var cmtIdx int = -1
	var cmt *flacvorbis.MetaDataBlockVorbisComment

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

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

	writeVorbisMetadata(cmt, metadata)

	cmtBlock := cmt.Marshal()
	if cmtIdx >= 0 {
		f.Meta[cmtIdx] = &cmtBlock
	} else {
		f.Meta = append(f.Meta, &cmtBlock)
	}

	if coverPath != "" {
		if fileExists(coverPath) {
			coverData, err := os.ReadFile(coverPath)
			if err != nil {
				fmt.Printf("[Metadata] Warning: Failed to read cover file %s: %v\n", coverPath, err)
			} else {
				for i := len(f.Meta) - 1; i >= 0; i-- {
					if f.Meta[i].Type == flac.Picture {
						f.Meta = append(f.Meta[:i], f.Meta[i+1:]...)
					}
				}

				picBlock, err := buildPictureBlock(coverPath, coverData)
				if err != nil {
					return fmt.Errorf("failed to create picture block: %w", err)
				}
				f.Meta = append(f.Meta, &picBlock)
				fmt.Printf("[Metadata] Cover art embedded successfully (%d bytes)\n", len(coverData))
			}
		} else {
			fmt.Printf("[Metadata] Warning: Cover file does not exist: %s\n", coverPath)
		}
	}

	return f.Save(filePath)
}

func EmbedMetadataWithCoverData(filePath string, metadata Metadata, coverData []byte) error {
	f, err := flac.ParseFile(filePath)
	if err != nil {
		return fmt.Errorf("failed to parse FLAC file: %w", err)
	}

	var cmtIdx int = -1
	var cmt *flacvorbis.MetaDataBlockVorbisComment

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

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

	writeVorbisMetadata(cmt, metadata)

	cmtBlock := cmt.Marshal()
	if cmtIdx >= 0 {
		f.Meta[cmtIdx] = &cmtBlock
	} else {
		f.Meta = append(f.Meta, &cmtBlock)
	}

	if len(coverData) > 0 {
		for i := len(f.Meta) - 1; i >= 0; i-- {
			if f.Meta[i].Type == flac.Picture {
				f.Meta = append(f.Meta[:i], f.Meta[i+1:]...)
			}
		}

		picBlock, err := buildPictureBlock("", coverData)
		if err != nil {
			return fmt.Errorf("failed to create picture block: %w", err)
		}
		f.Meta = append(f.Meta, &picBlock)
		fmt.Printf("[Metadata] Cover art embedded successfully (%d bytes)\n", len(coverData))
	}

	return f.Save(filePath)
}

func ReadMetadata(filePath string) (*Metadata, error) {
	f, err := flac.ParseFile(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to parse FLAC file: %w", err)
	}

	metadata := &Metadata{}

	for _, meta := range f.Meta {
		if meta.Type == flac.VorbisComment {
			cmt, err := flacvorbis.ParseFromMetaDataBlock(*meta)
			if err != nil {
				continue
			}

			metadata.Title = getComment(cmt, "TITLE")
			metadata.Artist = getJoinedComment(cmt, "ARTIST")
			metadata.Album = getComment(cmt, "ALBUM")
			metadata.AlbumArtist = getJoinedComment(cmt, "ALBUMARTIST")
			if metadata.AlbumArtist == "" {
				metadata.AlbumArtist = getJoinedComment(cmt, "ALBUM ARTIST")
			}
			if metadata.AlbumArtist == "" {
				metadata.AlbumArtist = getJoinedComment(cmt, "ALBUM_ARTIST")
			}
			metadata.Date = getComment(cmt, "DATE")
			metadata.ISRC = getComment(cmt, "ISRC")
			metadata.Description = getComment(cmt, "DESCRIPTION")

			metadata.Lyrics = getComment(cmt, "LYRICS")
			if metadata.Lyrics == "" {
				metadata.Lyrics = getComment(cmt, "UNSYNCEDLYRICS")
			}

			trackNum := getComment(cmt, "TRACKNUMBER")
			if trackNum != "" {
				metadata.TrackNumber, metadata.TotalTracks = parseIndexPair(trackNum)
			}
			if metadata.TrackNumber == 0 {
				trackNum = getComment(cmt, "TRACK")
				if trackNum != "" {
					metadata.TrackNumber, metadata.TotalTracks = parseIndexPair(trackNum)
				}
			}

			discNum := getComment(cmt, "DISCNUMBER")
			if discNum != "" {
				metadata.DiscNumber, metadata.TotalDiscs = parseIndexPair(discNum)
			}
			if metadata.DiscNumber == 0 {
				discNum = getComment(cmt, "DISC")
				if discNum != "" {
					metadata.DiscNumber, metadata.TotalDiscs = parseIndexPair(discNum)
				}
			}

			if metadata.Date == "" {
				metadata.Date = getComment(cmt, "YEAR")
			}

			metadata.Genre = getComment(cmt, "GENRE")
			metadata.Label = getComment(cmt, "ORGANIZATION")
			if metadata.Label == "" {
				metadata.Label = getComment(cmt, "LABEL")
			}
			if metadata.Label == "" {
				metadata.Label = getComment(cmt, "PUBLISHER")
			}
			metadata.Copyright = getComment(cmt, "COPYRIGHT")
			metadata.Composer = getComment(cmt, "COMPOSER")
			metadata.Comment = getComment(cmt, "COMMENT")

			metadata.ReplayGainTrackGain = getComment(cmt, "REPLAYGAIN_TRACK_GAIN")
			metadata.ReplayGainTrackPeak = getComment(cmt, "REPLAYGAIN_TRACK_PEAK")
			metadata.ReplayGainAlbumGain = getComment(cmt, "REPLAYGAIN_ALBUM_GAIN")
			metadata.ReplayGainAlbumPeak = getComment(cmt, "REPLAYGAIN_ALBUM_PEAK")

			break
		}
	}

	return metadata, nil
}

// EditFlacFields opens a FLAC file and updates only the Vorbis Comment keys
// that are explicitly present in the fields map.  Keys present with a non-empty
// value are set; keys present with an empty value are removed (cleared).  Keys
// absent from the map are left untouched.  This is the correct function for
// partial edits (e.g. writing only ReplayGain tags) and full editor saves alike.
func EditFlacFields(filePath string, fields map[string]string) error {
	f, err := flac.ParseFile(filePath)
	if err != nil {
		return fmt.Errorf("failed to parse FLAC file: %w", err)
	}

	var cmtIdx int = -1
	var cmt *flacvorbis.MetaDataBlockVorbisComment

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

	artistMode := fields["artist_tag_mode"]

	// Mapping from fields-map key → one or more Vorbis Comment keys.
	// Each entry is handled with set-or-clear semantics.
	simpleKeys := map[string]string{
		"title":                 "TITLE",
		"album":                 "ALBUM",
		"date":                  "DATE",
		"isrc":                  "ISRC",
		"genre":                 "GENRE",
		"label":                 "ORGANIZATION",
		"copyright":             "COPYRIGHT",
		"composer":              "COMPOSER",
		"comment":               "COMMENT",
		"replaygain_track_gain": "REPLAYGAIN_TRACK_GAIN",
		"replaygain_track_peak": "REPLAYGAIN_TRACK_PEAK",
		"replaygain_album_gain": "REPLAYGAIN_ALBUM_GAIN",
		"replaygain_album_peak": "REPLAYGAIN_ALBUM_PEAK",
	}

	for fieldKey, vorbisKey := range simpleKeys {
		if v, ok := fields[fieldKey]; ok {
			setOrClearComment(cmt, vorbisKey, v)
		}
	}

	// Remove known aliases for fields that were just written/cleared, so that
	// tags from other taggers (e.g. LABEL, PUBLISHER, ALBUM ARTIST) don't
	// conflict with the canonical keys we use.
	aliasCleanup := map[string][]string{
		"label":     {"LABEL", "PUBLISHER"}, // canonical: ORGANIZATION
		"date":      {"YEAR"},               // canonical: DATE
		"genre":     {},                     // no common aliases
		"copyright": {},
	}
	for fieldKey, aliases := range aliasCleanup {
		if _, ok := fields[fieldKey]; ok {
			for _, alias := range aliases {
				removeCommentKey(cmt, alias)
			}
		}
	}

	// Artist fields: use split-artist logic when mode is set.
	if v, ok := fields["artist"]; ok {
		setOrClearArtistComments(cmt, "ARTIST", v, artistMode)
	}
	if v, ok := fields["album_artist"]; ok {
		setOrClearArtistComments(cmt, "ALBUMARTIST", v, artistMode)
		// Remove aliases from other taggers.
		removeCommentKey(cmt, "ALBUM ARTIST")
		removeCommentKey(cmt, "ALBUM_ARTIST")
	}

	// Track/disc numbers: present + empty → clear; when only totals are edited,
	// preserve the current index number and rewrite the combined value.
	if _, ok := fields["track_number"]; ok || fields["track_total"] != "" || hasMapKey(fields, "track_total") {
		currentTrackNum, currentTotalTracks := parseIndexPair(getComment(cmt, "TRACKNUMBER"))
		if currentTrackNum == 0 && currentTotalTracks == 0 {
			currentTrackNum, currentTotalTracks = parseIndexPair(getComment(cmt, "TRACK"))
		}
		if v, ok := fields["track_number"]; ok {
			currentTrackNum = parsePositiveInt(v)
		}
		if v, ok := fields["track_total"]; ok {
			currentTotalTracks = parsePositiveInt(v)
		}
		if currentTrackNum > 0 {
			setOrClearComment(cmt, "TRACKNUMBER", formatIndexValue(currentTrackNum, currentTotalTracks))
		} else {
			removeCommentKey(cmt, "TRACKNUMBER")
		}
		removeCommentKey(cmt, "TRACK") // alias
	}
	if _, ok := fields["disc_number"]; ok || fields["disc_total"] != "" || hasMapKey(fields, "disc_total") {
		currentDiscNum, currentTotalDiscs := parseIndexPair(getComment(cmt, "DISCNUMBER"))
		if currentDiscNum == 0 && currentTotalDiscs == 0 {
			currentDiscNum, currentTotalDiscs = parseIndexPair(getComment(cmt, "DISC"))
		}
		if v, ok := fields["disc_number"]; ok {
			currentDiscNum = parsePositiveInt(v)
		}
		if v, ok := fields["disc_total"]; ok {
			currentTotalDiscs = parsePositiveInt(v)
		}
		if currentDiscNum > 0 {
			setOrClearComment(cmt, "DISCNUMBER", formatIndexValue(currentDiscNum, currentTotalDiscs))
		} else {
			removeCommentKey(cmt, "DISCNUMBER")
		}
		removeCommentKey(cmt, "DISC") // alias
	}

	// Lyrics: set both LYRICS + UNSYNCEDLYRICS, or clear both.
	if v, ok := fields["lyrics"]; ok {
		if v != "" {
			setOrClearComment(cmt, "LYRICS", v)
			setOrClearComment(cmt, "UNSYNCEDLYRICS", v)
		} else {
			removeCommentKey(cmt, "LYRICS")
			removeCommentKey(cmt, "UNSYNCEDLYRICS")
		}
	}

	cmtBlock := cmt.Marshal()
	if cmtIdx >= 0 {
		f.Meta[cmtIdx] = &cmtBlock
	} else {
		f.Meta = append(f.Meta, &cmtBlock)
	}

	coverPath := strings.TrimSpace(fields["cover_path"])
	if coverPath != "" && fileExists(coverPath) {
		coverData, err := os.ReadFile(coverPath)
		if err == nil && len(coverData) > 0 {
			for i := len(f.Meta) - 1; i >= 0; i-- {
				if f.Meta[i].Type == flac.Picture {
					f.Meta = append(f.Meta[:i], f.Meta[i+1:]...)
				}
			}
			picBlock, err := buildPictureBlock("", coverData)
			if err == nil {
				f.Meta = append(f.Meta, &picBlock)
			}
		}
	}

	return f.Save(filePath)
}

// writeVorbisMetadata writes all metadata fields to a Vorbis Comment block.
// Empty/zero values are simply skipped (not written, not cleared).  This is
// used by the download embedding path where absent fields should preserve any
// existing values.  The editor path uses EditFlacFields() instead.
func writeVorbisMetadata(cmt *flacvorbis.MetaDataBlockVorbisComment, metadata Metadata) {
	setComment(cmt, "TITLE", metadata.Title)
	setArtistComments(cmt, "ARTIST", metadata.Artist, metadata.ArtistTagMode)
	setComment(cmt, "ALBUM", metadata.Album)
	setArtistComments(cmt, "ALBUMARTIST", metadata.AlbumArtist, metadata.ArtistTagMode)
	setComment(cmt, "DATE", metadata.Date)

	if metadata.TrackNumber > 0 {
		setComment(cmt, "TRACKNUMBER", formatIndexValue(metadata.TrackNumber, metadata.TotalTracks))
	}

	if metadata.DiscNumber > 0 {
		setComment(cmt, "DISCNUMBER", formatIndexValue(metadata.DiscNumber, metadata.TotalDiscs))
	}

	if metadata.ISRC != "" {
		setComment(cmt, "ISRC", metadata.ISRC)
	}

	if metadata.Description != "" {
		setComment(cmt, "DESCRIPTION", metadata.Description)
	}

	if metadata.Lyrics != "" {
		setComment(cmt, "LYRICS", metadata.Lyrics)
		setComment(cmt, "UNSYNCEDLYRICS", metadata.Lyrics)
	}

	if metadata.Genre != "" {
		setComment(cmt, "GENRE", metadata.Genre)
	}

	if metadata.Label != "" {
		setComment(cmt, "ORGANIZATION", metadata.Label)
	}

	if metadata.Copyright != "" {
		setComment(cmt, "COPYRIGHT", metadata.Copyright)
	}

	if metadata.Composer != "" {
		setComment(cmt, "COMPOSER", metadata.Composer)
	}

	if metadata.Comment != "" {
		setComment(cmt, "COMMENT", metadata.Comment)
	}

	setComment(cmt, "REPLAYGAIN_TRACK_GAIN", metadata.ReplayGainTrackGain)
	setComment(cmt, "REPLAYGAIN_TRACK_PEAK", metadata.ReplayGainTrackPeak)
	setComment(cmt, "REPLAYGAIN_ALBUM_GAIN", metadata.ReplayGainAlbumGain)
	setComment(cmt, "REPLAYGAIN_ALBUM_PEAK", metadata.ReplayGainAlbumPeak)
}

func setComment(cmt *flacvorbis.MetaDataBlockVorbisComment, key, value string) {
	if value == "" {
		return
	}
	removeCommentKey(cmt, key)
	cmt.Comments = append(cmt.Comments, key+"="+value)
}

// setOrClearComment writes a Vorbis Comment, or removes the key if value is
// empty.  Used by the metadata editor path where empty means "delete this tag".
func setOrClearComment(cmt *flacvorbis.MetaDataBlockVorbisComment, key, value string) {
	if value == "" {
		removeCommentKey(cmt, key)
		return
	}
	removeCommentKey(cmt, key)
	cmt.Comments = append(cmt.Comments, key+"="+value)
}

func setArtistComments(cmt *flacvorbis.MetaDataBlockVorbisComment, key, value, mode string) {
	if value == "" {
		return
	}
	values := []string{value}
	if shouldSplitVorbisArtistTags(mode) {
		values = splitArtistTagValues(value)
	}
	if len(values) == 0 {
		return
	}
	removeCommentKey(cmt, key)
	for _, artist := range values {
		if strings.TrimSpace(artist) == "" {
			continue
		}
		cmt.Comments = append(cmt.Comments, key+"="+artist)
	}
}

// setOrClearArtistComments writes artist Vorbis Comments, or removes the key
// if value is empty.  Used by the metadata editor path.
func setOrClearArtistComments(cmt *flacvorbis.MetaDataBlockVorbisComment, key, value, mode string) {
	if value == "" {
		removeCommentKey(cmt, key)
		return
	}
	values := []string{value}
	if shouldSplitVorbisArtistTags(mode) {
		values = splitArtistTagValues(value)
	}
	if len(values) == 0 {
		removeCommentKey(cmt, key)
		return
	}
	removeCommentKey(cmt, key)
	for _, artist := range values {
		if strings.TrimSpace(artist) == "" {
			continue
		}
		cmt.Comments = append(cmt.Comments, key+"="+artist)
	}
}

// RewriteSplitArtistTags opens a FLAC file and rewrites the ARTIST and
// ALBUMARTIST Vorbis comments as multiple separate entries (one per artist).
// This is needed because FFmpeg's -metadata flag deduplicates keys, so only
// the last value survives when multiple -metadata ARTIST=X flags are used.
// The native go-flac writer correctly handles multiple Vorbis comments.
func RewriteSplitArtistTags(filePath, artist, albumArtist string) error {
	if !shouldSplitVorbisArtistTags(artistTagModeSplitVorbis) {
		return nil
	}

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

	var cmtIdx int = -1
	var cmt *flacvorbis.MetaDataBlockVorbisComment

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

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

	setArtistComments(cmt, "ARTIST", artist, artistTagModeSplitVorbis)
	setArtistComments(cmt, "ALBUMARTIST", albumArtist, artistTagModeSplitVorbis)

	cmtMeta := cmt.Marshal()
	if cmtIdx >= 0 {
		f.Meta[cmtIdx] = &cmtMeta
	} else {
		f.Meta = append(f.Meta, &cmtMeta)
	}

	return f.Save(filePath)
}

func removeCommentKey(cmt *flacvorbis.MetaDataBlockVorbisComment, key string) {
	keyUpper := strings.ToUpper(key)
	for i := len(cmt.Comments) - 1; i >= 0; i-- {
		comment := cmt.Comments[i]
		eqIdx := strings.Index(comment, "=")
		if eqIdx > 0 {
			existingKey := strings.ToUpper(comment[:eqIdx])
			if existingKey == keyUpper {
				cmt.Comments = append(cmt.Comments[:i], cmt.Comments[i+1:]...)
			}
		}
	}
}

func getComment(cmt *flacvorbis.MetaDataBlockVorbisComment, key string) string {
	values := getCommentValues(cmt, key)
	if len(values) == 0 {
		return ""
	}
	return values[0]
}

func getJoinedComment(cmt *flacvorbis.MetaDataBlockVorbisComment, key string) string {
	return joinVorbisCommentValues(getCommentValues(cmt, key))
}

func getCommentValues(cmt *flacvorbis.MetaDataBlockVorbisComment, key string) []string {
	keyUpper := strings.ToUpper(key) + "="
	values := make([]string, 0, 1)
	for _, comment := range cmt.Comments {
		if len(comment) > len(key) {
			commentUpper := strings.ToUpper(comment[:len(key)+1])
			if commentUpper == keyUpper {
				values = append(values, comment[len(key)+1:])
			}
		}
	}
	return values
}

func shouldSplitVorbisArtistTags(mode string) bool {
	return strings.EqualFold(strings.TrimSpace(mode), artistTagModeSplitVorbis)
}

func splitArtistTagValues(rawArtists string) []string {
	trimmed := strings.TrimSpace(rawArtists)
	if trimmed == "" {
		return nil
	}

	parts := artistTagSplitPattern.Split(trimmed, -1)
	values := make([]string, 0, len(parts))
	seen := make(map[string]struct{}, len(parts))
	for _, part := range parts {
		artist := strings.TrimSpace(part)
		if artist == "" {
			continue
		}
		key := strings.ToLower(artist)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		values = append(values, artist)
	}
	if len(values) > 0 {
		return values
	}
	return []string{trimmed}
}

func joinVorbisCommentValues(values []string) string {
	if len(values) == 0 {
		return ""
	}

	joined := make([]string, 0, len(values))
	seen := make(map[string]struct{}, len(values))
	for _, value := range values {
		trimmed := strings.TrimSpace(value)
		if trimmed == "" {
			continue
		}
		key := strings.ToLower(trimmed)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		joined = append(joined, trimmed)
	}
	return strings.Join(joined, ", ")
}

func fileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}

func ExtractCoverArt(filePath string) ([]byte, error) {
	f, err := flac.ParseFile(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to parse FLAC file: %w", err)
	}

	for _, meta := range f.Meta {
		if meta.Type == flac.Picture {
			pic, err := flacpicture.ParseFromMetaDataBlock(*meta)
			if err != nil {
				continue
			}
			if pic.PictureType == flacpicture.PictureTypeFrontCover && len(pic.ImageData) > 0 {
				return pic.ImageData, nil
			}
		}
	}

	for _, meta := range f.Meta {
		if meta.Type == flac.Picture {
			pic, err := flacpicture.ParseFromMetaDataBlock(*meta)
			if err != nil {
				continue
			}
			if len(pic.ImageData) > 0 {
				return pic.ImageData, nil
			}
		}
	}

	return nil, fmt.Errorf("no cover art found in file")
}

func EmbedLyrics(filePath string, lyrics string) error {
	f, err := flac.ParseFile(filePath)
	if err != nil {
		return fmt.Errorf("failed to parse FLAC file: %w", err)
	}

	var cmtIdx int = -1
	var cmt *flacvorbis.MetaDataBlockVorbisComment

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

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

	setComment(cmt, "LYRICS", lyrics)
	setComment(cmt, "UNSYNCEDLYRICS", lyrics)

	cmtBlock := cmt.Marshal()
	if cmtIdx >= 0 {
		f.Meta[cmtIdx] = &cmtBlock
	} else {
		f.Meta = append(f.Meta, &cmtBlock)
	}

	return f.Save(filePath)
}

func EmbedGenreLabel(filePath string, genre, label string) error {
	if genre == "" && label == "" {
		return nil
	}

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

	var cmtIdx int = -1
	var cmt *flacvorbis.MetaDataBlockVorbisComment

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

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

	if genre != "" {
		setComment(cmt, "GENRE", genre)
	}
	if label != "" {
		setComment(cmt, "ORGANIZATION", label)
	}

	cmtBlock := cmt.Marshal()
	if cmtIdx >= 0 {
		f.Meta[cmtIdx] = &cmtBlock
	} else {
		f.Meta = append(f.Meta, &cmtBlock)
	}

	return f.Save(filePath)
}

func ExtractLyrics(filePath string) (string, error) {
	lower := strings.ToLower(filePath)

	if strings.HasSuffix(lower, ".flac") {
		lyrics, err := extractLyricsFromFlac(filePath)
		if err == nil && strings.TrimSpace(lyrics) != "" {
			return lyrics, nil
		}
		return extractLyricsFromSidecarLRC(filePath)
	}

	if strings.HasSuffix(lower, ".m4a") || strings.HasSuffix(lower, ".mp4") || strings.HasSuffix(lower, ".aac") {
		lyrics, err := extractLyricsFromM4A(filePath)
		if err == nil && strings.TrimSpace(lyrics) != "" {
			return lyrics, nil
		}
		return extractLyricsFromSidecarLRC(filePath)
	}

	if strings.HasSuffix(lower, ".mp3") {
		meta, err := ReadID3Tags(filePath)
		if err == nil && meta != nil {
			if strings.TrimSpace(meta.Lyrics) != "" {
				return meta.Lyrics, nil
			}
			if looksLikeEmbeddedLyrics(meta.Comment) {
				return meta.Comment, nil
			}
		}
		return extractLyricsFromSidecarLRC(filePath)
	}

	if strings.HasSuffix(lower, ".opus") || strings.HasSuffix(lower, ".ogg") {
		meta, err := ReadOggVorbisComments(filePath)
		if err == nil && meta != nil {
			if strings.TrimSpace(meta.Lyrics) != "" {
				return meta.Lyrics, nil
			}
			if looksLikeEmbeddedLyrics(meta.Comment) {
				return meta.Comment, nil
			}
		}
		return extractLyricsFromSidecarLRC(filePath)
	}

	return extractLyricsFromSidecarLRC(filePath)
}

func ReadM4ATags(filePath string) (*AudioMetadata, error) {
	f, err := os.Open(filePath)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	fi, err := f.Stat()
	if err != nil {
		return nil, err
	}

	ilst, err := findM4AIlstAtom(f, fi.Size())
	if err != nil {
		return nil, err
	}

	metadata := &AudioMetadata{}
	start := ilst.offset + ilst.headerSize
	end := ilst.offset + ilst.size
	for pos := start; pos+8 <= end; {
		header, err := readAtomHeaderAt(f, pos, fi.Size())
		if err != nil {
			return nil, err
		}
		if header.size == 0 {
			header.size = end - pos
		}
		if header.size < header.headerSize {
			return nil, fmt.Errorf("invalid atom size for %s", header.typ)
		}

		switch header.typ {
		case "\xa9nam":
			metadata.Title, _ = readM4ATextValue(f, header, fi.Size())
		case "\xa9ART":
			metadata.Artist, _ = readM4ATextValue(f, header, fi.Size())
		case "\xa9alb":
			metadata.Album, _ = readM4ATextValue(f, header, fi.Size())
		case "aART":
			metadata.AlbumArtist, _ = readM4ATextValue(f, header, fi.Size())
		case "\xa9day":
			metadata.Date, _ = readM4ATextValue(f, header, fi.Size())
			metadata.Year = metadata.Date
		case "\xa9gen":
			metadata.Genre, _ = readM4ATextValue(f, header, fi.Size())
		case "\xa9wrt":
			metadata.Composer, _ = readM4ATextValue(f, header, fi.Size())
		case "\xa9cmt":
			metadata.Comment, _ = readM4ATextValue(f, header, fi.Size())
		case "cprt":
			metadata.Copyright, _ = readM4ATextValue(f, header, fi.Size())
		case "\xa9lyr":
			metadata.Lyrics, _ = readM4ATextValue(f, header, fi.Size())
		case "trkn":
			metadata.TrackNumber, metadata.TotalTracks, _ = readM4AIndexPair(f, header, fi.Size())
		case "disk":
			metadata.DiscNumber, metadata.TotalDiscs, _ = readM4AIndexPair(f, header, fi.Size())
		case "----":
			name, value, freeformErr := readM4AFreeformValue(f, header, fi.Size())
			if freeformErr == nil {
				switch strings.ToUpper(strings.TrimSpace(name)) {
				case "ISRC":
					metadata.ISRC = value
				case "LABEL", "ORGANIZATION":
					metadata.Label = value
				case "COMMENT":
					if metadata.Comment == "" {
						metadata.Comment = value
					}
				case "COMPOSER":
					if metadata.Composer == "" {
						metadata.Composer = value
					}
				case "COPYRIGHT":
					if metadata.Copyright == "" {
						metadata.Copyright = value
					}
				case "LYRICS", "UNSYNCEDLYRICS":
					if metadata.Lyrics == "" {
						metadata.Lyrics = value
					}
				case "REPLAYGAIN_TRACK_GAIN":
					metadata.ReplayGainTrackGain = value
				case "REPLAYGAIN_TRACK_PEAK":
					metadata.ReplayGainTrackPeak = value
				case "REPLAYGAIN_ALBUM_GAIN":
					metadata.ReplayGainAlbumGain = value
				case "REPLAYGAIN_ALBUM_PEAK":
					metadata.ReplayGainAlbumPeak = value
				}
			}
		}

		pos += header.size
	}

	if metadata.Title == "" &&
		metadata.Artist == "" &&
		metadata.Album == "" &&
		metadata.AlbumArtist == "" &&
		metadata.Lyrics == "" &&
		metadata.TrackNumber == 0 &&
		metadata.DiscNumber == 0 {
		return nil, fmt.Errorf("no M4A tags found")
	}

	return metadata, nil
}

func extractLyricsFromM4A(filePath string) (string, error) {
	metadata, err := ReadM4ATags(filePath)
	if err != nil {
		return "", err
	}
	if metadata == nil || strings.TrimSpace(metadata.Lyrics) == "" {
		return "", fmt.Errorf("no lyrics found in file")
	}
	return metadata.Lyrics, nil
}

func extractCoverFromM4A(filePath string) ([]byte, error) {
	f, err := os.Open(filePath)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	fi, err := f.Stat()
	if err != nil {
		return nil, err
	}
	fileSize := fi.Size()

	ilst, err := findM4AIlstAtom(f, fileSize)
	if err != nil {
		return nil, err
	}

	bodyStart := ilst.offset + ilst.headerSize
	bodySize := ilst.size - ilst.headerSize

	covr, found, err := findAtomInRange(f, bodyStart, bodySize, "covr", fileSize)
	if err != nil || !found {
		return nil, fmt.Errorf("cover atom not found")
	}

	dataStart := covr.offset + covr.headerSize
	dataSize := covr.size - covr.headerSize

	dataAtom, found, err := findAtomInRange(f, dataStart, dataSize, "data", fileSize)
	if err != nil || !found {
		return nil, fmt.Errorf("data atom not found in cover")
	}

	// data atom: header + 4 bytes type indicator + 4 bytes locale
	imgStart := dataAtom.offset + dataAtom.headerSize + 8
	imgLen := dataAtom.size - dataAtom.headerSize - 8
	if imgLen <= 0 {
		return nil, fmt.Errorf("empty cover data")
	}

	buf := make([]byte, imgLen)
	if _, err := f.ReadAt(buf, imgStart); err != nil {
		return nil, err
	}

	return buf, nil
}

// findM4AIlstAtom locates the ilst atom that holds all iTunes-style tags.
// It tries two common layouts:
//  1. moov > udta > meta > ilst  (iTunes, FFmpeg default)
//  2. moov > meta > ilst         (some encoders omit the udta wrapper)
func findM4AIlstAtom(f *os.File, fileSize int64) (atomHeader, error) {
	moov, found, err := findAtomInRange(f, 0, fileSize, "moov", fileSize)
	if err != nil || !found {
		return atomHeader{}, fmt.Errorf("moov not found")
	}

	moovBodyStart := moov.offset + moov.headerSize
	moovBodySize := moov.size - moov.headerSize

	// Path 1: moov > udta > meta > ilst
	if udta, ok, _ := findAtomInRange(f, moovBodyStart, moovBodySize, "udta", fileSize); ok {
		udtaBodyStart := udta.offset + udta.headerSize
		udtaBodySize := udta.size - udta.headerSize
		if meta, ok2, _ := findAtomInRange(f, udtaBodyStart, udtaBodySize, "meta", fileSize); ok2 {
			metaBodyStart := meta.offset + meta.headerSize + 4
			metaBodySize := meta.size - meta.headerSize - 4
			if ilst, ok3, _ := findAtomInRange(f, metaBodyStart, metaBodySize, "ilst", fileSize); ok3 {
				return ilst, nil
			}
		}
	}

	// Path 2: moov > meta > ilst (no udta wrapper)
	if meta, ok, _ := findAtomInRange(f, moovBodyStart, moovBodySize, "meta", fileSize); ok {
		metaBodyStart := meta.offset + meta.headerSize + 4
		metaBodySize := meta.size - meta.headerSize - 4
		if ilst, ok2, _ := findAtomInRange(f, metaBodyStart, metaBodySize, "ilst", fileSize); ok2 {
			return ilst, nil
		}
	}

	return atomHeader{}, fmt.Errorf("ilst not found (tried moov>udta>meta>ilst and moov>meta>ilst)")
}

func readM4ADataAtomPayload(f *os.File, dataAtom atomHeader) ([]byte, error) {
	payloadStart := dataAtom.offset + dataAtom.headerSize + 8
	payloadLen := dataAtom.size - dataAtom.headerSize - 8
	if payloadLen <= 0 {
		return nil, fmt.Errorf("empty data atom in %s", dataAtom.typ)
	}

	buf := make([]byte, payloadLen)
	if _, err := f.ReadAt(buf, payloadStart); err != nil {
		return nil, err
	}
	return buf, nil
}

func readM4ADataPayload(f *os.File, parent atomHeader, fileSize int64) ([]byte, error) {
	dataStart := parent.offset + parent.headerSize
	dataSize := parent.size - parent.headerSize

	dataAtom, found, err := findAtomInRange(f, dataStart, dataSize, "data", fileSize)
	if err != nil || !found {
		return nil, fmt.Errorf("data atom not found in %s", parent.typ)
	}
	return readM4ADataAtomPayload(f, dataAtom)
}

func readM4ATextValue(f *os.File, parent atomHeader, fileSize int64) (string, error) {
	payload, err := readM4ADataPayload(f, parent, fileSize)
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(strings.TrimRight(string(payload), "\x00")), nil
}

func readM4AIndexPair(f *os.File, parent atomHeader, fileSize int64) (int, int, error) {
	payload, err := readM4ADataPayload(f, parent, fileSize)
	if err != nil {
		return 0, 0, err
	}
	if len(payload) < 6 {
		return 0, 0, fmt.Errorf("index payload too short in %s", parent.typ)
	}
	return int(binary.BigEndian.Uint16(payload[2:4])), int(binary.BigEndian.Uint16(payload[4:6])), nil
}

func parsePositiveInt(value string) int {
	value = strings.TrimSpace(value)
	if value == "" {
		return 0
	}
	n, _ := strconv.Atoi(value)
	return n
}

func formatIndexValue(number, total int) string {
	if number <= 0 {
		return ""
	}
	if total > 0 {
		return fmt.Sprintf("%d/%d", number, total)
	}
	return strconv.Itoa(number)
}

func hasMapKey(fields map[string]string, key string) bool {
	_, ok := fields[key]
	return ok
}

func readM4AFreeformValue(f *os.File, parent atomHeader, fileSize int64) (string, string, error) {
	start := parent.offset + parent.headerSize
	end := parent.offset + parent.size

	var nameValue string
	var dataValue string
	for pos := start; pos+8 <= end; {
		header, err := readAtomHeaderAt(f, pos, fileSize)
		if err != nil {
			return "", "", err
		}
		if header.size == 0 {
			header.size = end - pos
		}
		if header.size < header.headerSize {
			return "", "", fmt.Errorf("invalid atom size for %s", header.typ)
		}

		switch header.typ {
		case "mean":
			// Domain qualifier (e.g. "com.apple.iTunes") — not needed, skip.
		case "name":
			// The "name" atom payload is: 4-byte version/flags, then raw UTF-8 text.
			// It does NOT contain a nested "data" atom, so read the payload directly.
			payloadStart := header.offset + header.headerSize + 4
			payloadLen := header.size - header.headerSize - 4
			if payloadLen > 0 {
				buf := make([]byte, payloadLen)
				if _, readErr := f.ReadAt(buf, payloadStart); readErr == nil {
					nameValue = strings.TrimSpace(strings.TrimRight(string(buf), "\x00"))
				}
			}
		case "data":
			payload, payloadErr := readM4ADataAtomPayload(f, header)
			if payloadErr == nil {
				dataValue = strings.TrimSpace(strings.TrimRight(string(payload), "\x00"))
			}
		}

		pos += header.size
	}

	if nameValue == "" || dataValue == "" {
		return "", "", fmt.Errorf("freeform M4A tag incomplete")
	}

	return nameValue, dataValue, nil
}

type m4aMetadataPath struct {
	moov atomHeader
	udta *atomHeader
	meta atomHeader
	ilst atomHeader
}

func findM4AMetadataPath(f *os.File, fileSize int64) (m4aMetadataPath, error) {
	moov, found, err := findAtomInRange(f, 0, fileSize, "moov", fileSize)
	if err != nil || !found {
		return m4aMetadataPath{}, fmt.Errorf("moov not found")
	}

	moovBodyStart := moov.offset + moov.headerSize
	moovBodySize := moov.size - moov.headerSize

	if udta, ok, _ := findAtomInRange(f, moovBodyStart, moovBodySize, "udta", fileSize); ok {
		udtaBodyStart := udta.offset + udta.headerSize
		udtaBodySize := udta.size - udta.headerSize
		if meta, ok2, _ := findAtomInRange(f, udtaBodyStart, udtaBodySize, "meta", fileSize); ok2 {
			metaBodyStart := meta.offset + meta.headerSize + 4
			metaBodySize := meta.size - meta.headerSize - 4
			if ilst, ok3, _ := findAtomInRange(f, metaBodyStart, metaBodySize, "ilst", fileSize); ok3 {
				udtaCopy := udta
				return m4aMetadataPath{
					moov: moov,
					udta: &udtaCopy,
					meta: meta,
					ilst: ilst,
				}, nil
			}
		}
	}

	if meta, ok, _ := findAtomInRange(f, moovBodyStart, moovBodySize, "meta", fileSize); ok {
		metaBodyStart := meta.offset + meta.headerSize + 4
		metaBodySize := meta.size - meta.headerSize - 4
		if ilst, ok2, _ := findAtomInRange(f, metaBodyStart, metaBodySize, "ilst", fileSize); ok2 {
			return m4aMetadataPath{
				moov: moov,
				meta: meta,
				ilst: ilst,
			}, nil
		}
	}

	return m4aMetadataPath{}, fmt.Errorf("ilst not found (tried moov>udta>meta>ilst and moov>meta>ilst)")
}

func buildM4AAtom(typ string, payload []byte) []byte {
	size := int64(8 + len(payload))
	buf := make([]byte, 8+len(payload))
	binary.BigEndian.PutUint32(buf[0:4], uint32(size))
	copy(buf[4:8], []byte(typ))
	copy(buf[8:], payload)
	return buf
}

func buildM4AFreeformAtom(name, value string) []byte {
	meanPayload := append([]byte{0, 0, 0, 0}, []byte("com.apple.iTunes")...)
	namePayload := append([]byte{0, 0, 0, 0}, []byte(name)...)
	dataPayload := make([]byte, 8+len(value))
	binary.BigEndian.PutUint32(dataPayload[0:4], 1) // UTF-8 text
	copy(dataPayload[8:], []byte(value))

	payload := append([]byte{}, buildM4AAtom("mean", meanPayload)...)
	payload = append(payload, buildM4AAtom("name", namePayload)...)
	payload = append(payload, buildM4AAtom("data", dataPayload)...)
	return buildM4AAtom("----", payload)
}

func buildITunNORMTag(trackGain, trackPeak string) string {
	gainDb, ok := parseReplayGainDb(trackGain)
	if !ok {
		return ""
	}
	peakLinear, ok := parseReplayGainPeak(trackPeak)
	if !ok {
		return ""
	}

	clamp := func(v int64) int64 {
		if v < 0 {
			return 0
		}
		if v > 65534 {
			return 65534
		}
		return v
	}

	g1 := clamp(int64(math.Round(math.Pow(10, gainDb/-10.0) * 1000.0)))
	g2 := clamp(int64(math.Round(math.Pow(10, gainDb/-10.0) * 2500.0)))
	peak := clamp(int64(math.Round(peakLinear * 32768.0)))
	values := []int64{g1, g1, g2, g2, 0, 0, peak, peak, 0, 0}
	parts := make([]string, 0, len(values))
	for _, value := range values {
		parts = append(parts, strings.ToUpper(fmt.Sprintf("%08x", value)))
	}
	return strings.Join(parts, " ")
}

func parseReplayGainDb(value string) (float64, bool) {
	match := regexp.MustCompile(`([+-]?\d+(?:\.\d+)?)`).FindStringSubmatch(strings.TrimSpace(value))
	if len(match) < 2 {
		return 0, false
	}
	parsed, err := strconv.ParseFloat(match[1], 64)
	if err != nil {
		return 0, false
	}
	return parsed, true
}

func parseReplayGainPeak(value string) (float64, bool) {
	parsed, err := strconv.ParseFloat(strings.TrimSpace(value), 64)
	if err != nil || parsed <= 0 {
		return 0, false
	}
	return parsed, true
}

func collectM4AReplayGainFields(fields map[string]string) map[string]string {
	result := map[string]string{}
	if value := strings.TrimSpace(fields["replaygain_track_gain"]); value != "" {
		result["replaygain_track_gain"] = value
	}
	if value := strings.TrimSpace(fields["replaygain_track_peak"]); value != "" {
		result["replaygain_track_peak"] = value
	}
	if value := strings.TrimSpace(fields["replaygain_album_gain"]); value != "" {
		result["replaygain_album_gain"] = value
	}
	if value := strings.TrimSpace(fields["replaygain_album_peak"]); value != "" {
		result["replaygain_album_peak"] = value
	}

	if norm := buildITunNORMTag(result["replaygain_track_gain"], result["replaygain_track_peak"]); norm != "" {
		result["iTunNORM"] = norm
	}

	return result
}

func writeAtomSize(buf []byte, header atomHeader, newSize int64) error {
	if newSize <= 0 {
		return fmt.Errorf("invalid size for %s", header.typ)
	}
	if header.headerSize == 16 {
		if int(header.offset)+16 > len(buf) {
			return io.ErrUnexpectedEOF
		}
		binary.BigEndian.PutUint32(buf[header.offset:header.offset+4], 1)
		binary.BigEndian.PutUint64(buf[header.offset+8:header.offset+16], uint64(newSize))
		return nil
	}
	if newSize > math.MaxUint32 {
		return fmt.Errorf("atom %s too large for 32-bit header", header.typ)
	}
	if int(header.offset)+8 > len(buf) {
		return io.ErrUnexpectedEOF
	}
	binary.BigEndian.PutUint32(buf[header.offset:header.offset+4], uint32(newSize))
	return nil
}

func EditM4AReplayGain(filePath string, fields map[string]string) error {
	replayGainFields := collectM4AReplayGainFields(fields)
	if len(replayGainFields) == 0 {
		return nil
	}

	f, err := os.Open(filePath)
	if err != nil {
		return err
	}
	defer f.Close()

	info, err := f.Stat()
	if err != nil {
		return err
	}

	path, err := findM4AMetadataPath(f, info.Size())
	if err != nil {
		return err
	}

	data, err := os.ReadFile(filePath)
	if err != nil {
		return err
	}

	bodyStart := path.ilst.offset + path.ilst.headerSize
	bodyEnd := path.ilst.offset + path.ilst.size
	newBody := make([]byte, 0, int(path.ilst.size))
	targets := map[string]struct{}{
		"REPLAYGAIN_TRACK_GAIN": {},
		"REPLAYGAIN_TRACK_PEAK": {},
		"REPLAYGAIN_ALBUM_GAIN": {},
		"REPLAYGAIN_ALBUM_PEAK": {},
		"ITUNNORM":              {},
	}

	for pos := bodyStart; pos+8 <= bodyEnd; {
		header, readErr := readAtomHeaderAt(f, pos, info.Size())
		if readErr != nil {
			return readErr
		}
		if header.size == 0 {
			header.size = bodyEnd - pos
		}
		if header.size < header.headerSize {
			return fmt.Errorf("invalid atom size for %s", header.typ)
		}

		keep := true
		if header.typ == "----" {
			name, _, freeformErr := readM4AFreeformValue(f, header, info.Size())
			if freeformErr == nil {
				if _, ok := targets[strings.ToUpper(strings.TrimSpace(name))]; ok {
					keep = false
				}
			}
		}
		if keep {
			newBody = append(newBody, data[pos:pos+header.size]...)
		}

		pos += header.size
	}

	order := []string{
		"replaygain_track_gain",
		"replaygain_track_peak",
		"replaygain_album_gain",
		"replaygain_album_peak",
		"iTunNORM",
	}
	for _, key := range order {
		value := strings.TrimSpace(replayGainFields[key])
		if value == "" {
			continue
		}
		name := key
		if key != "iTunNORM" {
			name = strings.ToLower(key)
		}
		newBody = append(newBody, buildM4AFreeformAtom(name, value)...)
	}

	newIlst := buildM4AAtom("ilst", newBody)
	updated := append([]byte{}, data[:path.ilst.offset]...)
	updated = append(updated, newIlst...)
	updated = append(updated, data[path.ilst.offset+path.ilst.size:]...)

	delta := int64(len(newIlst)) - path.ilst.size
	if err := writeAtomSize(updated, path.ilst, path.ilst.size+delta); err != nil {
		return err
	}
	if err := writeAtomSize(updated, path.meta, path.meta.size+delta); err != nil {
		return err
	}
	if path.udta != nil {
		if err := writeAtomSize(updated, *path.udta, path.udta.size+delta); err != nil {
			return err
		}
	}
	if err := writeAtomSize(updated, path.moov, path.moov.size+delta); err != nil {
		return err
	}

	return os.WriteFile(filePath, updated, 0o644)
}

func extractLyricsFromSidecarLRC(filePath string) (string, error) {
	ext := filepath.Ext(filePath)
	base := strings.TrimSuffix(filePath, ext)
	if strings.TrimSpace(base) == "" {
		return "", fmt.Errorf("no lyrics found in file")
	}

	lrcPath := base + ".lrc"
	data, err := os.ReadFile(lrcPath)
	if err != nil {
		return "", fmt.Errorf("no lyrics found in file")
	}

	lyrics := strings.TrimSpace(string(data))
	if lyrics == "" {
		return "", fmt.Errorf("no lyrics found in file")
	}
	return lyrics, nil
}

func extractLyricsFromFlac(filePath string) (string, error) {
	f, err := flac.ParseFile(filePath)
	if err != nil {
		return "", fmt.Errorf("failed to parse FLAC file: %w", err)
	}

	for _, meta := range f.Meta {
		if meta.Type != flac.VorbisComment {
			continue
		}

		cmt, err := flacvorbis.ParseFromMetaDataBlock(*meta)
		if err != nil {
			continue
		}

		lyrics, err := cmt.Get("LYRICS")
		if err == nil && len(lyrics) > 0 && strings.TrimSpace(lyrics[0]) != "" {
			return lyrics[0], nil
		}

		lyrics, err = cmt.Get("UNSYNCEDLYRICS")
		if err == nil && len(lyrics) > 0 && strings.TrimSpace(lyrics[0]) != "" {
			return lyrics[0], nil
		}
	}

	return "", fmt.Errorf("no lyrics found in file")
}

func looksLikeEmbeddedLyrics(value string) bool {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return false
	}

	lower := strings.ToLower(trimmed)
	if strings.Contains(lower, "[ar:") || strings.Contains(lower, "[ti:") {
		return true
	}

	if strings.Contains(trimmed, "\n") && strings.Contains(trimmed, "[") && strings.Contains(trimmed, "]") {
		return true
	}

	return false
}

type AudioQuality struct {
	BitDepth     int    `json:"bit_depth"`
	SampleRate   int    `json:"sample_rate"`
	TotalSamples int64  `json:"total_samples"`
	Duration     int    `json:"duration"`
	Bitrate      int    `json:"bitrate,omitempty"` // kbps, estimated for compressed MP4-family streams
	Codec        string `json:"codec,omitempty"`
}

func GetAudioQuality(filePath string) (AudioQuality, error) {
	file, err := os.Open(filePath)
	if err != nil {
		return AudioQuality{}, fmt.Errorf("failed to open file: %w", err)
	}
	defer file.Close()

	marker := make([]byte, 4)
	if _, err := file.Read(marker); err != nil {
		return AudioQuality{}, fmt.Errorf("failed to read marker: %w", err)
	}

	if string(marker) == "fLaC" {
		header := make([]byte, 4)
		if _, err := file.Read(header); err != nil {
			return AudioQuality{}, fmt.Errorf("failed to read header: %w", err)
		}

		blockType := header[0] & 0x7F
		if blockType != 0 {
			return AudioQuality{}, fmt.Errorf("first block is not STREAMINFO")
		}

		streamInfo := make([]byte, 34)
		if _, err := file.Read(streamInfo); err != nil {
			return AudioQuality{}, fmt.Errorf("failed to read STREAMINFO: %w", err)
		}

		sampleRate := (int(streamInfo[10]) << 12) | (int(streamInfo[11]) << 4) | (int(streamInfo[12]) >> 4)

		bitsPerSample := ((int(streamInfo[12]) & 0x01) << 4) | (int(streamInfo[13]) >> 4) + 1

		totalSamples := int64(streamInfo[13]&0x0F)<<32 |
			int64(streamInfo[14])<<24 |
			int64(streamInfo[15])<<16 |
			int64(streamInfo[16])<<8 |
			int64(streamInfo[17])

		duration := 0
		if sampleRate > 0 && totalSamples > 0 {
			duration = int(totalSamples / int64(sampleRate))
		}

		return AudioQuality{
			BitDepth:     bitsPerSample,
			SampleRate:   sampleRate,
			TotalSamples: totalSamples,
			Duration:     duration,
			Codec:        "flac",
		}, nil
	}

	file.Seek(0, 0)
	header8 := make([]byte, 8)
	if _, err := file.Read(header8); err != nil {
		return AudioQuality{}, fmt.Errorf("failed to read header: %w", err)
	}

	if string(header8[4:8]) == "ftyp" {
		file.Close()
		return GetM4AQuality(filePath)
	}

	return AudioQuality{}, fmt.Errorf("unsupported file format (not FLAC or M4A)")
}

func GetM4AQuality(filePath string) (AudioQuality, error) {
	f, err := os.Open(filePath)
	if err != nil {
		return AudioQuality{}, fmt.Errorf("failed to open M4A file: %w", err)
	}
	defer f.Close()

	info, err := f.Stat()
	if err != nil {
		return AudioQuality{}, fmt.Errorf("failed to stat M4A file: %w", err)
	}
	fileSize := info.Size()

	moovHeader, moovFound, err := findAtomInRange(f, 0, fileSize, "moov", fileSize)
	if err != nil {
		return AudioQuality{}, fmt.Errorf("failed to find moov atom: %w", err)
	}
	if !moovFound {
		return AudioQuality{}, fmt.Errorf("moov atom not found")
	}

	moovStart := moovHeader.offset
	moovEnd := moovHeader.offset + moovHeader.size
	duration := readM4ADurationSeconds(f, moovHeader, fileSize)

	sampleOffset, atomType, err := findAudioSampleEntry(f, moovStart, moovEnd, fileSize)
	if err != nil {
		return AudioQuality{}, err
	}

	buf := make([]byte, 32)
	if _, err := f.ReadAt(buf, sampleOffset); err != nil {
		return AudioQuality{}, fmt.Errorf("failed to read audio sample entry: %w", err)
	}

	// AudioSampleEntry layout from the box type field:
	//   [0:4]   type ("mp4a"/"alac")
	//   [4:10]  SampleEntry.reserved
	//   [10:12] data_reference_index
	//   [12:20] reserved[8]
	//   [20:22] channelcount
	//   [22:24] samplesize (bit depth)
	//   [24:26] pre_defined
	//   [26:28] reserved
	//   [28:32] samplerate (16.16 fixed-point)
	sampleRate := int(buf[28])<<8 | int(buf[29])
	bitDepth := 0
	codec := normalizeM4AAudioCodec(atomType)

	if atomType == "alac" {
		bitDepth = int(buf[22])<<8 | int(buf[23])
		if alacBitDepth, alacSampleRate, ok := readALACSpecificConfig(f, sampleOffset, fileSize); ok {
			if alacBitDepth > 0 {
				bitDepth = alacBitDepth
			}
			if alacSampleRate > 0 {
				sampleRate = alacSampleRate
			}
		}
	} else if atomType == "fLaC" {
		bitDepth = int(buf[22])<<8 | int(buf[23])
		if flacBitDepth, flacSampleRate, flacTotalSamples, ok := readMP4FLACSpecificConfig(f, sampleOffset, fileSize); ok {
			if flacBitDepth > 0 {
				bitDepth = flacBitDepth
			}
			if flacSampleRate > 0 {
				sampleRate = flacSampleRate
			}
			if flacTotalSamples > 0 && sampleRate > 0 && duration <= 0 {
				duration = int(flacTotalSamples / int64(sampleRate))
			}
		}
	}

	bitrate := estimateAudioBitrateKbps(fileSize, duration)
	if bitrate > 0 && bitrate < 16 {
		bitrate = 0
	}
	return AudioQuality{
		BitDepth:   bitDepth,
		SampleRate: sampleRate,
		Duration:   duration,
		Bitrate:    bitrate,
		Codec:      codec,
	}, nil
}

func normalizeM4AAudioCodec(atomType string) string {
	switch atomType {
	case "mp4a":
		return "aac"
	case "alac":
		return "alac"
	case "fLaC":
		return "flac"
	case "ec-3":
		return "eac3"
	case "ac-3":
		return "ac3"
	case "ac-4":
		return "ac4"
	default:
		return strings.TrimSpace(atomType)
	}
}

func estimateAudioBitrateKbps(fileSize int64, durationSeconds int) int {
	if fileSize <= 0 || durationSeconds <= 0 {
		return 0
	}
	return int(math.Round(float64(fileSize*8) / float64(durationSeconds) / 1000.0))
}

func readM4ADurationSeconds(f *os.File, moovHeader atomHeader, fileSize int64) int {
	childStart := moovHeader.offset + moovHeader.headerSize
	childSize := moovHeader.size - moovHeader.headerSize
	mvhdHeader, found, err := findAtomInRange(f, childStart, childSize, "mvhd", fileSize)
	if err == nil && found {
		if duration := readMP4DurationAtomSeconds(f, mvhdHeader, fileSize); duration > 0 {
			return duration
		}
	}

	return readM4ATrackDurationSeconds(f, moovHeader, fileSize)
}

func readMP4DurationAtomSeconds(f *os.File, header atomHeader, fileSize int64) int {
	payloadOffset := header.offset + header.headerSize
	versionBuf := make([]byte, 1)
	if _, err := f.ReadAt(versionBuf, payloadOffset); err != nil {
		return 0
	}

	if versionBuf[0] == 1 {
		buf := make([]byte, 32)
		if _, err := f.ReadAt(buf, payloadOffset); err != nil {
			return 0
		}
		timescale := binary.BigEndian.Uint32(buf[20:24])
		duration := binary.BigEndian.Uint64(buf[24:32])
		if timescale == 0 || duration == 0 {
			return 0
		}
		return int(math.Round(float64(duration) / float64(timescale)))
	}

	buf := make([]byte, 20)
	if _, err := f.ReadAt(buf, payloadOffset); err != nil {
		return 0
	}
	timescale := binary.BigEndian.Uint32(buf[12:16])
	duration := binary.BigEndian.Uint32(buf[16:20])
	if timescale == 0 || duration == 0 {
		return 0
	}
	return int(math.Round(float64(duration) / float64(timescale)))
}

func readM4ATrackDurationSeconds(f *os.File, moovHeader atomHeader, fileSize int64) int {
	childStart := moovHeader.offset + moovHeader.headerSize
	childSize := moovHeader.size - moovHeader.headerSize
	bestDuration := 0
	_ = walkMP4AtomsInRange(f, childStart, childSize, fileSize, func(header atomHeader) bool {
		if header.typ == "mdhd" {
			if duration := readMP4DurationAtomSeconds(f, header, fileSize); duration > bestDuration {
				bestDuration = duration
			}
			return false
		}
		return header.typ == "trak" || header.typ == "mdia"
	})
	return bestDuration
}

func walkMP4AtomsInRange(f *os.File, start, size, fileSize int64, visit func(atomHeader) bool) error {
	if size <= 0 {
		return nil
	}

	end := start + size
	for pos := start; pos+8 <= end; {
		header, err := readAtomHeaderAt(f, pos, fileSize)
		if err != nil {
			return err
		}
		atomSize := header.size
		if atomSize == 0 {
			atomSize = end - pos
		}
		if atomSize < header.headerSize {
			return fmt.Errorf("invalid atom size for %s", header.typ)
		}
		header.size = atomSize
		if visit(header) {
			childStart := header.offset + header.headerSize
			childSize := header.size - header.headerSize
			if err := walkMP4AtomsInRange(f, childStart, childSize, fileSize, visit); err != nil {
				return err
			}
		}
		pos += atomSize
	}
	return nil
}

func readALACSpecificConfig(f *os.File, sampleOffset, fileSize int64) (int, int, bool) {
	if sampleOffset < 4 {
		return 0, 0, false
	}

	sampleEntryHeader, err := readAtomHeaderAt(f, sampleOffset-4, fileSize)
	if err != nil {
		return 0, 0, false
	}

	childStart := sampleOffset + 32
	childEnd := sampleEntryHeader.offset + sampleEntryHeader.size
	if childStart >= childEnd {
		return 0, 0, false
	}

	configHeader, found, err := findAtomInRange(f, childStart, childEnd-childStart, "alac", fileSize)
	if err != nil || !found {
		return 0, 0, false
	}

	payloadSize := configHeader.size - configHeader.headerSize
	if payloadSize <= 0 {
		return 0, 0, false
	}

	payload := make([]byte, payloadSize)
	if _, err := f.ReadAt(payload, configHeader.offset+configHeader.headerSize); err != nil {
		return 0, 0, false
	}

	return parseALACSpecificConfig(payload)
}

func readMP4FLACSpecificConfig(f *os.File, sampleOffset, fileSize int64) (int, int, int64, bool) {
	if sampleOffset < 4 {
		return 0, 0, 0, false
	}

	sampleEntryHeader, err := readAtomHeaderAt(f, sampleOffset-4, fileSize)
	if err != nil {
		return 0, 0, 0, false
	}

	childStart := sampleOffset + 32
	childEnd := sampleEntryHeader.offset + sampleEntryHeader.size
	if childStart >= childEnd {
		return 0, 0, 0, false
	}

	configHeader, found, err := findAtomInRange(f, childStart, childEnd-childStart, "dfLa", fileSize)
	if err != nil || !found {
		return 0, 0, 0, false
	}

	payloadSize := configHeader.size - configHeader.headerSize
	if payloadSize <= 0 {
		return 0, 0, 0, false
	}

	payload := make([]byte, payloadSize)
	if _, err := f.ReadAt(payload, configHeader.offset+configHeader.headerSize); err != nil {
		return 0, 0, 0, false
	}

	return parseMP4FLACSpecificConfig(payload)
}

func parseMP4FLACSpecificConfig(payload []byte) (int, int, int64, bool) {
	if len(payload) >= 4 && string(payload[:4]) == "fLaC" {
		payload = payload[4:]
	} else if len(payload) >= 4 {
		// FLACSpecificBox starts with a full-box version/flags field.
		payload = payload[4:]
	}

	for len(payload) >= 4 {
		blockType := payload[0] & 0x7F
		blockLen := int(payload[1])<<16 | int(payload[2])<<8 | int(payload[3])
		if blockLen < 0 || len(payload) < 4+blockLen {
			return 0, 0, 0, false
		}
		block := payload[4 : 4+blockLen]
		if blockType == 0 && len(block) >= 34 {
			bitDepth, sampleRate, totalSamples := parseFLACStreamInfoQuality(block[:34])
			return bitDepth, sampleRate, totalSamples, bitDepth > 0 || sampleRate > 0
		}
		payload = payload[4+blockLen:]
	}

	return 0, 0, 0, false
}

func parseFLACStreamInfoQuality(streamInfo []byte) (int, int, int64) {
	if len(streamInfo) < 18 {
		return 0, 0, 0
	}
	sampleRate := (int(streamInfo[10]) << 12) | (int(streamInfo[11]) << 4) | (int(streamInfo[12]) >> 4)
	bitsPerSample := (((int(streamInfo[12]) & 0x01) << 4) | (int(streamInfo[13]) >> 4)) + 1
	totalSamples := int64(streamInfo[13]&0x0F)<<32 |
		int64(streamInfo[14])<<24 |
		int64(streamInfo[15])<<16 |
		int64(streamInfo[16])<<8 |
		int64(streamInfo[17])
	return bitsPerSample, sampleRate, totalSamples
}

func parseALACSpecificConfig(payload []byte) (int, int, bool) {
	if len(payload) < 24 {
		return 0, 0, false
	}

	bitDepth := int(payload[5])
	sampleRate := int(binary.BigEndian.Uint32(payload[20:24]))
	if bitDepth > 0 && sampleRate > 0 {
		return bitDepth, sampleRate, true
	}

	// Some encoders prepend 4 bytes before the ALACSpecificConfig payload.
	if len(payload) >= 28 {
		bitDepth = int(payload[9])
		sampleRate = int(binary.BigEndian.Uint32(payload[24:28]))
		if bitDepth > 0 && sampleRate > 0 {
			return bitDepth, sampleRate, true
		}
	}

	return 0, 0, false
}

type atomHeader struct {
	offset     int64
	size       int64
	headerSize int64
	typ        string
}

func readAtomHeaderAt(f *os.File, offset, fileSize int64) (atomHeader, error) {
	if offset+8 > fileSize {
		return atomHeader{}, io.ErrUnexpectedEOF
	}

	headerBuf := make([]byte, 8)
	if _, err := f.ReadAt(headerBuf, offset); err != nil {
		return atomHeader{}, err
	}

	size32 := binary.BigEndian.Uint32(headerBuf[0:4])
	typ := string(headerBuf[4:8])

	if size32 == 1 {
		if offset+16 > fileSize {
			return atomHeader{}, io.ErrUnexpectedEOF
		}
		extBuf := make([]byte, 8)
		if _, err := f.ReadAt(extBuf, offset+8); err != nil {
			return atomHeader{}, err
		}
		size64 := binary.BigEndian.Uint64(extBuf)
		return atomHeader{offset: offset, size: int64(size64), headerSize: 16, typ: typ}, nil
	}

	return atomHeader{offset: offset, size: int64(size32), headerSize: 8, typ: typ}, nil
}

func findAtomInRange(f *os.File, start, size int64, target string, fileSize int64) (atomHeader, bool, error) {
	if size <= 0 {
		return atomHeader{}, false, nil
	}

	end := start + size
	pos := start

	for pos+8 <= end {
		header, err := readAtomHeaderAt(f, pos, fileSize)
		if err != nil {
			return atomHeader{}, false, err
		}

		atomSize := header.size
		if atomSize == 0 {
			atomSize = end - pos
		}

		if atomSize < header.headerSize {
			return atomHeader{}, false, fmt.Errorf("invalid atom size for %s", header.typ)
		}

		header.size = atomSize
		if header.typ == target {
			return header, true, nil
		}

		pos += atomSize
	}

	return atomHeader{}, false, nil
}

func findAudioSampleEntry(f *os.File, start, end, fileSize int64) (int64, string, error) {
	const chunkSize = 64 * 1024
	patterns := [][]byte{
		[]byte("mp4a"),
		[]byte("alac"),
		[]byte("fLaC"),
		[]byte("ec-3"),
		[]byte("ac-3"),
		[]byte("ac-4"),
	}

	var tail []byte
	readPos := start

	for readPos < end {
		toRead := end - readPos
		if toRead > chunkSize {
			toRead = chunkSize
		}

		buf := make([]byte, toRead)
		n, err := f.ReadAt(buf, readPos)
		if err != nil && err != io.EOF {
			return 0, "", fmt.Errorf("failed to read M4A atom data: %w", err)
		}
		if n == 0 {
			break
		}

		data := append(tail, buf[:n]...)
		bestIdx := -1
		bestType := ""
		for _, pattern := range patterns {
			idx := bytes.Index(data, pattern)
			if idx >= 0 && (bestIdx < 0 || idx < bestIdx) {
				bestIdx = idx
				bestType = string(pattern)
			}
		}

		if bestIdx >= 0 {
			absolute := readPos - int64(len(tail)) + int64(bestIdx)
			if absolute+32 > fileSize {
				return 0, "", fmt.Errorf("audio info not found in M4A file")
			}
			return absolute, bestType, nil
		}

		if len(data) >= 3 {
			tail = append([]byte{}, data[len(data)-3:]...)
		} else {
			tail = append([]byte{}, data...)
		}

		readPos += int64(n)
	}

	return 0, "", fmt.Errorf("audio info not found in M4A file")
}
