package pipeline

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf"
	"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/music"
	"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
)

type preparedMusicAssets struct {
	Generated     []assetResource
	SkipSourceRel map[string]struct{}
	Artifacts     []string
	TempRoots     []string
	Summary       CreditsRefreshSummary
}

type MusicBuildOptions struct {
	DatasetIDs []string
	Write      bool
}

type musicPrepareOptions struct {
	datasetIDs []string
	write      bool
}

type musicSourceGroup struct {
	DatasetID string
	Dataset   project.EffectiveMusicDataset
	SourceDir string
	OutputDir string
	Sources   []string
}

type musicCreditGroup struct {
	CreditsRoot string
	SourceDir   string
	Entries     []music.CreditsEntry
}

func musicRootPath(p *project.Project, path string) string {
	if strings.TrimSpace(path) == "" {
		return p.Root
	}
	if filepath.IsAbs(path) {
		return path
	}
	return filepath.Join(p.Root, filepath.FromSlash(path))
}

func effectiveMusicDatasets(p *project.Project, onlyIDs []string) (map[string]project.EffectiveMusicDataset, error) {
	effective := p.EffectiveConfig()
	wanted := map[string]struct{}{}
	for _, id := range onlyIDs {
		id = strings.TrimSpace(id)
		if id != "" {
			wanted[id] = struct{}{}
		}
	}
	datasets := map[string]project.EffectiveMusicDataset{}
	for id, dataset := range effective.Music.Datasets {
		if len(wanted) > 0 {
			if _, ok := wanted[id]; !ok {
				continue
			}
			delete(wanted, id)
		}
		datasets[id] = dataset
	}
	if len(wanted) > 0 {
		unknown := make([]string, 0, len(wanted))
		for id := range wanted {
			unknown = append(unknown, id)
		}
		sort.Strings(unknown)
		return nil, fmt.Errorf("unknown music dataset(s): %s", strings.Join(unknown, ", "))
	}
	if len(datasets) == 0 && len(wanted) == 0 {
		for _, rel := range p.Inventory.AssetFiles {
			ext := strings.ToLower(filepath.Ext(rel))
			if !music.PathIsUnder("envi/music", rel) {
				continue
			}
			if _, ok := extensionSet(effective.Music.ConvertExtensions)[ext]; ok {
				datasets["legacy_envi_music"] = project.EffectiveMusicDataset{
					Source:            "envi/music",
					Output:            "envi/music",
					StageRoot:         effective.Music.StageRoot,
					CreditsRoot:       effective.Music.CreditsRoot,
					NamingScheme:      effective.Music.NamingScheme,
					OutputExtension:   effective.Music.OutputExtension,
					MaxStemLength:     effective.Music.MaxStemLength,
					PackageMode:       "hak_asset",
					ConvertExtensions: effective.Music.ConvertExtensions,
				}
				break
			}
		}
	}
	return datasets, nil
}

func extensionSet(values []string) map[string]struct{} {
	out := make(map[string]struct{}, len(values))
	for _, value := range values {
		value = strings.ToLower(strings.TrimSpace(value))
		if value != "" {
			out[value] = struct{}{}
		}
	}
	return out
}

func datasetForMusicSource(datasets map[string]project.EffectiveMusicDataset, rel, ext string) (string, project.EffectiveMusicDataset, bool) {
	bestID := ""
	var best project.EffectiveMusicDataset
	bestLen := -1
	for id, dataset := range datasets {
		if _, ok := extensionSet(dataset.ConvertExtensions)[ext]; !ok {
			continue
		}
		if !music.PathIsUnder(dataset.Source, rel) {
			continue
		}
		if len(dataset.Source) > bestLen {
			bestID = id
			best = dataset
			bestLen = len(dataset.Source)
		}
	}
	return bestID, best, bestID != ""
}

