package backend

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

	id3v2 "github.com/bogem/id3v2/v2"
	"github.com/go-flac/flacvorbis"
	"github.com/go-flac/go-flac"
)

type FileInfo struct {
	Name     string     `json:"name"`
	Path     string     `json:"path"`
	IsDir    bool       `json:"is_dir"`
	Size     int64      `json:"size"`
	Children []FileInfo `json:"children,omitempty"`
}

type AudioMetadata struct {
	Title       string `json:"title"`
	Artist      string `json:"artist"`
	Album       string `json:"album"`
	AlbumArtist string `json:"album_artist"`
	TrackNumber int    `json:"track_number"`
	DiscNumber  int    `json:"disc_number"`
	Year        string `json:"year"`
}

type RenamePreview struct {
	OldPath  string        `json:"old_path"`
	OldName  string        `json:"old_name"`
	NewName  string        `json:"new_name"`
	NewPath  string        `json:"new_path"`
	Error    string        `json:"error,omitempty"`
	Metadata AudioMetadata `json:"metadata"`
}

type RenameResult struct {
	OldPath string `json:"old_path"`
	NewPath string `json:"new_path"`
	Success bool   `json:"success"`
	Error   string `json:"error,omitempty"`
}

func ListDirectory(dirPath string) ([]FileInfo, error) {
	entries, err := os.ReadDir(dirPath)
	if err != nil {
		return nil, fmt.Errorf("failed to read directory: %w", err)
	}

	var result []FileInfo
	for _, entry := range entries {
		info, err := entry.Info()
		if err != nil {
			continue
		}

		fileInfo := FileInfo{
			Name:  entry.Name(),
			Path:  filepath.Join(dirPath, entry.Name()),
			IsDir: entry.IsDir(),
			Size:  info.Size(),
		}

		if entry.IsDir() {
			children, err := ListDirectory(fileInfo.Path)
			if err == nil {
				fileInfo.Children = children
			}
		}

		result = append(result, fileInfo)
	}

	return result, nil
}

func ListAudioFiles(dirPath string) ([]FileInfo, error) {
	var result []FileInfo

	err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil
		}

		if info.IsDir() {
			return nil
		}

		ext := strings.ToLower(filepath.Ext(path))
		if ext == ".flac" || ext == ".mp3" || ext == ".m4a" || ext == ".aac" {
			result = append(result, FileInfo{
				Name:  info.Name(),
				Path:  path,
				IsDir: false,
				Size:  info.Size(),
			})
		}

		return nil
	})

	if err != nil {
		return nil, fmt.Errorf("failed to walk directory: %w", err)
	}

	return result, nil
}

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

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

	switch ext {
	case ".flac":
		return readFlacMetadata(filePath)
	case ".mp3":
		return readMp3Metadata(filePath)
	case ".m4a":
		return readM4aMetadata(filePath)
	default:
		return nil, fmt.Errorf("unsupported file format: %s", ext)
	}
}

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

	metadata := &AudioMetadata{}

	for _, block := range f.Meta {
		if block.Type == flac.VorbisComment {
			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])
				value := parts[1]

				switch fieldName {
				case "TITLE":
					metadata.Title = value
				case "ARTIST":
					metadata.Artist = value
				case "ALBUM":
					metadata.Album = value
				case "ALBUMARTIST":
					metadata.AlbumArtist = value
				case "TRACKNUMBER":
					if num, err := strconv.Atoi(value); err == nil {
						metadata.TrackNumber = num
					}
				case "DISCNUMBER":
					if num, err := strconv.Atoi(value); err == nil {
						metadata.DiscNumber = num
					}
				case "DATE", "YEAR":
					metadata.Year = value
				}
			}
		}
	}

	return metadata, nil
}

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

	metadata := &AudioMetadata{
		Title:  tag.Title(),
		Artist: tag.Artist(),
		Album:  tag.Album(),
		Year:   tag.Year(),
	}

	if frames := tag.GetFrames("TPE2"); len(frames) > 0 {
		if textFrame, ok := frames[0].(id3v2.TextFrame); ok {
			metadata.AlbumArtist = textFrame.Text
		}
	}

	if frames := tag.GetFrames(tag.CommonID("Track number/Position in set")); len(frames) > 0 {
		if textFrame, ok := frames[0].(id3v2.TextFrame); ok {
			trackStr := strings.Split(textFrame.Text, "/")[0]
			if num, err := strconv.Atoi(trackStr); err == nil {
				metadata.TrackNumber = num
			}
		}
	}

	if frames := tag.GetFrames(tag.CommonID("Part of a set")); len(frames) > 0 {
		if textFrame, ok := frames[0].(id3v2.TextFrame); ok {
			discStr := strings.Split(textFrame.Text, "/")[0]
			if num, err := strconv.Atoi(discStr); err == nil {
				metadata.DiscNumber = num
			}
		}
	}

	return metadata, nil
}

