package project

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"time"

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

const (
	DefaultBuildPath                       = "build"
	DefaultCachePath                       = ".cache"
	DefaultToolsPath                       = "tools"
	DefaultModuleArchiveTemplate           = "{module.resref}.mod"
	DefaultHAKManifest                     = "haks.json"
	DefaultHAKArchiveTemplate              = "{hak.name}.hak"
	DefaultSourceJSONPattern               = "{resref}.{extension}.json"
	DefaultValidationProfile               = "nwn_module"
	DefaultScriptsSourceDir                = "scripts"
	DefaultScriptsCache                    = "{paths.cache}/ncs"
	DefaultScriptCompilerEnvPath           = "SOW_NWN_SCRIPT_COMPILER"
	DefaultMusicStageRoot                  = "{paths.cache}/music"
	DefaultCreditsRoot                     = "{paths.cache}/credits"
	DefaultCreditsOverlayFile              = "CREDITS.md"
	DefaultMusicNamingScheme               = "nwn_bmu"
	DefaultMusicOutputExtension            = ".bmu"
	DefaultTopDataBuild                    = "{paths.build}/topdata"
	DefaultTopDataCompiled2DADir           = "2da"
	DefaultTopDataCompiledTLK              = "sow_tlk.tlk"
	DefaultTopDataWikiOutputRoot           = "wiki"
	DefaultTopDataWikiPagesDir             = "pages"
	DefaultTopDataWikiStateFile            = "state.json"
	DefaultTopDataWikiSource               = "topdata/wiki"
	DefaultTopDataWikiRenderer             = "nodebb_tiptap_html"
	DefaultTopDataWikiLinkStrategy         = "preserve_westgate_wiki_links"
	DefaultTopDataWikiNamespacesFile       = "namespaces.yaml"
	DefaultTopDataWikiTemplatesDir         = "templates"
	DefaultTopDataWikiManualSectionsDir    = "manual-sections"
	DefaultTopDataWikiStaleDefault         = "report"
	DefaultTopDataWikiStaleLiveCleanup     = "archive"
	DefaultTopDataWikiMarkerFormat         = "html_comments"
	DefaultTopDataWikiPageMarkerPrefix     = "sow-topdata-wiki"
	DefaultTopDataWikiTitlePrefixMinLength = 3
	DefaultWikiDeployManifest              = ".wiki_deploy_manifest.json"
	DefaultWikiDeployEditSummary           = "Auto-generated from native builder"
	DefaultTopDataPackageHAK               = "sow_top.hak"
	DefaultTopDataPackageTLK               = "sow_tlk.tlk"
	DefaultAutogenCacheRoot                = "{paths.cache}"
	DefaultAutogenCacheMaxAge              = time.Hour
	DefaultAutogenRefreshEnv               = "SOW_AUTOGEN_MANIFEST_REFRESH"
	DefaultPartsManifestRefreshEnv         = "SOW_PARTS_MANIFEST_REFRESH"
	DefaultAutogenReleaseProvider          = "gitea"
	DefaultAutogenServerURLEnv             = "SOW_ASSETS_SERVER_URL"
	DefaultAutogenRepoEnv                  = "SOW_ASSETS_REPO"
	DefaultExtractLayout                   = "nwn_canonical_json"
	DefaultExtractHAKDiscovery             = "build_glob"
)

type ConfigProvenance map[string]ConfigValueProvenance

