package tsoptions

import (
	"reflect"
	"strings"

	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/collections"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/diagnostics"
	"github.com/microsoft/typescript-go/internal/tspath"
)

func ParseTristate(value any) core.Tristate {
	if value == nil {
		return core.TSUnknown
	}
	if v, ok := value.(core.Tristate); ok {
		return v
	}
	if value == true {
		return core.TSTrue
	} else {
		return core.TSFalse
	}
}

func ParseStringArray(value any) []string {
	if arr, ok := value.([]any); ok {
		if arr == nil {
			return nil
		}
		result := make([]string, 0, len(arr))
		for _, v := range arr {
			if str, ok := v.(string); ok {
				result = append(result, str)
			}
		}
		return result
	}
	return nil
}

func parseStringMap(value any) *collections.OrderedMap[string, []string] {
	if m, ok := value.(*collections.OrderedMap[string, any]); ok {
		result := collections.NewOrderedMapWithSizeHint[string, []string](m.Size())
		for k, v := range m.Entries() {
			result.Set(k, ParseStringArray(v))
		}
		return result
	}
	return nil
}

func ParseString(value any) string {
	if str, ok := value.(string); ok {
		return str
	}
	return ""
}

func parseNumber(value any) *int {
	if num, ok := value.(int); ok {
		return &num
	}
	if num, ok := value.(float64); ok {
		n := int(num)
		return &n
	}
	return nil
}

func parseProjectReference(json any) []*core.ProjectReference {
	var result []*core.ProjectReference
	if v, ok := json.(*collections.OrderedMap[string, any]); ok {
		var reference core.ProjectReference
		if v, ok := v.Get("path"); ok {
			reference.Path = v.(string)
		}
		if v, ok := v.Get("circular"); ok {
			reference.Circular = v.(bool)
		}
		result = append(result, &reference)
	}
	return result
}

func parseJsonToStringKey(json any) *collections.OrderedMap[string, any] {
	result := collections.NewOrderedMapWithSizeHint[string, any](6)
	if m, ok := json.(*collections.OrderedMap[string, any]); ok {
		if v, ok := m.Get("include"); ok {
			result.Set("include", v)
		}
		if v, ok := m.Get("exclude"); ok {
			result.Set("exclude", v)
		}
		if v, ok := m.Get("files"); ok {
			result.Set("files", v)
		}
		if v, ok := m.Get("references"); ok {
			result.Set("references", v)
		}
		if v, ok := m.Get("extends"); ok {
			if str, ok := v.(string); ok {
				result.Set("extends", []any{str})
			}
			result.Set("extends", v)
		}
		if v, ok := m.Get("compilerOptions"); ok {
			result.Set("compilerOptions", v)
		}
		if v, ok := m.Get("excludes"); ok {
			result.Set("excludes", v)
		}
		if v, ok := m.Get("typeAcquisition"); ok {
			result.Set("typeAcquisition", v)
		}
	}
	return result
}

type optionParser interface {
	ParseOption(key string, value any) []*ast.Diagnostic
	UnknownOptionDiagnostic() *diagnostics.Message
	UnknownDidYouMeanDiagnostic() *diagnostics.Message
}

type compilerOptionsParser struct {
	*core.CompilerOptions
}

func (o *compilerOptionsParser) ParseOption(key string, value any) []*ast.Diagnostic {
	return ParseCompilerOptions(key, value, o.CompilerOptions)
}

func (o *compilerOptionsParser) UnknownOptionDiagnostic() *diagnostics.Message {
	return extraKeyDiagnostics("compilerOptions")
}

func (o *compilerOptionsParser) UnknownDidYouMeanDiagnostic() *diagnostics.Message {
	return extraKeyDidYouMeanDiagnostics("compilerOptions")
}

type watchOptionsParser struct {
	*core.WatchOptions
}

func (o *watchOptionsParser) ParseOption(key string, value any) []*ast.Diagnostic {
	return ParseWatchOptions(key, value, o.WatchOptions)
}

func (o *watchOptionsParser) UnknownOptionDiagnostic() *diagnostics.Message {
	return extraKeyDiagnostics("watchOptions")
}