func plannedMusicAsset(outputRel, sourceRel string) (assetResource, error) {
	extension := strings.TrimPrefix(strings.ToLower(filepath.Ext(outputRel)), ".")
	resourceType, ok := erf.HAKResourceTypeForExtension(extension)
	if !ok {
		return assetResource{}, fmt.Errorf("unsupported generated music resource extension %q", filepath.Ext(outputRel))
	}
	name := strings.ToLower(strings.TrimSuffix(filepath.Base(outputRel), filepath.Ext(outputRel)))
	return assetResource{
		Rel: outputRel,
		Resource: erf.Resource{
			Name: name,
			Type: resourceType,
		},
		ContentID: "music-plan:" + filepath.ToSlash(sourceRel),
	}, nil
}

func BuildMusic(p *project.Project, opts MusicBuildOptions) (BuildResult, error) {
	prepared, err := prepareMusicAssetsWithOptions(p, musicPrepareOptions{datasetIDs: opts.DatasetIDs, write: opts.Write})
	if err != nil {
		return BuildResult{}, err
	}
	if err := cleanupPreparedMusicAssets(prepared); err != nil {
		return BuildResult{}, err
	}
	return BuildResult{
		CreditsArtifactPaths: prepared.Artifacts,
		CreditsSummary:       prepared.Summary,
		HAKAssets:            len(prepared.Generated),
	}, nil
}

func prepareMusicAssets(p *project.Project) (*preparedMusicAssets, error) {
	return prepareMusicAssetsWithOptions(p, musicPrepareOptions{write: true})
}

