package music

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

func ParseCreditsOverlay(path string) (CreditsOverlay, error) {
	entries, err := ParseCreditsMarkdown(path)
	if err != nil {
		return CreditsOverlay{}, err
	}
	overlay := CreditsOverlay{
		ByOriginal: make(map[string]CreditsEntry),
		ByOutput:   make(map[string]CreditsEntry),
		Entries:    entries,
	}
	for _, entry := range entries {
		if entry.OriginalFile != "" {
			key := strings.ToLower(strings.TrimSpace(entry.OriginalFile))
			if _, exists := overlay.ByOriginal[key]; exists {
				return CreditsOverlay{}, fmt.Errorf("duplicate manual credits row for original file %s", entry.OriginalFile)
			}
			overlay.ByOriginal[key] = entry
		}
		if entry.OutputFile != "" {
			key := strings.ToLower(strings.TrimSpace(entry.OutputFile))
			if _, exists := overlay.ByOutput[key]; exists {
				return CreditsOverlay{}, fmt.Errorf("duplicate manual credits row for output file %s", entry.OutputFile)
			}
			overlay.ByOutput[key] = entry
		}
	}
	return overlay, nil
}

func (o CreditsOverlay) Match(originalFile, outputFile string) *CreditsEntry {
	if entry, ok := o.ByOutput[strings.ToLower(strings.TrimSpace(outputFile))]; ok {
		entryCopy := entry
		return &entryCopy
	}
	if entry, ok := o.ByOriginal[strings.ToLower(strings.TrimSpace(originalFile))]; ok {
		entryCopy := entry
		return &entryCopy
	}
	return nil
}

func ValidateOverlayEntries(dir string, overlay CreditsOverlay, generated []CreditsEntry) error {
	if len(overlay.Entries) == 0 {
		return nil
	}
	validOriginal := make(map[string]struct{}, len(generated))
	validOutput := make(map[string]struct{}, len(generated))
	for _, entry := range generated {
		validOriginal[strings.ToLower(entry.OriginalFile)] = struct{}{}
		validOutput[strings.ToLower(entry.OutputFile)] = struct{}{}
	}
	for _, entry := range overlay.Entries {
		if entry.OriginalFile != "" {
			if _, ok := validOriginal[strings.ToLower(entry.OriginalFile)]; !ok {
				return fmt.Errorf("manual credits entry in %s references unknown original file %s", dir, entry.OriginalFile)
			}
		}
		if entry.OutputFile != "" {
			if _, ok := validOutput[strings.ToLower(entry.OutputFile)]; !ok {
				return fmt.Errorf("manual credits entry in %s references unknown output file %s", dir, entry.OutputFile)
			}
		}
	}
	return nil
}

func ParseCreditsMarkdown(path string) ([]CreditsEntry, error) {
	raw, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, err
	}
	lines := strings.Split(strings.ReplaceAll(string(raw), "\r\n", "\n"), "\n")
	header := []string(nil)
	entries := make([]CreditsEntry, 0)
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if !strings.HasPrefix(line, "|") {
			continue
		}
		cells := ParseMarkdownTableRow(line)
		if len(cells) == 0 {
			continue
		}
		if header == nil {
			header = make([]string, len(cells))
			for i, cell := range cells {
				header[i] = NormalizeCreditsHeader(cell)
			}
			continue
		}
		if IsMarkdownSeparatorRow(cells) {
			continue
		}
		entry := CreditsEntry{}
		for i, key := range header {
			if i >= len(cells) {
				continue
			}
			value := CleanCreditsCell(cells[i])
			switch key {
			case "artist":
				entry.Artist = value
			case "title":
				entry.Title = value
			case "output_file":
				entry.OutputFile = value
			case "original_file":
				entry.OriginalFile = value
			case "album":
				entry.Album = value
			case "date":
				entry.Date = value
			case "rights":
				entry.Rights = value
			case "notes":
				entry.Notes = value
			}
		}
		if entry.Artist == "" && entry.Title == "" && entry.OutputFile == "" && entry.OriginalFile == "" {
			continue
		}
		entries = append(entries, entry)
	}
	return entries, nil
}

func ParseMarkdownTableRow(line string) []string {
	trimmed := strings.TrimSpace(line)
	trimmed = strings.TrimPrefix(trimmed, "|")
	trimmed = strings.TrimSuffix(trimmed, "|")
	parts := strings.Split(trimmed, "|")
	cells := make([]string, 0, len(parts))
	for _, part := range parts {
		cells = append(cells, strings.TrimSpace(strings.ReplaceAll(part, `\|`, "|")))
	}
	return cells
}