func (o *watchOptionsParser) UnknownDidYouMeanDiagnostic() *diagnostics.Message {
	return extraKeyDidYouMeanDiagnostics("watchOptions")
}

type typeAcquisitionParser struct {
	*core.TypeAcquisition
}

func (o *typeAcquisitionParser) ParseOption(key string, value any) []*ast.Diagnostic {
	return ParseTypeAcquisition(key, value, o.TypeAcquisition)
}

func (o *typeAcquisitionParser) UnknownOptionDiagnostic() *diagnostics.Message {
	return extraKeyDiagnostics("typeAcquisition")
}

func (o *typeAcquisitionParser) UnknownDidYouMeanDiagnostic() *diagnostics.Message {
	return extraKeyDidYouMeanDiagnostics("typeAcquisition")
}

type buildOptionsParser struct {
	*core.BuildOptions
}

func (o *buildOptionsParser) ParseOption(key string, value any) []*ast.Diagnostic {
	return ParseBuildOptions(key, value, o.BuildOptions)
}

func (o *buildOptionsParser) UnknownOptionDiagnostic() *diagnostics.Message {
	return extraKeyDiagnostics("buildOptions")
}

func (o *buildOptionsParser) UnknownDidYouMeanDiagnostic() *diagnostics.Message {
	return extraKeyDidYouMeanDiagnostics("buildOptions")
}

func ParseCompilerOptions(key string, value any, allOptions *core.CompilerOptions) []*ast.Diagnostic {
	if value == nil {
		return nil
	}
	if allOptions == nil {
		return nil
	}
	parseCompilerOptions(key, value, allOptions)
	return nil
}