func prepareMusicAssetsWithOptions(p *project.Project, opts musicPrepareOptions) (*preparedMusicAssets, error) {
	usedNames := make(map[string]struct{})
	groupsByKey := make(map[string]*musicSourceGroup)
	datasets, err := effectiveMusicDatasets(p, opts.datasetIDs)
	if err != nil {
		return nil, err
	}
	for _, rel := range p.Inventory.AssetFiles {
		rel = filepath.ToSlash(rel)
		ext := strings.ToLower(filepath.Ext(rel))
		name := strings.ToLower(strings.TrimSuffix(filepath.Base(rel), filepath.Ext(rel)))
		if datasetID, dataset, ok := datasetForMusicSource(datasets, rel, ext); ok {
			sourceDir := filepath.ToSlash(filepath.Dir(rel))
			relDir, err := filepath.Rel(filepath.FromSlash(dataset.Source), filepath.FromSlash(sourceDir))
			if err != nil {
				return nil, fmt.Errorf("resolve music source directory %s: %w", sourceDir, err)
			}
			relDir = filepath.ToSlash(relDir)
			outputDir := dataset.Output
			if relDir != "." && relDir != "" {
				outputDir = filepath.ToSlash(filepath.Join(outputDir, filepath.FromSlash(relDir)))
			}
			key := datasetID + "\x00" + sourceDir
			group := groupsByKey[key]
			if group == nil {
				group = &musicSourceGroup{
					DatasetID: datasetID,
					Dataset:   dataset,
					SourceDir: sourceDir,
					OutputDir: outputDir,
				}
				groupsByKey[key] = group
			}
			group.Sources = append(group.Sources, rel)
			continue
		}
		if name != "" {
			usedNames[name] = struct{}{}
		}
	}

	result := &preparedMusicAssets{
		SkipSourceRel: make(map[string]struct{}),
	}
	if len(groupsByKey) == 0 {
		if !opts.write {
			return result, nil
		}
		writeResult, err := writeCreditsArtifacts(p, nil)
		if err != nil {
			return nil, err
		}
		result.Artifacts = writeResult.Artifacts
		result.Summary = writeResult.Summary
		return result, nil
	}

	groups := make([]*musicSourceGroup, 0, len(groupsByKey))
	for _, group := range groupsByKey {
		sort.Strings(group.Sources)
		groups = append(groups, group)
	}
	sort.Slice(groups, func(i, j int) bool {
		if groups[i].SourceDir != groups[j].SourceDir {
			return groups[i].SourceDir < groups[j].SourceDir
		}
		return groups[i].DatasetID < groups[j].DatasetID
	})

	creditGroups := make([]musicCreditGroup, 0, len(groups))
	for _, group := range groups {
		result.Summary.ScannedDirs = append(result.Summary.ScannedDirs, group.SourceDir)
		result.Summary.TrackCount += len(group.Sources)
	}
	ffmpegPath := ""
	ffprobePath := ""
	if opts.write {
		var err error
		ffmpegPath, err = music.ResolveFFmpegWithConfig(p.MusicFFmpegPath())
		if err != nil {
			return nil, err
		}
		ffprobePath, err = music.ResolveFFprobeWithConfig(p.MusicFFprobePath())
		if err != nil {
			return nil, err
		}
	}

	for _, group := range groups {
		stageRoot := musicRootPath(p, group.Dataset.StageRoot)
		result.TempRoots = append(result.TempRoots, stageRoot)
		prefix := music.SanitizePrefix(group.Dataset.Prefix)
		overlayPath := filepath.Join(p.AssetsDir(), filepath.FromSlash(group.SourceDir), p.EffectiveConfig().Music.CreditsOverlay)
		overlay, err := music.ParseCreditsOverlay(overlayPath)
		if err != nil {
			return nil, fmt.Errorf("parse credits overlay %s: %w", overlayPath, err)
		}

		entries := make([]music.CreditsEntry, 0, len(group.Sources))
		for _, rel := range group.Sources {
			base := filepath.Base(rel)
			manual := overlay.Match(base, "")
			outputFile := ""
			switch {
			case manual != nil && strings.TrimSpace(manual.OutputFile) != "":
				outputFile = strings.ToLower(strings.TrimSpace(manual.OutputFile))
				if err := music.ValidateManualOutputFileWithRules(outputFile, group.Dataset.OutputExtension, group.Dataset.MaxStemLength); err != nil {
					return nil, fmt.Errorf("manual credits output for %s: %w", rel, err)
				}
				if err := music.ReserveName(strings.TrimSuffix(outputFile, filepath.Ext(outputFile)), usedNames); err != nil {
					return nil, fmt.Errorf("manual credits output for %s: %w", rel, err)
				}
			default:
				stem, err := music.GenerateStemWithMax(base, prefix, group.Dataset.MaxStemLength, usedNames)
				if err != nil {
					return nil, fmt.Errorf("generate music name for %s: %w", rel, err)
				}
				outputFile = stem + group.Dataset.OutputExtension
			}
			outputRel := filepath.ToSlash(filepath.Join(group.OutputDir, outputFile))
			result.Summary.Mappings = append(result.Summary.Mappings, MusicFileMapping{
				Source: rel,
				Output: outputRel,
			})
			result.SkipSourceRel[rel] = struct{}{}

			if !opts.write {
				if group.Dataset.PackageMode == "hak_asset" {
					asset, err := plannedMusicAsset(outputRel, rel)
					if err != nil {
						return nil, err
					}
					result.Generated = append(result.Generated, asset)
				}
				continue
			}

			metadata, err := music.ProbeMetadata(ffprobePath, filepath.Join(p.AssetsDir(), filepath.FromSlash(rel)))
			if err != nil {
				return nil, fmt.Errorf("probe music metadata for %s: %w", rel, err)
			}
			entry := music.CreditsEntry{
				Artist:       metadata.Artist,
				Title:        music.FirstNonEmpty(metadata.Title, strings.TrimSuffix(base, filepath.Ext(base))),
				OutputFile:   outputFile,
				OriginalFile: base,
				Album:        metadata.Album,
				Date:         metadata.Date,
				Rights:       music.FirstNonEmpty(metadata.Rights, music.JoinNonEmpty("; ", metadata.License, metadata.Copyright)),
				Notes:        music.FirstNonEmpty(metadata.Notes, metadata.Comment),
			}
			music.ApplyCreditsOverlay(&entry, manual)

			stagePath := filepath.Join(stageRoot, filepath.FromSlash(outputRel))
			if err := os.MkdirAll(filepath.Dir(stagePath), 0o755); err != nil {
				return nil, fmt.Errorf("create music stage dir: %w", err)
			}
			if err := music.ConvertSource(ffmpegPath, filepath.Join(p.AssetsDir(), filepath.FromSlash(rel)), stagePath); err != nil {
				return nil, fmt.Errorf("convert music %s: %w", rel, err)
			}
			if group.Dataset.PackageMode == "hak_asset" {
				resourceInfo, err := assetResourceFromPath(stagePath, outputRel, lfsAssetInfo{}, false)
				if err != nil {
					return nil, fmt.Errorf("load converted music %s: %w", outputRel, err)
				}
				info, err := os.Stat(stagePath)
				if err != nil {
					return nil, fmt.Errorf("stat converted music %s: %w", stagePath, err)
				}
				result.Generated = append(result.Generated, assetResource{
					Rel:       outputRel,
					Resource:  resourceInfo.Resource,
					Size:      erf.ArchiveSize([]erf.Resource{resourceInfo.Resource}),
					CreatedAt: info.ModTime(),
					ContentID: resourceInfo.ContentID,
				})
			}
			entries = append(entries, entry)
		}
		if opts.write {
			if err := music.ValidateOverlayEntries(group.SourceDir, overlay, entries); err != nil {
				return nil, err
			}
			creditGroups = append(creditGroups, musicCreditGroup{
				CreditsRoot: group.Dataset.CreditsRoot,
				SourceDir:   group.SourceDir,
				Entries:     entries,
			})
		}
	}

	if !opts.write {
		sort.Slice(result.Generated, func(i, j int) bool {
			return compareResourceKeys(result.Generated[i].Resource, result.Generated[j].Resource) < 0
		})
		return result, nil
	}

	writeResult, err := writeCreditsArtifacts(p, creditGroups)
	if err != nil {
		return nil, err
	}
	writeResult.Summary.ScannedDirs = append(writeResult.Summary.ScannedDirs, result.Summary.ScannedDirs...)
	writeResult.Summary.TrackCount = result.Summary.TrackCount
	result.Artifacts = writeResult.Artifacts
	result.Summary = writeResult.Summary
	sort.Slice(result.Generated, func(i, j int) bool {
		return compareResourceKeys(result.Generated[i].Resource, result.Generated[j].Resource) < 0
	})
	return result, nil
}

