package packagejson

import (
	"sync"

	"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/semver"
	"github.com/microsoft/typescript-go/internal/tspath"
)

var typeScriptVersion = semver.MustParse(core.Version())

type PackageJson struct {
	Fields
	Parseable     bool
	versionPaths  VersionPaths
	versionTraces []diagnosticAndArgs
	once          sync.Once
}

type diagnosticAndArgs struct {
	message *diagnostics.Message
	args    []any
}

func (p *PackageJson) GetVersionPaths(trace func(m *diagnostics.Message, args ...any)) VersionPaths {
	p.once.Do(func() {
		if p.Fields.TypesVersions.Type == JSONValueTypeNotPresent {
			p.versionTraces = append(p.versionTraces, diagnosticAndArgs{
				diagnostics.X_package_json_does_not_have_a_0_field,
				[]any{"typesVersions"},
			})
			return
		}
		if p.Fields.TypesVersions.Type != JSONValueTypeObject {
			p.versionTraces = append(p.versionTraces, diagnosticAndArgs{
				diagnostics.Expected_type_of_0_field_in_package_json_to_be_1_got_2,
				[]any{"typesVersions", "object", p.Fields.TypesVersions.Type.String()},
			})
			return
		}

		p.versionTraces = append(p.versionTraces, diagnosticAndArgs{
			diagnostics.X_package_json_has_a_typesVersions_field_with_version_specific_path_mappings,
			[]any{"typesVersions"},
		})

		for key, value := range p.Fields.TypesVersions.AsObject().Entries() {
			keyRange, ok := semver.TryParseVersionRange(key)
			if !ok {
				p.versionTraces = append(p.versionTraces, diagnosticAndArgs{
					diagnostics.X_package_json_has_a_typesVersions_entry_0_that_is_not_a_valid_semver_range,
					[]any{key},
				})
				continue
			}
			if keyRange.Test(&typeScriptVersion) {
				if value.Type != JSONValueTypeObject {
					p.versionTraces = append(p.versionTraces, diagnosticAndArgs{
						diagnostics.Expected_type_of_0_field_in_package_json_to_be_1_got_2,
						[]any{"typesVersions['" + key + "']", "object", value.Type.String()},
					})
					return
				}
				p.versionPaths = VersionPaths{
					Version:   key,
					pathsJSON: value.AsObject(),
				}
				return
			}
		}

		p.versionTraces = append(p.versionTraces, diagnosticAndArgs{
			diagnostics.X_package_json_does_not_have_a_typesVersions_entry_that_matches_version_0,
			[]any{core.VersionMajorMinor()},
		})
	})
	if trace != nil {
		for _, msg := range p.versionTraces {
			trace(msg.message, msg.args...)
		}
	}
	return p.versionPaths
}

type VersionPaths struct {
	Version   string
	pathsJSON *collections.OrderedMap[string, JSONValue]
	paths     *collections.OrderedMap[string, []string]
}

func (v *VersionPaths) Exists() bool {
	return v != nil && v.Version != "" && v.pathsJSON != nil
}

func (v *VersionPaths) GetPaths() *collections.OrderedMap[string, []string] {
	if !v.Exists() {
		return nil
	}
	if v.paths != nil {
		return v.paths
	}
	paths := collections.NewOrderedMapWithSizeHint[string, []string](v.pathsJSON.Size())
	for key, value := range v.pathsJSON.Entries() {
		if value.Type != JSONValueTypeArray {
			continue
		}
		slice := make([]string, len(value.AsArray()))
		for i, path := range value.AsArray() {
			if path.Type != JSONValueTypeString {
				continue
			}
			slice[i] = path.Value.(string)
		}
		paths.Set(key, slice)
	}
	v.paths = paths
	return v.paths
}

type InfoCacheEntry struct {
	PackageDirectory string
	DirectoryExists  bool
	Contents         *PackageJson
}

func (p *InfoCacheEntry) Exists() bool {
	return p != nil && p.Contents != nil
}

func (p *InfoCacheEntry) GetContents() *PackageJson {
	if p == nil || p.Contents == nil {
		return nil
	}
	return p.Contents
}

func (p *InfoCacheEntry) GetDirectory() string {
	if p == nil {
		return ""
	}
	return p.PackageDirectory
}

// WithPackageDirectory returns an entry whose PackageDirectory matches the
// caller's value. The package.json info cache is keyed by the canonical
// path of the package.json file, but multiple callers may look up the same
// package.json using directory paths that differ only by a trailing
// separator (e.g. "node_modules/preact/compat" vs
// "node_modules/preact/compat/"). Because the cache uses first-writer-wins
// semantics, a later caller may receive an entry whose PackageDirectory
// doesn't match its own candidate path. Downstream code compares the
// candidate against PackageDirectory, so we must return a corrected
// shallow copy when they diverge.
// See https://github.com/microsoft/TypeScript/pull/50740.
func (p *InfoCacheEntry) WithPackageDirectory(packageDirectory string) *InfoCacheEntry {
	if p.PackageDirectory == packageDirectory {
		return p
	}
	return &InfoCacheEntry{
		PackageDirectory: packageDirectory,
		DirectoryExists:  p.DirectoryExists,
		Contents:         p.Contents,
	}
}

type InfoCache struct {
	cache                     collections.SyncMap[tspath.Path, *InfoCacheEntry]
	currentDirectory          string
	useCaseSensitiveFileNames bool
}

func NewInfoCache(currentDirectory string, useCaseSensitiveFileNames bool) *InfoCache {
	return &InfoCache{
		currentDirectory:          currentDirectory,
		useCaseSensitiveFileNames: useCaseSensitiveFileNames,
	}
}

func (p *InfoCache) Get(packageJsonPath string) *InfoCacheEntry {
	key := tspath.ToPath(packageJsonPath, p.currentDirectory, p.useCaseSensitiveFileNames)
	if value, ok := p.cache.Load(key); ok {
		return value
	}
	return nil
}

func (p *InfoCache) Set(packageJsonPath string, info *InfoCacheEntry) *InfoCacheEntry {
	key := tspath.ToPath(packageJsonPath, p.currentDirectory, p.useCaseSensitiveFileNames)
	actual, _ := p.cache.LoadOrStore(key, info)
	return actual
}

func (p *InfoCache) Range(f func(key tspath.Path, value *InfoCacheEntry) bool) {
	p.cache.Range(f)
}