func parseCompilerOptions(key string, value any, allOptions *core.CompilerOptions) (foundKey bool) {
	option := CommandLineCompilerOptionsMap.Get(key)
	if option != nil {
		key = option.Name
	}
	switch key {
	case "allowJs":
		allOptions.AllowJs = ParseTristate(value)
	case "allowImportingTsExtensions":
		allOptions.AllowImportingTsExtensions = ParseTristate(value)
	case "allowSyntheticDefaultImports":
		allOptions.AllowSyntheticDefaultImports = ParseTristate(value)
	case "allowNonTsExtensions":
		allOptions.AllowNonTsExtensions = ParseTristate(value)
	case "allowUmdGlobalAccess":
		allOptions.AllowUmdGlobalAccess = ParseTristate(value)
	case "allowUnreachableCode":
		allOptions.AllowUnreachableCode = ParseTristate(value)
	case "allowUnusedLabels":
		allOptions.AllowUnusedLabels = ParseTristate(value)
	case "allowArbitraryExtensions":
		allOptions.AllowArbitraryExtensions = ParseTristate(value)
	case "alwaysStrict":
		allOptions.AlwaysStrict = ParseTristate(value)
	case "assumeChangesOnlyAffectDirectDependencies":
		allOptions.AssumeChangesOnlyAffectDirectDependencies = ParseTristate(value)
	case "baseUrl":
		allOptions.BaseUrl = ParseString(value)
	case "build":
		allOptions.Build = ParseTristate(value)
	case "checkJs":
		allOptions.CheckJs = ParseTristate(value)
	case "customConditions":
		allOptions.CustomConditions = ParseStringArray(value)
	case "composite":
		allOptions.Composite = ParseTristate(value)
	case "declarationDir":
		allOptions.DeclarationDir = ParseString(value)
	case "deduplicatePackages":
		allOptions.DeduplicatePackages = ParseTristate(value)
	case "diagnostics":
		allOptions.Diagnostics = ParseTristate(value)
	case "disableSizeLimit":
		allOptions.DisableSizeLimit = ParseTristate(value)
	case "disableSourceOfProjectReferenceRedirect":
		allOptions.DisableSourceOfProjectReferenceRedirect = ParseTristate(value)
	case "disableSolutionSearching":
		allOptions.DisableSolutionSearching = ParseTristate(value)
	case "disableReferencedProjectLoad":
		allOptions.DisableReferencedProjectLoad = ParseTristate(value)
	case "declarationMap":
		allOptions.DeclarationMap = ParseTristate(value)
	case "declaration":
		allOptions.Declaration = ParseTristate(value)
	case "downlevelIteration":
		allOptions.DownlevelIteration = ParseTristate(value)
	case "erasableSyntaxOnly":
		allOptions.ErasableSyntaxOnly = ParseTristate(value)
	case "emitDeclarationOnly":
		allOptions.EmitDeclarationOnly = ParseTristate(value)
	case "extendedDiagnostics":
		allOptions.ExtendedDiagnostics = ParseTristate(value)
	case "emitDecoratorMetadata":
		allOptions.EmitDecoratorMetadata = ParseTristate(value)
	case "emitBOM":
		allOptions.EmitBOM = ParseTristate(value)
	case "esModuleInterop":
		allOptions.ESModuleInterop = ParseTristate(value)
	case "exactOptionalPropertyTypes":
		allOptions.ExactOptionalPropertyTypes = ParseTristate(value)
	case "explainFiles":
		allOptions.ExplainFiles = ParseTristate(value)
	case "experimentalDecorators":
		allOptions.ExperimentalDecorators = ParseTristate(value)
	case "forceConsistentCasingInFileNames":
		allOptions.ForceConsistentCasingInFileNames = ParseTristate(value)
	case "generateCpuProfile":
		allOptions.GenerateCpuProfile = ParseString(value)
	case "generateTrace":
		allOptions.GenerateTrace = ParseString(value)
	case "isolatedModules":
		allOptions.IsolatedModules = ParseTristate(value)
	case "ignoreConfig":
		allOptions.IgnoreConfig = ParseTristate(value)
	case "ignoreDeprecations":
		allOptions.IgnoreDeprecations = ParseString(value)
	case "importHelpers":
		allOptions.ImportHelpers = ParseTristate(value)
	case "incremental":
		allOptions.Incremental = ParseTristate(value)
	case "init":
		allOptions.Init = ParseTristate(value)
	case "inlineSourceMap":
		allOptions.InlineSourceMap = ParseTristate(value)
	case "inlineSources":
		allOptions.InlineSources = ParseTristate(value)
	case "isolatedDeclarations":
		allOptions.IsolatedDeclarations = ParseTristate(value)
	case "jsx":
		allOptions.Jsx = floatOrInt32ToFlag[core.JsxEmit](value)
	case "jsxFactory":
		allOptions.JsxFactory = ParseString(value)
	case "jsxFragmentFactory":
		allOptions.JsxFragmentFactory = ParseString(value)
	case "jsxImportSource":
		allOptions.JsxImportSource = ParseString(value)
	case "lib":
		if _, ok := value.([]string); ok {
			allOptions.Lib = value.([]string)
		} else {
			allOptions.Lib = ParseStringArray(value)
		}
	case "libReplacement":
		allOptions.LibReplacement = ParseTristate(value)
	case "listEmittedFiles":
		allOptions.ListEmittedFiles = ParseTristate(value)
	case "listFiles":
		allOptions.ListFiles = ParseTristate(value)
	case "listFilesOnly":
		allOptions.ListFilesOnly = ParseTristate(value)
	case "locale":
		allOptions.Locale = ParseString(value)
	case "mapRoot":
		allOptions.MapRoot = ParseString(value)
	case "module":
		allOptions.Module = floatOrInt32ToFlag[core.ModuleKind](value)
	case "moduleDetectionKind":
		allOptions.ModuleDetection = floatOrInt32ToFlag[core.ModuleDetectionKind](value)
	case "moduleResolution":
		allOptions.ModuleResolution = floatOrInt32ToFlag[core.ModuleResolutionKind](value)
	case "moduleSuffixes":
		allOptions.ModuleSuffixes = ParseStringArray(value)
	case "moduleDetection":
		allOptions.ModuleDetection = floatOrInt32ToFlag[core.ModuleDetectionKind](value)
	case "noCheck":
		allOptions.NoCheck = ParseTristate(value)
	case "noFallthroughCasesInSwitch":
		allOptions.NoFallthroughCasesInSwitch = ParseTristate(value)
	case "noEmitForJsFiles":
		allOptions.NoEmitForJsFiles = ParseTristate(value)
	case "noErrorTruncation":
		allOptions.NoErrorTruncation = ParseTristate(value)
	case "noImplicitAny":
		allOptions.NoImplicitAny = ParseTristate(value)
	case "noImplicitThis":
		allOptions.NoImplicitThis = ParseTristate(value)
	case "noLib":
		allOptions.NoLib = ParseTristate(value)
	case "noPropertyAccessFromIndexSignature":
		allOptions.NoPropertyAccessFromIndexSignature = ParseTristate(value)
	case "noUncheckedIndexedAccess":
		allOptions.NoUncheckedIndexedAccess = ParseTristate(value)
	case "noEmitHelpers":
		allOptions.NoEmitHelpers = ParseTristate(value)
	case "noEmitOnError":
		allOptions.NoEmitOnError = ParseTristate(value)
	case "noImplicitReturns":
		allOptions.NoImplicitReturns = ParseTristate(value)
	case "noUnusedLocals":
		allOptions.NoUnusedLocals = ParseTristate(value)
	case "noUnusedParameters":
		allOptions.NoUnusedParameters = ParseTristate(value)
	case "noImplicitOverride":
		allOptions.NoImplicitOverride = ParseTristate(value)
	case "noUncheckedSideEffectImports":
		allOptions.NoUncheckedSideEffectImports = ParseTristate(value)
	case "outFile":
		allOptions.OutFile = ParseString(value)
	case "noResolve":
		allOptions.NoResolve = ParseTristate(value)
	case "paths":
		allOptions.Paths = parseStringMap(value)
	case "preserveWatchOutput":
		allOptions.PreserveWatchOutput = ParseTristate(value)
	case "preserveConstEnums":
		allOptions.PreserveConstEnums = ParseTristate(value)
	case "preserveSymlinks":
		allOptions.PreserveSymlinks = ParseTristate(value)
	case "project":
		allOptions.Project = ParseString(value)
	case "pretty":
		allOptions.Pretty = ParseTristate(value)
	case "resolveJsonModule":
		allOptions.ResolveJsonModule = ParseTristate(value)
	case "resolvePackageJsonExports":
		allOptions.ResolvePackageJsonExports = ParseTristate(value)
	case "resolvePackageJsonImports":
		allOptions.ResolvePackageJsonImports = ParseTristate(value)
	case "reactNamespace":
		allOptions.ReactNamespace = ParseString(value)
	case "rewriteRelativeImportExtensions":
		allOptions.RewriteRelativeImportExtensions = ParseTristate(value)
	case "rootDir":
		allOptions.RootDir = ParseString(value)
	case "rootDirs":
		allOptions.RootDirs = ParseStringArray(value)
	case "removeComments":
		allOptions.RemoveComments = ParseTristate(value)
	case "stableTypeOrdering":
		allOptions.StableTypeOrdering = ParseTristate(value)
	case "strict":
		allOptions.Strict = ParseTristate(value)
	case "strictBindCallApply":
		allOptions.StrictBindCallApply = ParseTristate(value)
	case "strictBuiltinIteratorReturn":
		allOptions.StrictBuiltinIteratorReturn = ParseTristate(value)
	case "strictFunctionTypes":
		allOptions.StrictFunctionTypes = ParseTristate(value)
	case "strictNullChecks":
		allOptions.StrictNullChecks = ParseTristate(value)
	case "strictPropertyInitialization":
		allOptions.StrictPropertyInitialization = ParseTristate(value)
	case "skipDefaultLibCheck":
		allOptions.SkipDefaultLibCheck = ParseTristate(value)
	case "sourceMap":
		allOptions.SourceMap = ParseTristate(value)
	case "sourceRoot":
		allOptions.SourceRoot = ParseString(value)
	case "stripInternal":
		allOptions.StripInternal = ParseTristate(value)
	case "suppressOutputPathCheck":
		allOptions.SuppressOutputPathCheck = ParseTristate(value)
	case "target":
		allOptions.Target = floatOrInt32ToFlag[core.ScriptTarget](value)
	case "traceResolution":
		allOptions.TraceResolution = ParseTristate(value)
	case "tsBuildInfoFile":
		allOptions.TsBuildInfoFile = ParseString(value)
	case "typeRoots":
		allOptions.TypeRoots = ParseStringArray(value)
	case "types":
		allOptions.Types = ParseStringArray(value)
	case "useDefineForClassFields":
		allOptions.UseDefineForClassFields = ParseTristate(value)
	case "useUnknownInCatchVariables":
		allOptions.UseUnknownInCatchVariables = ParseTristate(value)
	case "verbatimModuleSyntax":
		allOptions.VerbatimModuleSyntax = ParseTristate(value)
	case "version":
		allOptions.Version = ParseTristate(value)
	case "help":
		allOptions.Help = ParseTristate(value)
	case "all":
		allOptions.All = ParseTristate(value)
	case "maxNodeModuleJsDepth":
		allOptions.MaxNodeModuleJsDepth = parseNumber(value)
	case "skipLibCheck":
		allOptions.SkipLibCheck = ParseTristate(value)
	case "noEmit":
		allOptions.NoEmit = ParseTristate(value)
	case "showConfig":
		allOptions.ShowConfig = ParseTristate(value)
	case "configFilePath":
		allOptions.ConfigFilePath = ParseString(value)
	case "noDtsResolution":
		allOptions.NoDtsResolution = ParseTristate(value)
	case "pathsBasePath":
		allOptions.PathsBasePath = ParseString(value)
	case "outDir":
		allOptions.OutDir = ParseString(value)
	case "newLine":
		allOptions.NewLine = floatOrInt32ToFlag[core.NewLineKind](value)
	case "watch":
		allOptions.Watch = ParseTristate(value)
	case "pprofDir":
		allOptions.PprofDir = ParseString(value)
	case "singleThreaded":
		allOptions.SingleThreaded = ParseTristate(value)
	case "quiet":
		allOptions.Quiet = ParseTristate(value)
	case "checkers":
		allOptions.Checkers = parseNumber(value)
	default:
		// different than any key above
		return false
	}
	return true
}

