package backend

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"

	id3v2 "github.com/bogem/id3v2/v2"
	"github.com/go-flac/flacvorbis"
	"github.com/go-flac/go-flac"
	"github.com/wailsapp/wails/v2/pkg/runtime"
)

type EmbeddedLyrics struct {
	Path   string `json:"path"`
	Name   string `json:"name"`
	Lyrics string `json:"lyrics"`
	Source string `json:"source"`
	Synced bool   `json:"synced"`
	Error  string `json:"error,omitempty"`
}

var lrcTimestampRe = regexp.MustCompile(`\[\d{1,2}:\d{2}(?:\.\d{1,3})?\]`)

func isSyncedLyrics(lyrics string) bool {
	return lrcTimestampRe.MatchString(lyrics)
}

func ReadEmbeddedLyrics(filePath string) (*EmbeddedLyrics, error) {
	if !fileExists(filePath) {
		return nil, fmt.Errorf("file does not exist")
	}

	result := &EmbeddedLyrics{
		Path: filePath,
		Name: filepath.Base(filePath),
	}

	ext := strings.ToLower(filepath.Ext(filePath))

	var lyrics string
	var err error

	switch ext {
	case ".lrc", ".txt":
		var content []byte
		content, err = os.ReadFile(filePath)
		if err == nil {
			lyrics = string(content)
			result.Source = "lrc"
		}
	case ".flac":
		lyrics, err = readFlacLyrics(filePath)
		result.Source = "embedded"
	case ".mp3":
		lyrics, err = readMp3Lyrics(filePath)
		result.Source = "embedded"
	case ".m4a", ".aac", ".opus", ".ogg":
		lyrics, err = readLyricsWithFFprobe(filePath)
		result.Source = "embedded"
	default:
		return nil, fmt.Errorf("unsupported file format: %s", ext)
	}

	if err != nil {
		result.Error = err.Error()
		return result, nil
	}

	lyrics = strings.TrimSpace(lyrics)
	if lyrics == "" {
		result.Error = "No lyrics found in this file"
		return result, nil
	}

	result.Lyrics = lyrics
	result.Synced = isSyncedLyrics(lyrics)
	return result, nil
}

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

	for _, block := range f.Meta {
		if block.Type != flac.VorbisComment {
			continue
		}
		cmt, err := flacvorbis.ParseFromMetaDataBlock(*block)
		if err != nil {
			continue
		}
		for _, comment := range cmt.Comments {
			parts := strings.SplitN(comment, "=", 2)
			if len(parts) != 2 {
				continue
			}
			fieldName := strings.ToUpper(parts[0])
			switch fieldName {
			case "LYRICS", "UNSYNCEDLYRICS", "SYNCEDLYRICS", "LYRICS-XXX":
				if strings.TrimSpace(parts[1]) != "" {
					return parts[1], nil
				}
			}
		}
	}

	return "", nil
}

func readMp3Lyrics(filePath string) (string, error) {
	tag, err := id3v2.Open(filePath, id3v2.Options{Parse: true})
	if err != nil {
		return "", fmt.Errorf("failed to open MP3 file: %w", err)
	}
	defer tag.Close()

	frames := tag.GetFrames(tag.CommonID("Unsynchronised lyrics/text transcription"))
	for _, frame := range frames {
		uslf, ok := frame.(id3v2.UnsynchronisedLyricsFrame)
		if !ok {
			continue
		}
		if strings.TrimSpace(uslf.Lyrics) != "" {
			return uslf.Lyrics, nil
		}
	}

	return "", nil
}

func readLyricsWithFFprobe(filePath string) (string, error) {
	ffprobePath, err := GetFFprobePath()
	if err != nil {
		return "", err
	}

	if err := ValidateExecutable(ffprobePath); err != nil {
		return "", fmt.Errorf("invalid ffprobe executable: %w", err)
	}

	cmd := exec.Command(ffprobePath,
		"-v", "quiet",
		"-print_format", "json",
		"-show_format",
		"-show_streams",
		filePath,
	)

	setHideWindow(cmd)

	output, err := cmd.Output()
	if err != nil {
		return "", err
	}

	var probe struct {
		Format struct {
			Tags map[string]string `json:"tags"`
		} `json:"format"`
		Streams []struct {
			Tags map[string]string `json:"tags"`
		} `json:"streams"`
	}

	if err := json.Unmarshal(output, &probe); err != nil {
		return "", err
	}

	collect := func(tags map[string]string) string {
		for key, value := range tags {
			lk := strings.ToLower(key)
			if lk == "lyrics" || strings.HasPrefix(lk, "lyrics-") || lk == "unsyncedlyrics" {
				if strings.TrimSpace(value) != "" {
					return value
				}
			}
		}
		return ""
	}

	if lyrics := collect(probe.Format.Tags); lyrics != "" {
		return lyrics, nil
	}
	for _, stream := range probe.Streams {
		if lyrics := collect(stream.Tags); lyrics != "" {
			return lyrics, nil
		}
	}

	return "", nil
}

type ExtractLyricsResult struct {
	Path          string `json:"path"`
	OutputPath    string `json:"output_path,omitempty"`
	Success       bool   `json:"success"`
	Error         string `json:"error,omitempty"`
	AlreadyExists bool   `json:"already_exists,omitempty"`
}

func ExtractLyricsToLRC(filePath string, overwrite bool) (*ExtractLyricsResult, error) {
	result := &ExtractLyricsResult{Path: filePath}

	embedded, err := ReadEmbeddedLyrics(filePath)
	if err != nil {
		result.Error = err.Error()
		return result, nil
	}

	if embedded.Error != "" {
		result.Error = embedded.Error
		return result, nil
	}

	if strings.TrimSpace(embedded.Lyrics) == "" {
		result.Error = "No lyrics found in this file"
		return result, nil
	}

	dir := filepath.Dir(filePath)
	base := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath))
	outputPath := filepath.Join(dir, base+".lrc")
	result.OutputPath = outputPath

	if !overwrite {
		if info, statErr := os.Stat(outputPath); statErr == nil && info.Size() > 0 {
			result.AlreadyExists = true
			result.Error = "LRC file already exists"
			return result, nil
		}
	}

	content := embedded.Lyrics
	if !strings.HasSuffix(content, "\n") {
		content += "\n"
	}

	if writeErr := os.WriteFile(outputPath, []byte(content), 0644); writeErr != nil {
		result.Error = fmt.Sprintf("failed to write LRC file: %v", writeErr)
		return result, nil
	}

	result.Success = true
	return result, nil
}

func SelectLyricsFiles(ctx context.Context) ([]string, error) {
	return runtime.OpenMultipleFilesDialog(ctx, runtime.OpenDialogOptions{
		Title: "Select Lyrics or Audio Files",
		Filters: []runtime.FileFilter{
			{
				DisplayName: "Lyrics & Audio (*.lrc, *.flac, *.mp3, *.m4a, *.opus)",
				Pattern:     "*.lrc;*.flac;*.mp3;*.m4a;*.aac;*.opus;*.ogg;*.txt",
			},
			{
				DisplayName: "LRC Files (*.lrc)",
				Pattern:     "*.lrc",
			},
			{
				DisplayName: "Audio Files (*.flac, *.mp3, *.m4a, *.opus)",
				Pattern:     "*.flac;*.mp3;*.m4a;*.aac;*.opus;*.ogg",
			},
			{
				DisplayName: "All Files (*.*)",
				Pattern:     "*.*",
			},
		},
	})
}