type creditsArtifactWriteResult struct {
	Artifacts []string
	Summary   CreditsRefreshSummary
}

func writeCreditsArtifacts(p *project.Project, generated []musicCreditGroup) (creditsArtifactWriteResult, error) {
	defaultCreditsRoot := p.CreditsDir()
	sources := make([]music.CreditsSource, 0)
	desiredByRoot := map[string]map[string][]byte{}
	summary := CreditsRefreshSummary{}
	if generated != nil {
		sort.Slice(generated, func(i, j int) bool {
			if generated[i].SourceDir != generated[j].SourceDir {
				return generated[i].SourceDir < generated[j].SourceDir
			}
			return generated[i].CreditsRoot < generated[j].CreditsRoot
		})
		for _, group := range generated {
			creditsRoot := musicRootPath(p, group.CreditsRoot)
			if desiredByRoot[creditsRoot] == nil {
				desiredByRoot[creditsRoot] = map[string][]byte{}
			}
			entries := append([]music.CreditsEntry(nil), group.Entries...)
			sort.Slice(entries, func(i, j int) bool {
				if strings.ToLower(entries[i].Artist) != strings.ToLower(entries[j].Artist) {
					return strings.ToLower(entries[i].Artist) < strings.ToLower(entries[j].Artist)
				}
				if strings.ToLower(entries[i].Title) != strings.ToLower(entries[j].Title) {
					return strings.ToLower(entries[i].Title) < strings.ToLower(entries[j].Title)
				}
				return strings.ToLower(entries[i].OutputFile) < strings.ToLower(entries[j].OutputFile)
			})
			outputPath := filepath.Join(creditsRoot, filepath.FromSlash(group.SourceDir), p.EffectiveConfig().Music.CreditsOverlay)
			desiredByRoot[creditsRoot][outputPath] = []byte(music.RenderCreditsMarkdown(entries))
			summary.GeneratedPaths = append(summary.GeneratedPaths, outputPath)
			for _, entry := range entries {
				summary.Mappings = append(summary.Mappings, MusicFileMapping{
					Source: entry.OriginalFile,
					Output: entry.OutputFile,
				})
			}
			sources = append(sources, music.CreditsSource{
				Path:    filepath.ToSlash(outputPath),
				Kind:    "generated_music",
				Entries: entries,
			})
		}
	}

	err := filepath.WalkDir(p.AssetsDir(), func(path string, d os.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if d.IsDir() {
			return nil
		}
		if strings.ToLower(d.Name()) != "credits.md" {
			return nil
		}
		entries, err := music.ParseCreditsMarkdown(path)
		if err != nil {
			return fmt.Errorf("parse credits %s: %w", path, err)
		}
		rel, err := filepath.Rel(p.Root, path)
		if err != nil {
			return err
		}
		sources = append(sources, music.CreditsSource{
			Path:    filepath.ToSlash(rel),
			Kind:    "authored",
			Entries: entries,
		})
		return nil
	})
	if err != nil {
		return creditsArtifactWriteResult{}, err
	}

	sort.Slice(sources, func(i, j int) bool { return sources[i].Path < sources[j].Path })
	payload, err := json.MarshalIndent(music.CreditsInventory{Sources: sources}, "", "  ")
	if err != nil {
		return creditsArtifactWriteResult{}, fmt.Errorf("marshal credits inventory: %w", err)
	}
	payload = append(payload, '\n')
	if len(desiredByRoot) == 0 {
		desiredByRoot[defaultCreditsRoot] = map[string][]byte{}
	}
	artifacts := make([]string, 0)
	changedFiles := 0
	roots := make([]string, 0, len(desiredByRoot))
	for root := range desiredByRoot {
		roots = append(roots, root)
	}
	sort.Strings(roots)
	for _, creditsRoot := range roots {
		desiredFiles := desiredByRoot[creditsRoot]
		inventoryPath := filepath.Join(creditsRoot, "credits.json")
		desiredFiles[inventoryPath] = payload
		if err := os.MkdirAll(creditsRoot, 0o755); err != nil {
			return creditsArtifactWriteResult{}, fmt.Errorf("create credits dir: %w", err)
		}
		changed, err := music.SyncGeneratedCreditsArtifacts(creditsRoot, desiredFiles)
		if err != nil {
			return creditsArtifactWriteResult{}, err
		}
		changedFiles += changed
		for path := range desiredFiles {
			artifacts = append(artifacts, path)
		}
		if summary.InventoryPath == "" {
			summary.InventoryPath = inventoryPath
		}
	}
	sort.Strings(artifacts)
	summary.ArtifactPaths = append(summary.ArtifactPaths, artifacts...)
	summary.ChangedFiles = changedFiles
	return creditsArtifactWriteResult{
		Artifacts: artifacts,
		Summary:   summary,
	}, nil
}

func cleanupPreparedMusicAssets(prepared *preparedMusicAssets) error {
	if prepared == nil {
		return nil
	}
	for _, root := range prepared.TempRoots {
		if strings.TrimSpace(root) == "" {
			continue
		}
		if err := os.RemoveAll(root); err != nil {
			return fmt.Errorf("remove temporary music artifact root %s: %w", root, err)
		}
	}
	return nil
}