func floatOrInt32ToFlag[T ~int32](value any) T {
	if v, ok := value.(T); ok {
		return v
	}
	return T(value.(float64))
}

func ParseWatchOptions(key string, value any, allOptions *core.WatchOptions) []*ast.Diagnostic {
	if allOptions == nil {
		return nil
	}
	switch key {
	case "watchInterval":
		allOptions.Interval = parseNumber(value)
	case "watchFile":
		if value != nil {
			allOptions.FileKind = value.(core.WatchFileKind)
		}
	case "watchDirectory":
		if value != nil {
			allOptions.DirectoryKind = value.(core.WatchDirectoryKind)
		}
	case "fallbackPolling":
		if value != nil {
			allOptions.FallbackPolling = value.(core.PollingKind)
		}
	case "synchronousWatchDirectory":
		allOptions.SyncWatchDir = ParseTristate(value)
	case "excludeDirectories":
		allOptions.ExcludeDir = ParseStringArray(value)
	case "excludeFiles":
		allOptions.ExcludeFiles = ParseStringArray(value)
	}
	return nil
}

func ParseTypeAcquisition(key string, value any, allOptions *core.TypeAcquisition) []*ast.Diagnostic {
	if value == nil {
		return nil
	}
	if allOptions == nil {
		return nil
	}
	switch key {
	case "enable":
		allOptions.Enable = ParseTristate(value)
	case "include":
		allOptions.Include = ParseStringArray(value)
	case "exclude":
		allOptions.Exclude = ParseStringArray(value)
	case "disableFilenameBasedTypeAcquisition":
		allOptions.DisableFilenameBasedTypeAcquisition = ParseTristate(value)
	}
	return nil
}

