package fs

import (
	"bytes"
	"encoding/json"
	"sort"
	"sync"

	mapset "github.com/deckarep/golang-set"
	"github.com/vercel/turbo/cli/internal/lockfile"
	"github.com/vercel/turbo/cli/internal/turbopath"
)

// PackageJSON represents NodeJS package.json
type PackageJSON struct {
	Name                 string            `json:"name"`
	Version              string            `json:"version"`
	Scripts              map[string]string `json:"scripts"`
	Dependencies         map[string]string `json:"dependencies"`
	DevDependencies      map[string]string `json:"devDependencies"`
	OptionalDependencies map[string]string `json:"optionalDependencies"`
	PeerDependencies     map[string]string `json:"peerDependencies"`
	PackageManager       string            `json:"packageManager"`
	Os                   []string          `json:"os"`
	Workspaces           Workspaces        `json:"workspaces"`
	Private              bool              `json:"private"`
	// Exact JSON object stored in package.json including unknown fields
	// During marshalling struct fields will take priority over raw fields
	RawJSON map[string]interface{} `json:"-"`

	// relative path from repo root to the package.json file
	PackageJSONPath turbopath.AnchoredSystemPath `json:"-"`
	// relative path from repo root to the package
	Dir                    turbopath.AnchoredSystemPath `json:"-"`
	InternalDeps           []string                     `json:"-"`
	UnresolvedExternalDeps map[string]string            `json:"-"`
	TransitiveDeps         []lockfile.Package           `json:"-"`
	LegacyTurboConfig      *TurboJSON                   `json:"turbo"`
	Mu                     sync.Mutex                   `json:"-"`
	ExternalDepsHash       string                       `json:"-"`
}

type Workspaces []string

type WorkspacesAlt struct {
	Packages []string `json:"packages,omitempty"`
}

func (r *Workspaces) UnmarshalJSON(data []byte) error {
	var tmp = &WorkspacesAlt{}
	if err := json.Unmarshal(data, tmp); err == nil {
		*r = Workspaces(tmp.Packages)
		return nil
	}
	var tempstr = []string{}
	if err := json.Unmarshal(data, &tempstr); err != nil {
		return err
	}
	*r = tempstr
	return nil
}

// ReadPackageJSON returns a struct of package.json
func ReadPackageJSON(path turbopath.AbsoluteSystemPath) (*PackageJSON, error) {
	b, err := path.ReadFile()
	if err != nil {
		return nil, err
	}
	return UnmarshalPackageJSON(b)
}

// UnmarshalPackageJSON decodes a byte slice into a PackageJSON struct
func UnmarshalPackageJSON(data []byte) (*PackageJSON, error) {
	var rawJSON map[string]interface{}
	if err := json.Unmarshal(data, &rawJSON); err != nil {
		return nil, err
	}

	pkgJSON := &PackageJSON{}
	if err := json.Unmarshal(data, &pkgJSON); err != nil {
		return nil, err
	}
	pkgJSON.RawJSON = rawJSON

	return pkgJSON, nil
}

// MarshalPackageJSON Serialize PackageJSON to a slice of bytes
func MarshalPackageJSON(pkgJSON *PackageJSON) ([]byte, error) {
	structuredContent, err := json.Marshal(pkgJSON)
	if err != nil {
		return nil, err
	}
	var structuredFields map[string]interface{}
	if err := json.Unmarshal(structuredContent, &structuredFields); err != nil {
		return nil, err
	}

	fieldsToSerialize := make(map[string]interface{}, len(pkgJSON.RawJSON))

	// copy pkgJSON.RawJSON
	for key, value := range pkgJSON.RawJSON {
		fieldsToSerialize[key] = value
	}

	for key, value := range structuredFields {
		if isEmpty(value) {
			delete(fieldsToSerialize, key)
		} else {
			fieldsToSerialize[key] = value
		}
	}

	var b bytes.Buffer
	encoder := json.NewEncoder(&b)
	encoder.SetEscapeHTML(false)
	encoder.SetIndent("", "  ")
	if err := encoder.Encode(fieldsToSerialize); err != nil {
		return nil, err
	}

	return b.Bytes(), nil
}

// SetExternalDeps sets TransitiveDeps and populates ExternalDepsHash
func (p *PackageJSON) SetExternalDeps(externalDeps mapset.Set) error {
	p.Mu.Lock()
	defer p.Mu.Unlock()
	p.TransitiveDeps = make([]lockfile.Package, 0, externalDeps.Cardinality())
	for _, dependency := range externalDeps.ToSlice() {
		dependency := dependency.(lockfile.Package)
		p.TransitiveDeps = append(p.TransitiveDeps, dependency)
	}
	sort.Sort(lockfile.ByKey(p.TransitiveDeps))
	hashOfExternalDeps, err := HashLockfilePackages(p.TransitiveDeps)
	if err != nil {
		return err
	}
	p.ExternalDepsHash = hashOfExternalDeps
	return nil
}

func isEmpty(value interface{}) bool {
	if value == nil {
		return true
	}
	switch s := value.(type) {
	case string:
		return s == ""
	case bool:
		return !s
	case []string:
		return len(s) == 0
	case map[string]interface{}:
		return len(s) == 0
	case Workspaces:
		return len(s) == 0
	default:
		// Assume any unknown types aren't empty
		return false
	}
}