type ConfigValueProvenance struct {
	Source     string `json:"source" yaml:"source"`
	Detail     string `json:"detail,omitempty" yaml:"detail,omitempty"`
	Deprecated bool   `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
}

type EffectiveConfig struct {
	ConfigSource ConfigSource           `json:"config_source" yaml:"config_source"`
	Module       ModuleConfig           `json:"module" yaml:"module"`
	Paths        EffectivePathConfig    `json:"paths" yaml:"paths"`
	Outputs      EffectiveOutputConfig  `json:"outputs" yaml:"outputs"`
	Build        BuildConfig            `json:"build" yaml:"build"`
	Generated    GeneratedConfig        `json:"generated_assets" yaml:"generated_assets"`
	Inventory    InventoryConfig        `json:"inventory" yaml:"inventory"`
	Validation   ValidationConfig       `json:"validation" yaml:"validation"`
	Scripts      EffectiveScriptsConfig `json:"scripts" yaml:"scripts"`
	Music        EffectiveMusicConfig   `json:"music" yaml:"music"`
	TopData      EffectiveTopDataConfig `json:"topdata" yaml:"topdata"`
	Extract      ExtractConfig          `json:"extract" yaml:"extract"`
	Autogen      EffectiveAutogenConfig `json:"autogen" yaml:"autogen"`
	HAKs         []HAKConfig            `json:"haks" yaml:"haks"`
	Provenance   ConfigProvenance       `json:"provenance" yaml:"provenance"`
	Overrides    []ConfigOverride       `json:"overrides,omitempty" yaml:"overrides,omitempty"`
}

type EffectivePathConfig struct {
	Source string `json:"source" yaml:"source"`
	Assets string `json:"assets" yaml:"assets"`
	Build  string `json:"build" yaml:"build"`
	Cache  string `json:"cache" yaml:"cache"`
	Tools  string `json:"tools" yaml:"tools"`
}

type EffectiveOutputConfig struct {
	ModuleArchive string `json:"module_archive" yaml:"module_archive"`
	HAKManifest   string `json:"hak_manifest" yaml:"hak_manifest"`
	HAKArchive    string `json:"hak_archive" yaml:"hak_archive"`
}

type EffectiveScriptsConfig struct {
	SourceDir string                        `json:"source_dir" yaml:"source_dir"`
	Cache     string                        `json:"cache" yaml:"cache"`
	Compiler  EffectiveScriptCompilerConfig `json:"compiler" yaml:"compiler"`
}

type EffectiveScriptCompilerConfig struct {
	Path   string                           `json:"path" yaml:"path"`
	Search []string                         `json:"search" yaml:"search"`
	Env    EffectiveScriptCompilerEnvConfig `json:"env" yaml:"env"`
}

type EffectiveScriptCompilerEnvConfig struct {
	Path             string   `json:"path" yaml:"path"`
	NWNRoot          []string `json:"nwn_root" yaml:"nwn_root"`
	NWNUserDirectory []string `json:"nwn_user_directory" yaml:"nwn_user_directory"`
}

type EffectiveMusicConfig struct {
	Prefixes          map[string]string                `json:"prefixes" yaml:"prefixes"`
	Tools             MusicToolsConfig                 `json:"tools" yaml:"tools"`
	StageRoot         string                           `json:"stage_root" yaml:"stage_root"`
	CreditsRoot       string                           `json:"credits_root" yaml:"credits_root"`
	CreditsOverlay    string                           `json:"credits_overlay" yaml:"credits_overlay"`
	MaxStemLength     int                              `json:"max_stem_length" yaml:"max_stem_length"`
	NamingScheme      string                           `json:"naming_scheme" yaml:"naming_scheme"`
	OutputExtension   string                           `json:"output_extension" yaml:"output_extension"`
	ConvertExtensions []string                         `json:"convert_extensions" yaml:"convert_extensions"`
	Datasets          map[string]EffectiveMusicDataset `json:"datasets,omitempty" yaml:"datasets,omitempty"`
}

type EffectiveMusicDataset struct {
	Source            string   `json:"source" yaml:"source"`
	Output            string   `json:"output" yaml:"output"`
	Prefix            string   `json:"prefix,omitempty" yaml:"prefix,omitempty"`
	StageRoot         string   `json:"stage_root,omitempty" yaml:"stage_root,omitempty"`
	CreditsRoot       string   `json:"credits_root,omitempty" yaml:"credits_root,omitempty"`
	NamingScheme      string   `json:"naming_scheme" yaml:"naming_scheme"`
	OutputExtension   string   `json:"output_extension" yaml:"output_extension"`
	MaxStemLength     int      `json:"max_stem_length" yaml:"max_stem_length"`
	PackageMode       string   `json:"package_mode" yaml:"package_mode"`
	ConvertExtensions []string `json:"convert_extensions" yaml:"convert_extensions"`
}

type EffectiveTopDataConfig struct {
	Source           string            `json:"source" yaml:"source"`
	Build            string            `json:"build" yaml:"build"`
	ReferenceBuilder string            `json:"reference_builder" yaml:"reference_builder"`
	Assets           string            `json:"assets" yaml:"assets"`
	Compiled2DADir   string            `json:"compiled_2da_dir" yaml:"compiled_2da_dir"`
	CompiledTLK      string            `json:"compiled_tlk" yaml:"compiled_tlk"`
	PackageHAK       string            `json:"package_hak" yaml:"package_hak"`
	PackageTLK       string            `json:"package_tlk" yaml:"package_tlk"`
	Wiki             TopDataWikiConfig `json:"wiki" yaml:"wiki"`
}

type EffectiveAutogenConfig struct {
	Producers     []AutogenProducerConfig             `json:"producers" yaml:"producers"`
	Consumers     []AutogenConsumerConfig             `json:"consumers" yaml:"consumers"`
	Cache         EffectiveAutogenCacheConfig         `json:"cache" yaml:"cache"`
	ReleaseSource EffectiveAutogenReleaseSourceConfig `json:"release_source" yaml:"release_source"`
}

type EffectiveAutogenCacheConfig struct {
	Root                    string `json:"root" yaml:"root"`
	MaxAge                  string `json:"max_age" yaml:"max_age"`
	RefreshEnv              string `json:"refresh_env" yaml:"refresh_env"`
	PartsManifestRefreshEnv string `json:"parts_manifest_refresh_env" yaml:"parts_manifest_refresh_env"`
}

type EffectiveAutogenReleaseSourceConfig struct {
	Provider     string `json:"provider" yaml:"provider"`
	ServerURLEnv string `json:"server_url_env" yaml:"server_url_env"`
	RepoEnv      string `json:"repo_env" yaml:"repo_env"`
}

type ConfigOverride struct {
	Key    string `json:"key" yaml:"key"`
	Value  string `json:"value" yaml:"value"`
	Source string `json:"source" yaml:"source"`
}

func (p *Project) EffectiveConfig() EffectiveConfig {
	provenance := cloneProvenance(p.Provenance)
	markMissingDefaults(provenance)

	paths := EffectivePathConfig{
		Source: strings.TrimSpace(p.Config.Paths.Source),
		Assets: strings.TrimSpace(p.Config.Paths.Assets),
		Build:  defaultString(p.Config.Paths.Build, DefaultBuildPath),
		Cache:  defaultString(p.Config.Paths.Cache, DefaultCachePath),
		Tools:  defaultString(p.Config.Paths.Tools, DefaultToolsPath),
	}
	outputs := EffectiveOutputConfig{
		ModuleArchive: defaultString(p.Config.Outputs.ModuleArchive, DefaultModuleArchiveTemplate),
		HAKManifest:   defaultString(p.Config.Outputs.HAKManifest, DefaultHAKManifest),
		HAKArchive:    defaultString(p.Config.Outputs.HAKArchive, DefaultHAKArchiveTemplate),
	}
	inventory := InventoryConfig{
		SourceExtensions:  defaultStringSlice(p.Config.Inventory.SourceExtensions, SourceExtensions),
		AssetExtensions:   defaultStringSlice(p.Config.Inventory.AssetExtensions, AssetExtensions),
		SourceJSONPattern: defaultString(p.Config.Inventory.SourceJSONPattern, DefaultSourceJSONPattern),
	}
	validation := ValidationConfig{
		Profile:               defaultString(p.Config.Validation.Profile, DefaultValidationProfile),
		BuiltinScriptPrefixes: normalizeLowerStringSlice(defaultStringSlice(p.Config.Validation.BuiltinScriptPrefixes, DefaultBuiltinScriptPrefixes())),
		RequiredFields:        defaultRequiredFields(p.Config.Validation.RequiredFields),
	}
	scripts := EffectiveScriptsConfig{
		SourceDir: defaultString(p.Config.Scripts.SourceDir, DefaultScriptsSourceDir),
		Cache:     expandPathTemplate(defaultString(p.Config.Scripts.Cache, DefaultScriptsCache), paths),
		Compiler: EffectiveScriptCompilerConfig{
			Path:   strings.TrimSpace(p.Config.Scripts.Compiler.Path),
			Search: expandPathTemplates(defaultStringSlice(p.Config.Scripts.Compiler.Search, defaultScriptCompilerSearch()), paths),
			Env: EffectiveScriptCompilerEnvConfig{
				Path:             defaultString(p.Config.Scripts.Compiler.Env.Path, DefaultScriptCompilerEnvPath),
				NWNRoot:          defaultStringSlice(p.Config.Scripts.Compiler.Env.NWNRoot, []string{"SOW_NWN_ROOT", "NWN_ROOT"}),
				NWNUserDirectory: defaultStringSlice(p.Config.Scripts.Compiler.Env.NWNUserDirectory, []string{"SOW_NWN_USER_DIRECTORY", "NWN_HOME", "NWN_USER_DIRECTORY"}),
			},
		},
	}
	topBuild := expandPathTemplate(defaultString(p.Config.TopData.Build, DefaultTopDataBuild), paths)
	top := EffectiveTopDataConfig{
		Source:           strings.TrimSpace(p.Config.TopData.Source),
		Build:            topBuild,
		ReferenceBuilder: strings.TrimSpace(p.Config.TopData.ReferenceBuilder),
		Assets:           strings.TrimSpace(p.Config.TopData.Assets),
		Compiled2DADir:   defaultString(p.Config.TopData.Compiled2DADir, DefaultTopDataCompiled2DADir),
		CompiledTLK:      defaultString(p.Config.TopData.CompiledTLK, DefaultTopDataCompiledTLK),
		PackageHAK:       defaultString(p.Config.TopData.PackageHAK, DefaultTopDataPackageHAK),
		PackageTLK:       defaultString(p.Config.TopData.PackageTLK, DefaultTopDataPackageTLK),
		Wiki: TopDataWikiConfig{
			OutputRoot:           defaultString(p.Config.TopData.Wiki.OutputRoot, DefaultTopDataWikiOutputRoot),
			PagesDir:             defaultString(p.Config.TopData.Wiki.PagesDir, DefaultTopDataWikiPagesDir),
			StateFile:            defaultString(p.Config.TopData.Wiki.StateFile, DefaultTopDataWikiStateFile),
			ManagedNamespaces:    defaultStringSlice(p.Config.TopData.Wiki.ManagedNamespaces, []string{"classes", "feat", "itemtypes", "races", "skills", "spells", "meta"}),
			DeployManifest:       defaultString(p.Config.TopData.Wiki.DeployManifest, DefaultWikiDeployManifest),
			DeployEditSummary:    defaultString(p.Config.TopData.Wiki.DeployEditSummary, DefaultWikiDeployEditSummary),
			Source:               defaultString(p.Config.TopData.Wiki.Source, DefaultTopDataWikiSource),
			Renderer:             defaultString(p.Config.TopData.Wiki.Renderer, DefaultTopDataWikiRenderer),
			LinkStrategy:         defaultString(p.Config.TopData.Wiki.LinkStrategy, DefaultTopDataWikiLinkStrategy),
			NamespacesFile:       defaultString(p.Config.TopData.Wiki.NamespacesFile, DefaultTopDataWikiNamespacesFile),
			TemplatesDir:         defaultString(p.Config.TopData.Wiki.TemplatesDir, DefaultTopDataWikiTemplatesDir),
			ManualSectionsDir:    defaultString(p.Config.TopData.Wiki.ManualSectionsDir, DefaultTopDataWikiManualSectionsDir),
			TitlePrefixMinLength: defaultInt(p.Config.TopData.Wiki.TitlePrefixMinLength, DefaultTopDataWikiTitlePrefixMinLength),
			StalePages: TopDataWikiStalePagesConfig{
				Default:     defaultString(p.Config.TopData.Wiki.StalePages.Default, DefaultTopDataWikiStaleDefault),
				LiveCleanup: defaultString(p.Config.TopData.Wiki.StalePages.LiveCleanup, DefaultTopDataWikiStaleLiveCleanup),
			},
			ManagedRegion: TopDataWikiManagedRegionConfig{
				MarkerFormat:     defaultString(p.Config.TopData.Wiki.ManagedRegion.MarkerFormat, DefaultTopDataWikiMarkerFormat),
				PageMarkerPrefix: defaultString(p.Config.TopData.Wiki.ManagedRegion.PageMarkerPrefix, DefaultTopDataWikiPageMarkerPrefix),
			},
		},
	}
	generated := p.Config.Generated
	for index := range generated.TopData2DA {
		generated.TopData2DA[index].Output = expandPathTemplate(defaultString(generated.TopData2DA[index].Output, "{paths.cache}/generated-assets/"+generated.TopData2DA[index].ID), paths)
	}

	extract := p.Config.Extract
	extract.Layout = defaultString(extract.Layout, DefaultExtractLayout)
	extract.HAKDiscovery = defaultString(extract.HAKDiscovery, DefaultExtractHAKDiscovery)
	if len(extract.Archives) == 0 {
		extract.Archives = []string{outputs.ModuleArchiveName(p.Config.Module)}
	}
	if extract.CleanupStale == nil {
		defaultCleanup := true
		extract.CleanupStale = &defaultCleanup
	}
	if extract.ConsumeArchives == nil {
		defaultConsume := false
		extract.ConsumeArchives = &defaultConsume
	}

	musicStageRoot := expandPathTemplate(DefaultMusicStageRoot, paths)
	musicCreditsRoot := expandPathTemplate(DefaultCreditsRoot, paths)
	musicCreditsOverlay := DefaultCreditsOverlayFile
	musicMaxStemLength := 16
	musicNamingScheme := DefaultMusicNamingScheme
	musicOutputExtension := DefaultMusicOutputExtension
	musicConvertExtensions := music.DefaultConvertExtensions()
	if d := p.Config.Music.Defaults; d != nil {
		if d.StageRoot != "" {
			musicStageRoot = expandPathTemplate(d.StageRoot, paths)
		}
		if d.CreditsRoot != "" {
			musicCreditsRoot = expandPathTemplate(d.CreditsRoot, paths)
		}
		if d.CreditsOverlay != "" {
			musicCreditsOverlay = d.CreditsOverlay
		}
		if d.MaxStemLength != nil {
			musicMaxStemLength = *d.MaxStemLength
		}
		if d.NamingScheme != "" {
			musicNamingScheme = d.NamingScheme
		}
		if d.OutputExtension != "" {
			musicOutputExtension = normalizeExtension(d.OutputExtension)
		}
		if len(d.ConvertExtensions) > 0 {
			musicConvertExtensions = normalizeExtensionSlice(d.ConvertExtensions)
		}
	}
	musicPrefixes := cloneStringMap(p.Config.Music.Prefixes)
	if len(musicPrefixes) == 0 {
		musicPrefixes = make(map[string]string, len(p.Config.Music.Datasets))
		for _, ds := range p.Config.Music.Datasets {
			if ds.Source != "" && ds.Prefix != "" {
				musicPrefixes[ds.Source] = ds.Prefix
			}
		}
	}
	effectiveDatasets := make(map[string]EffectiveMusicDataset, len(p.Config.Music.Datasets))
	for id, ds := range p.Config.Music.Datasets {
		dsStageRoot := ds.StageRoot
		if dsStageRoot == "" {
			dsStageRoot = musicStageRoot
		}
		dsCreditsRoot := ds.CreditsRoot
		if dsCreditsRoot == "" {
			dsCreditsRoot = musicCreditsRoot
		}
		dsOutput := strings.TrimSpace(ds.Output)
		if dsOutput == "" {
			dsOutput = ds.Source
		}
		dsNamingScheme := defaultString(ds.NamingScheme, musicNamingScheme)
		dsOutputExtension := musicOutputExtension
		if ds.OutputExtension != "" {
			dsOutputExtension = normalizeExtension(ds.OutputExtension)
		}
		dsConvertExtensions := musicConvertExtensions
		if len(ds.ConvertExtensions) > 0 {
			dsConvertExtensions = normalizeExtensionSlice(ds.ConvertExtensions)
		}
		effectiveDatasets[id] = EffectiveMusicDataset{
			Source:            ds.Source,
			Output:            dsOutput,
			Prefix:            ds.Prefix,
			StageRoot:         dsStageRoot,
			CreditsRoot:       dsCreditsRoot,
			NamingScheme:      dsNamingScheme,
			OutputExtension:   dsOutputExtension,
			MaxStemLength:     musicMaxStemLength,
			PackageMode:       defaultString(ds.PackageMode, "hak_asset"),
			ConvertExtensions: dsConvertExtensions,
		}
	}
	effective := EffectiveConfig{
		ConfigSource: p.ConfigSource,
		Module:       p.Config.Module,
		Paths:        paths,
		Outputs:      outputs,
		Build:        p.Config.Build,
		Generated:    generated,
		Inventory:    inventory,
		Validation:   validation,
		Scripts:      scripts,
		Music: EffectiveMusicConfig{
			Prefixes:          musicPrefixes,
			Tools:             p.Config.Music.Tools,
			StageRoot:         musicStageRoot,
			CreditsRoot:       musicCreditsRoot,
			CreditsOverlay:    musicCreditsOverlay,
			MaxStemLength:     musicMaxStemLength,
			NamingScheme:      musicNamingScheme,
			OutputExtension:   musicOutputExtension,
			ConvertExtensions: musicConvertExtensions,
			Datasets:          effectiveDatasets,
		},
		TopData: top,
		Extract: extract,
		Autogen: EffectiveAutogenConfig{
			Producers: slices.Clone(p.Config.Autogen.Producers),
			Consumers: slices.Clone(p.Config.Autogen.Consumers),
			Cache: EffectiveAutogenCacheConfig{
				Root:                    expandPathTemplate(defaultString(p.Config.Autogen.Cache.Root, DefaultAutogenCacheRoot), paths),
				MaxAge:                  defaultString(p.Config.Autogen.Cache.MaxAge, DefaultAutogenCacheMaxAge.String()),
				RefreshEnv:              defaultString(p.Config.Autogen.Cache.RefreshEnv, DefaultAutogenRefreshEnv),
				PartsManifestRefreshEnv: DefaultPartsManifestRefreshEnv,
			},
			ReleaseSource: EffectiveAutogenReleaseSourceConfig{
				Provider:     defaultString(p.Config.Autogen.ReleaseSource.Provider, DefaultAutogenReleaseProvider),
				ServerURLEnv: defaultString(p.Config.Autogen.ReleaseSource.ServerURLEnv, DefaultAutogenServerURLEnv),
				RepoEnv:      defaultString(p.Config.Autogen.ReleaseSource.RepoEnv, DefaultAutogenRepoEnv),
			},
		},
		HAKs:       slices.Clone(p.Config.HAKs),
		Provenance: provenance,
	}
	effective.Overrides = activeOverrides(effective)
	return effective
}

func (p *Project) EffectiveConfigJSON() ([]byte, error) {
	return json.MarshalIndent(p.EffectiveConfig(), "", "  ")
}

func (p *Project) ExplainConfig(key string) (any, ConfigValueProvenance, bool) {
	effective := p.EffectiveConfig()
	value, ok := lookupEffectiveValue(effective, key)
	if !ok {
		return nil, ConfigValueProvenance{}, false
	}
	prov := effective.Provenance[key]
	if prov.Source == "" {
		prov = ConfigValueProvenance{Source: "yaml", Detail: p.ConfigSource.Name}
	}
	return value, prov, true
}

func lookupEffectiveValue(effective EffectiveConfig, key string) (any, bool) {
	raw, err := json.Marshal(effective)
	if err != nil {
		return nil, false
	}
	var data any
	if err := json.Unmarshal(raw, &data); err != nil {
		return nil, false
	}
	current := data
	for _, part := range strings.Split(key, ".") {
		object, ok := current.(map[string]any)
		if !ok {
			return nil, false
		}
		current, ok = object[part]
		if !ok {
			return nil, false
		}
	}
	return current, true
}

func markMissingDefaults(provenance ConfigProvenance) {
	defaults := map[string]string{
		"paths.build":                                    "build",
		"paths.cache":                                    ".cache",
		"paths.tools":                                    "tools",
		"outputs.module_archive":                         DefaultModuleArchiveTemplate,
		"outputs.hak_manifest":                           DefaultHAKManifest,
		"outputs.hak_archive":                            DefaultHAKArchiveTemplate,
		"generated_assets.topdata_2da":                   "no generated topdata 2DA assets",
		"inventory.source_extensions":                    "NWN source resource extensions",
		"inventory.asset_extensions":                     "NWN asset resource extensions",
		"inventory.source_json_pattern":                  DefaultSourceJSONPattern,
		"validation.profile":                             DefaultValidationProfile,
		"validation.builtin_script_prefixes":             "NWN built-in script prefixes",
		"validation.required_fields":                     "NWN required GFF fields",
		"scripts.source_dir":                             DefaultScriptsSourceDir,
		"scripts.cache":                                  DefaultScriptsCache,
		"scripts.compiler.env.path":                      DefaultScriptCompilerEnvPath,
		"scripts.compiler.search":                        "repo tools and PATH:nwn_script_comp",
		"music.stage_root":                               DefaultMusicStageRoot,
		"music.credits_root":                             DefaultCreditsRoot,
		"music.credits_overlay":                          DefaultCreditsOverlayFile,
		"music.max_stem_length":                          "16",
		"music.naming_scheme":                            DefaultMusicNamingScheme,
		"music.output_extension":                         DefaultMusicOutputExtension,
		"music.convert_extensions":                       strings.Join(music.DefaultConvertExtensions(), ", "),
		"topdata.build":                                  DefaultTopDataBuild,
		"topdata.compiled_2da_dir":                       DefaultTopDataCompiled2DADir,
		"topdata.compiled_tlk":                           DefaultTopDataCompiledTLK,
		"topdata.package_hak":                            DefaultTopDataPackageHAK,
		"topdata.package_tlk":                            DefaultTopDataPackageTLK,
		"topdata.wiki.output_root":                       DefaultTopDataWikiOutputRoot,
		"topdata.wiki.pages_dir":                         DefaultTopDataWikiPagesDir,
		"topdata.wiki.state_file":                        DefaultTopDataWikiStateFile,
		"topdata.wiki.managed_namespaces":                "NWN topdata wiki namespaces",
		"topdata.wiki.deploy_manifest":                   DefaultWikiDeployManifest,
		"topdata.wiki.deploy_edit_summary":               DefaultWikiDeployEditSummary,
		"topdata.wiki.source":                            DefaultTopDataWikiSource,
		"topdata.wiki.renderer":                          DefaultTopDataWikiRenderer,
		"topdata.wiki.link_strategy":                     DefaultTopDataWikiLinkStrategy,
		"topdata.wiki.namespaces_file":                   DefaultTopDataWikiNamespacesFile,
		"topdata.wiki.templates_dir":                     DefaultTopDataWikiTemplatesDir,
		"topdata.wiki.manual_sections_dir":               DefaultTopDataWikiManualSectionsDir,
		"topdata.wiki.title_prefix_min_length":           "3",
		"topdata.wiki.stale_pages.default":               DefaultTopDataWikiStaleDefault,
		"topdata.wiki.stale_pages.live_cleanup":          DefaultTopDataWikiStaleLiveCleanup,
		"topdata.wiki.managed_region.marker_format":      DefaultTopDataWikiMarkerFormat,
		"topdata.wiki.managed_region.page_marker_prefix": DefaultTopDataWikiPageMarkerPrefix,
		"extract.layout":                                 DefaultExtractLayout,
		"extract.hak_discovery":                          DefaultExtractHAKDiscovery,
		"extract.archives":                               DefaultModuleArchiveTemplate,
		"extract.cleanup_stale":                          "true",
		"extract.consume_archives":                       "false",
		"autogen.cache.root":                             DefaultAutogenCacheRoot,
		"autogen.cache.max_age":                          DefaultAutogenCacheMaxAge.String(),
		"autogen.cache.refresh_env":                      DefaultAutogenRefreshEnv,
		"autogen.cache.parts_manifest_refresh_env":       DefaultPartsManifestRefreshEnv,
		"autogen.release_source.provider":                DefaultAutogenReleaseProvider,
		"autogen.release_source.server_url_env":          DefaultAutogenServerURLEnv,
		"autogen.release_source.repo_env":                DefaultAutogenRepoEnv,
	}
	for key, detail := range defaults {
		if _, ok := provenance[key]; !ok {
			provenance[key] = ConfigValueProvenance{Source: "toolkit default", Detail: detail}
		}
	}
}

func cloneProvenance(input ConfigProvenance) ConfigProvenance {
	out := ConfigProvenance{}
	for key, value := range input {
		out[key] = value
	}
	return out
}

func defaultString(value, fallback string) string {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return fallback
	}
	return trimmed
}

func defaultInt(value, fallback int) int {
	if value == 0 {
		return fallback
	}
	return value
}

func defaultStringSlice(value, fallback []string) []string {
	if len(value) == 0 {
		return slices.Clone(fallback)
	}
	return slices.Clone(value)
}

func defaultRequiredFields(configured map[string][]string) map[string][]string {
	if len(configured) > 0 {
		return cloneStringSliceMap(normalizeRequiredFields(configured))
	}
	return DefaultRequiredFields()
}

func cloneStringSliceMap(input map[string][]string) map[string][]string {
	out := make(map[string][]string, len(input))
	for key, values := range input {
		out[key] = slices.Clone(values)
	}
	return out
}

func cloneStringMap(input map[string]string) map[string]string {
	if len(input) == 0 {
		return nil
	}
	out := make(map[string]string, len(input))
	for key, value := range input {
		out[key] = value
	}
	return out
}

func expandPathTemplate(template string, paths EffectivePathConfig) string {
	replacer := strings.NewReplacer(
		"{paths.source}", paths.Source,
		"{paths.assets}", paths.Assets,
		"{paths.build}", paths.Build,
		"{paths.cache}", paths.Cache,
		"{paths.tools}", paths.Tools,
	)
	return filepath.ToSlash(replacer.Replace(template))
}

func expandPathTemplates(templates []string, paths EffectivePathConfig) []string {
	out := make([]string, 0, len(templates))
	for _, template := range templates {
		out = append(out, expandPathTemplate(template, paths))
	}
	return out
}

func defaultScriptCompilerSearch() []string {
	return []string{
		"{paths.tools}/script-compiler/nwn_script_comp",
		"{paths.tools}/script-compiler/nwn_script_comp.exe",
		"{paths.tools}/nwn_script_comp",
		"{paths.tools}/nwn_script_comp.exe",
		"PATH:nwn_script_comp",
		"PATH:nwn_script_comp.exe",
	}
}

func (p *Project) ActiveOverrides() []ConfigOverride {
	return activeOverrides(p.EffectiveConfig())
}

func activeOverrides(effective EffectiveConfig) []ConfigOverride {
	envs := map[string]string{
		"build.keep_existing_haks":             effectiveBuildKeepExistingEnv,
		"scripts.compiler.path":                effective.Scripts.Compiler.Env.Path,
		"autogen.cache.refresh":                effective.Autogen.Cache.RefreshEnv,
		"autogen.cache.parts_manifest_refresh": effective.Autogen.Cache.PartsManifestRefreshEnv,
		"autogen.release_source.server_url":    effective.Autogen.ReleaseSource.ServerURLEnv,
		"autogen.release_source.repo":          effective.Autogen.ReleaseSource.RepoEnv,
		"music.ffmpeg":                         "SOW_FFMPEG",
		"music.ffprobe":                        "SOW_FFPROBE",
		"wiki.endpoint":                        "NODEBB_API_ENDPOINT",
		"wiki.token":                           "NODEBB_API_TOKEN",
		"wiki.categories":                      "NODEBB_WIKI_CATEGORIES",
		"release.version":                      "GITHUB_REF_NAME",
	}
	for _, key := range effective.Scripts.Compiler.Env.NWNRoot {
		envs["scripts.compiler.env.nwn_root."+key] = key
	}
	for _, key := range effective.Scripts.Compiler.Env.NWNUserDirectory {
		envs["scripts.compiler.env.nwn_user_directory."+key] = key
	}
	keys := make([]string, 0, len(envs))
	for key := range envs {
		keys = append(keys, key)
	}
	slices.Sort(keys)
	overrides := make([]ConfigOverride, 0)
	for _, key := range keys {
		env := envs[key]
		if value := strings.TrimSpace(os.Getenv(env)); value != "" {
			if isSensitiveOverrideKey(key) || isSensitiveOverrideKey(env) {
				value = "<set>"
			}
			overrides = append(overrides, ConfigOverride{Key: key, Value: value, Source: "env:" + env})
		}
	}
	return overrides
}

const effectiveBuildKeepExistingEnv = "SOW_BUILD_HAKS_KEEP_EXISTING"

func isSensitiveOverrideKey(key string) bool {
	lower := strings.ToLower(key)
	return strings.Contains(lower, "token") || strings.Contains(lower, "password")
}

func (o EffectiveOutputConfig) ModuleArchiveName(module ModuleConfig) string {
	return strings.NewReplacer("{module.resref}", strings.TrimSpace(module.ResRef)).Replace(o.ModuleArchive)
}

func (o EffectiveOutputConfig) HAKArchiveName(name string) string {
	return strings.NewReplacer("{hak.name}", strings.TrimSpace(name)).Replace(o.HAKArchive)
}

func (p ConfigValueProvenance) String() string {
	if p.Detail == "" {
		return p.Source
	}
	return fmt.Sprintf("%s (%s)", p.Source, p.Detail)
}