func ParseBuildOptions(key string, value any, allOptions *core.BuildOptions) []*ast.Diagnostic {
	if value == nil {
		return nil
	}
	if allOptions == nil {
		return nil
	}
	option := BuildNameMap.Get(key)
	if option != nil {
		key = option.Name
	}
	switch key {
	case "clean":
		allOptions.Clean = ParseTristate(value)
	case "dry":
		allOptions.Dry = ParseTristate(value)
	case "force":
		allOptions.Force = ParseTristate(value)
	case "builders":
		allOptions.Builders = parseNumber(value)
	case "stopBuildOnErrors":
		allOptions.StopBuildOnErrors = ParseTristate(value)
	case "verbose":
		allOptions.Verbose = ParseTristate(value)
	}
	return nil
}

// mergeCompilerOptions merges the source compiler options into the target compiler options
// with optional awareness of explicitly set null values in the raw JSON.
// Fields in the source options will overwrite the corresponding fields in the target options,
// including when they are explicitly set to null in the raw configuration (if rawSource is provided).
func mergeCompilerOptions(targetOptions, sourceOptions *core.CompilerOptions, rawSource any) *core.CompilerOptions {
	if sourceOptions == nil {
		return targetOptions
	}

	// Collect explicitly null field names from raw JSON
	var explicitNullFields collections.Set[string]
	if rawSource != nil {
		if rawMap, ok := rawSource.(*collections.OrderedMap[string, any]); ok && rawMap != nil {
			// Options are nested under "compilerOptions" in both tsconfig.json and wrapped command line options
			if compilerOptionsRaw, exists := rawMap.Get("compilerOptions"); exists {
				if compilerOptionsMap, ok := compilerOptionsRaw.(*collections.OrderedMap[string, any]); ok {
					for key, value := range compilerOptionsMap.Entries() {
						if value == nil {
							explicitNullFields.Add(key)
						}
					}
				}
			}
		}
	}

	// Do the merge, handling explicit nulls during the normal merge
	targetValue := reflect.ValueOf(targetOptions).Elem()
	sourceValue := reflect.ValueOf(sourceOptions).Elem()
	targetType := targetValue.Type()

	for i := range targetValue.NumField() {
		targetField := targetValue.Field(i)
		sourceField := sourceValue.Field(i)

		// Get the JSON field name for this struct field and check if it's explicitly null
		if jsonTag := targetType.Field(i).Tag.Get("json"); jsonTag != "" {
			if jsonFieldName, _, _ := strings.Cut(jsonTag, ","); jsonFieldName != "" && explicitNullFields.Has(jsonFieldName) {
				targetField.SetZero()
				continue
			}
		}

		// Normal merge behavior: copy non-zero fields
		if !sourceField.IsZero() {
			targetField.Set(sourceField)
		}
	}

	return targetOptions
}