func readMetadataWithFFprobe(filePath string) (*AudioMetadata, error) {
	ffprobePath, err := GetFFprobePath()
	if err != nil {
		return nil, err
	}

	if err := ValidateExecutable(ffprobePath); err != nil {
		return nil, 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 nil, err
	}

	var result 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, &result); err != nil {
		return nil, err
	}

	metadata := &AudioMetadata{}

	allTags := make(map[string]string)

	for _, stream := range result.Streams {
		for key, value := range stream.Tags {
			allTags[strings.ToLower(key)] = value
		}
	}

	for key, value := range result.Format.Tags {
		allTags[strings.ToLower(key)] = value
	}

	for key, value := range allTags {
		switch key {
		case "title":
			metadata.Title = value
		case "artist":
			metadata.Artist = value
		case "album":
			metadata.Album = value
		case "album_artist", "albumartist":
			metadata.AlbumArtist = value
		case "track":

			trackStr := strings.Split(value, "/")[0]
			if num, err := strconv.Atoi(trackStr); err == nil {
				metadata.TrackNumber = num
			}
		case "disc":
			discStr := strings.Split(value, "/")[0]
			if num, err := strconv.Atoi(discStr); err == nil {
				metadata.DiscNumber = num
			}
		case "date", "year":
			if metadata.Year == "" || len(value) > len(metadata.Year) {
				metadata.Year = value
			}
		}
	}

	return metadata, nil
}

func readM4aMetadata(filePath string) (*AudioMetadata, error) {
	metadata, err := readMetadataWithFFprobe(filePath)
	if err != nil {
		return &AudioMetadata{}, nil
	}
	return metadata, nil
}

func GenerateFilename(metadata *AudioMetadata, format string, ext string) string {
	if metadata == nil {
		return ""
	}

	result := format

	year := metadata.Year
	if len(year) >= 4 {
		year = year[:4]
	}

	result = strings.ReplaceAll(result, "{title}", sanitizeFilenameForRename(metadata.Title))
	result = strings.ReplaceAll(result, "{artist}", sanitizeFilenameForRename(metadata.Artist))
	result = strings.ReplaceAll(result, "{album}", sanitizeFilenameForRename(metadata.Album))
	result = strings.ReplaceAll(result, "{album_artist}", sanitizeFilenameForRename(metadata.AlbumArtist))
	result = strings.ReplaceAll(result, "{year}", sanitizeFilenameForRename(year))
	result = strings.ReplaceAll(result, "{date}", sanitizeFilenameForRename(metadata.Year))

	if metadata.TrackNumber > 0 {
		result = strings.ReplaceAll(result, "{track}", fmt.Sprintf("%02d", metadata.TrackNumber))
	} else {
		result = strings.ReplaceAll(result, "{track}", "")
	}

	if metadata.DiscNumber > 0 {
		result = strings.ReplaceAll(result, "{disc}", fmt.Sprintf("%d", metadata.DiscNumber))
	} else {
		result = strings.ReplaceAll(result, "{disc}", "")
	}

	result = strings.TrimSpace(result)
	result = strings.Join(strings.Fields(result), " ")

	result = strings.Trim(result, " -._")

	if result == "" {
		return ""
	}

	return result + ext
}

func sanitizeFilenameForRename(name string) string {

	invalid := []string{"<", ">", ":", "\"", "/", "\\", "|", "?", "*"}
	result := name
	for _, char := range invalid {
		result = strings.ReplaceAll(result, char, "")
	}
	return strings.TrimSpace(result)
}

func PreviewRename(files []string, format string) []RenamePreview {
	var previews []RenamePreview

	for _, filePath := range files {
		preview := RenamePreview{
			OldPath: filePath,
			OldName: filepath.Base(filePath),
		}

		metadata, err := ReadAudioMetadata(filePath)
		if err != nil {
			preview.Error = err.Error()
			previews = append(previews, preview)
			continue
		}

		preview.Metadata = *metadata

		ext := filepath.Ext(filePath)
		newName := GenerateFilename(metadata, format, ext)

		if newName == "" {
			preview.Error = "Could not generate filename (missing metadata)"
			previews = append(previews, preview)
			continue
		}

		preview.NewName = newName
		preview.NewPath = filepath.Join(filepath.Dir(filePath), newName)

		previews = append(previews, preview)
	}

	return previews
}

func GetFileSizes(files []string) map[string]int64 {
	result := make(map[string]int64)
	for _, filePath := range files {
		info, err := os.Stat(filePath)
		if err == nil {
			result[filePath] = info.Size()
		}
	}
	return result
}

func RenameFiles(files []string, format string) []RenameResult {
	var results []RenameResult

	for _, filePath := range files {
		result := RenameResult{
			OldPath: filePath,
		}

		metadata, err := ReadAudioMetadata(filePath)
		if err != nil {
			result.Error = err.Error()
			result.Success = false
			results = append(results, result)
			continue
		}

		ext := filepath.Ext(filePath)
		newName := GenerateFilename(metadata, format, ext)

		if newName == "" {
			result.Error = "Could not generate filename (missing metadata)"
			result.Success = false
			results = append(results, result)
			continue
		}

		newPath := filepath.Join(filepath.Dir(filePath), newName)
		result.NewPath = newPath

		if newPath != filePath {
			if _, err := os.Stat(newPath); err == nil {
				result.Error = "File already exists"
				result.Success = false
				results = append(results, result)
				continue
			}
		}

		if err := os.Rename(filePath, newPath); err != nil {
			result.Error = err.Error()
			result.Success = false
			results = append(results, result)
			continue
		}

		result.Success = true
		results = append(results, result)
	}

	return results
}