func IsMarkdownSeparatorRow(cells []string) bool {
	if len(cells) == 0 {
		return false
	}
	for _, cell := range cells {
		cell = strings.TrimSpace(cell)
		cell = strings.ReplaceAll(cell, "-", "")
		cell = strings.ReplaceAll(cell, ":", "")
		if cell != "" {
			return false
		}
	}
	return true
}

func NormalizeCreditsHeader(value string) string {
	switch strings.ToLower(CleanCreditsCell(value)) {
	case "artist":
		return "artist"
	case "title":
		return "title"
	case "output file":
		return "output_file"
	case "original file":
		return "original_file"
	case "album":
		return "album"
	case "date":
		return "date"
	case "license / copyright":
		return "rights"
	case "notes":
		return "notes"
	default:
		return ""
	}
}

func CleanCreditsCell(value string) string {
	value = strings.TrimSpace(value)
	value = strings.Trim(value, "`")
	value = strings.ReplaceAll(value, "<br>", "\n")
	return strings.TrimSpace(value)
}

func RenderCreditsMarkdown(entries []CreditsEntry) string {
	var builder strings.Builder
	builder.WriteString(CreditsHeader)
	for _, entry := range entries {
		builder.WriteString("| ")
		builder.WriteString(EscapeMarkdownCell(entry.Artist))
		builder.WriteString(" | ")
		builder.WriteString(EscapeMarkdownCell(entry.Title))
		builder.WriteString(" | ")
		builder.WriteString(EscapeMarkdownCell(WrapCodeCell(entry.OutputFile)))
		builder.WriteString(" | ")
		builder.WriteString(EscapeMarkdownCell(WrapCodeCell(entry.OriginalFile)))
		builder.WriteString(" | ")
		builder.WriteString(EscapeMarkdownCell(entry.Album))
		builder.WriteString(" | ")
		builder.WriteString(EscapeMarkdownCell(entry.Date))
		builder.WriteString(" | ")
		builder.WriteString(EscapeMarkdownCell(entry.Rights))
		builder.WriteString(" | ")
		builder.WriteString(EscapeMarkdownCell(entry.Notes))
		builder.WriteString(" |\n")
	}
	return builder.String()
}

func WrapCodeCell(value string) string {
	if strings.TrimSpace(value) == "" {
		return ""
	}
	return "`" + value + "`"
}

func EscapeMarkdownCell(value string) string {
	value = strings.ReplaceAll(value, `\`, `\\`)
	value = strings.ReplaceAll(value, "|", `\|`)
	value = strings.ReplaceAll(value, "\n", "<br>")
	return value
}

func ApplyCreditsOverlay(entry *CreditsEntry, overlay *CreditsEntry) {
	if overlay == nil {
		return
	}
	entry.Artist = FirstNonEmpty(overlay.Artist, entry.Artist)
	entry.Title = FirstNonEmpty(overlay.Title, entry.Title)
	entry.Album = FirstNonEmpty(overlay.Album, entry.Album)
	entry.Date = FirstNonEmpty(overlay.Date, entry.Date)
	entry.Rights = FirstNonEmpty(overlay.Rights, entry.Rights)
	entry.Notes = FirstNonEmpty(overlay.Notes, entry.Notes)
}

func SyncGeneratedCreditsArtifacts(root string, desiredFiles map[string][]byte) (int, error) {
	changedFiles := 0
	existingFiles := make(map[string]struct{})
	if err := filepath.WalkDir(root, func(path string, d os.DirEntry, walkErr error) error {
		if walkErr != nil {
			if os.IsNotExist(walkErr) {
				return nil
			}
			return walkErr
		}
		if d.IsDir() {
			return nil
		}
		existingFiles[path] = struct{}{}
		return nil
	}); err != nil {
		return 0, fmt.Errorf("scan credits dir: %w", err)
	}

	for path, content := range desiredFiles {
		current, err := os.ReadFile(path)
		if err != nil && !os.IsNotExist(err) {
			return 0, fmt.Errorf("read credits artifact %s: %w", path, err)
		}
		if err == nil && string(current) == string(content) {
			delete(existingFiles, path)
			continue
		}
		if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
			return 0, fmt.Errorf("create generated credits dir: %w", err)
		}
		if err := os.WriteFile(path, content, 0o644); err != nil {
			return 0, fmt.Errorf("write credits artifact %s: %w", path, err)
		}
		changedFiles++
		delete(existingFiles, path)
	}

	for path := range existingFiles {
		if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
			return 0, fmt.Errorf("remove stale credits artifact %s: %w", path, err)
		}
		changedFiles++
	}
	return changedFiles, nil
}