func convertToOptionsWithAbsolutePaths(optionsBase *collections.OrderedMap[string, any], optionMap CommandLineOptionNameMap, cwd string) *collections.OrderedMap[string, any] {
	// !!! convert to options with absolute paths was previously done with `CompilerOptions` object, but for ease of implementation, we do it pre-conversion.
	// !!! Revisit this choice if/when refactoring when conversion is done in tsconfig parsing
	if optionsBase == nil {
		return nil
	}
	for o, v := range optionsBase.Entries() {
		result, ok := ConvertOptionToAbsolutePath(o, v, optionMap, cwd)
		if ok {
			optionsBase.Set(o, result)
		}
	}
	return optionsBase
}

func ConvertOptionToAbsolutePath(o string, v any, optionMap CommandLineOptionNameMap, cwd string) (any, bool) {
	option := optionMap.Get(o)
	if option == nil {
		return nil, false
	}
	if option.Kind == "list" {
		if option.Elements().IsFilePath {
			if arr, ok := v.([]string); ok {
				return core.Map(arr, func(item string) string {
					return tspath.GetNormalizedAbsolutePath(item, cwd)
				}), true
			}
			if arr, ok := v.([]any); ok {
				return core.Map(arr, func(item any) any {
					if s, isStr := item.(string); isStr {
						return tspath.GetNormalizedAbsolutePath(s, cwd)
					}
					return item
				}), true
			}
		}
	} else if option.IsFilePath {
		if value, ok := v.(string); ok {
			return tspath.GetNormalizedAbsolutePath(value, cwd), true
		}
	}
	return nil, false
}
