package parser

import (
	"encoding/json/jsontext"
	"encoding/json/v2"
	"fmt"
	"io/fs"
	"reflect"
	"strconv"
	"strings"

	"gopkg.in/yaml.v3"

	"github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes"
	iacTypes "github.com/aquasecurity/trivy/pkg/iac/types"
	xjson "github.com/aquasecurity/trivy/pkg/x/json"
)

type EqualityOptions = int

const (
	IgnoreCase EqualityOptions = iota
)

type Property struct {
	xjson.Location
	ctx         *FileContext
	Type        cftypes.CfType
	Value       any `json:"Value" yaml:"Value"`
	name        string
	comment     string
	rng         iacTypes.Range
	parentRange iacTypes.Range
	logicalId   string
	unresolved  bool

	loopCtx *LoopContext
}

func (p *Property) setName(name string) {
	p.name = name
	if p.Type == cftypes.Map {
		for n, subProp := range p.AsMap() {
			if subProp == nil {
				continue
			}
			subProp.setName(n)
		}
	}
}

func (p *Property) setContext(ctx *FileContext) {
	p.walk(func(prop *Property) bool {
		prop.ctx = ctx
		return true
	})
}

func (p *Property) setLogicalResource(id string) {
	p.walk(func(prop *Property) bool {
		prop.logicalId = id
		return !prop.isFunction()
	})
}

func (p *Property) setFileAndParentRange(target fs.FS, filepath string, parentRange iacTypes.Range) {
	p.rng = iacTypes.NewRange(filepath, p.StartLine, p.EndLine, p.rng.GetSourcePrefix(), target)
	p.parentRange = parentRange

	switch p.Type {
	case cftypes.Map:
		for _, subProp := range p.AsMap() {
			if subProp == nil {
				continue
			}
			subProp.setFileAndParentRange(target, filepath, p.rng)
		}
	case cftypes.List:
		for _, subProp := range p.AsList() {
			if subProp == nil {
				continue
			}
			subProp.setFileAndParentRange(target, filepath, p.rng)
		}
	}
}

func (p *Property) clone() *Property {
	if p == nil {
		return nil
	}

	clone := &Property{
		Location:    p.Location,
		ctx:         p.ctx,
		Type:        p.Type,
		name:        p.name,
		comment:     p.comment,
		rng:         p.rng,
		parentRange: p.parentRange,
		logicalId:   p.logicalId,
		unresolved:  p.unresolved,
	}

	switch v := p.Value.(type) {
	case map[string]*Property:
		m := make(map[string]*Property, len(v))
		for k, el := range v {
			m[k] = el.clone()
		}
		clone.Value = m
	case []*Property:
		slice := make([]*Property, len(v))
		for i, el := range v {
			slice[i] = el.clone()
		}
		clone.Value = slice
	default:
		clone.Value = v
	}

	return clone
}

func (p *Property) UnmarshalYAML(node *yaml.Node) error {
	p.StartLine = node.Line
	p.EndLine = calculateEndLine(node)
	p.comment = node.LineComment
	return setPropertyValueFromYaml(node, p)
}

func (p *Property) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
	var valPtr any
	var nodeType cftypes.CfType

	switch k := dec.PeekKind(); k {
	case 't', 'f':
		valPtr = new(bool)
		nodeType = cftypes.Bool
	case '"':
		valPtr = new(string)
		nodeType = cftypes.String
	case '0':
		return p.parseNumericValue(dec)
	case '[', 'n':
		valPtr = new([]*Property)
		nodeType = cftypes.List
	case '{':
		valPtr = new(map[string]*Property)
		nodeType = cftypes.Map
	case 0:
		return dec.SkipValue()
	default:
		return fmt.Errorf("unexpected token kind %q at %d", k.String(), dec.InputOffset())
	}

	if err := json.UnmarshalDecode(dec, valPtr); err != nil {
		return err
	}

	p.Value = reflect.ValueOf(valPtr).Elem().Interface()
	p.Type = nodeType
	return nil
}

func (p *Property) parseNumericValue(dec *jsontext.Decoder) error {
	raw, err := dec.ReadValue()
	if err != nil {
		return err
	}
	strVal := string(raw)

	if v, err := strconv.ParseInt(strVal, 10, 64); err == nil {
		p.Value = int(v)
		p.Type = cftypes.Int
		return nil
	}
	if v, err := strconv.ParseFloat(strVal, 64); err == nil {
		p.Value = v
		p.Type = cftypes.Float64
		return nil
	}
	return fmt.Errorf("invalid numeric value: %q", strVal)
}

func (p *Property) Metadata() iacTypes.Metadata {
	return iacTypes.NewMetadata(p.rng, p.name).
		WithParent(iacTypes.NewMetadata(p.parentRange, p.logicalId))
}

func (p *Property) isFunction() bool {
	if p == nil {
		return false
	}
	if p.Type == cftypes.Map {
		for n := range p.AsMap() {
			return IsIntrinsic(n)
		}
	}
	return false
}

func (p *Property) RawValue() any {
	return p.Value
}

func (p *Property) AsRawStrings() ([]string, error) {
	if len(p.ctx.lines) < p.rng.GetEndLine() {
		return p.ctx.lines, nil
	}
	return p.ctx.lines[p.rng.GetStartLine()-1 : p.rng.GetEndLine()], nil
}

func (p *Property) resolveValue() (*Property, bool) {
	if !p.isFunction() || p.IsUnresolved() {
		return p, true
	}

	resolved, ok := ResolveIntrinsicFunc(p)
	if ok {
		return resolved, true
	}

	p.unresolved = true
	return p, false
}

func (p *Property) GetStringProperty(path string, defaultValue ...string) iacTypes.StringValue {
	defVal := ""
	if len(defaultValue) > 0 {
		defVal = defaultValue[0]
	}

	if p.IsUnresolved() {
		return iacTypes.StringUnresolvable(p.Metadata())
	}

	prop := p.GetProperty(path)
	if prop.IsNotString() {
		return p.StringDefault(defVal)
	}
	return prop.AsStringValue()
}

func (p *Property) StringDefault(defaultValue string) iacTypes.StringValue {
	return iacTypes.StringDefault(defaultValue, p.Metadata())
}

func (p *Property) GetBoolProperty(path string, defaultValue ...bool) iacTypes.BoolValue {
	defVal := false
	if len(defaultValue) > 0 {
		defVal = defaultValue[0]
	}

	if p.IsUnresolved() {
		return iacTypes.BoolUnresolvable(p.Metadata())
	}

	prop := p.GetProperty(path)

	if prop.isFunction() {
		prop, _ = prop.resolveValue()
	}

	if prop.IsNotBool() {
		return p.inferBool(prop, defVal)
	}
	return prop.AsBoolValue()
}

func (p *Property) GetIntProperty(path string, defaultValue ...int) iacTypes.IntValue {
	defVal := 0
	if len(defaultValue) > 0 {
		defVal = defaultValue[0]
	}

	if p.IsUnresolved() {
		return iacTypes.IntUnresolvable(p.Metadata())
	}

	prop := p.GetProperty(path)

	if prop.IsNotInt() {
		return p.IntDefault(defVal)
	}
	return prop.AsIntValue()
}

func (p *Property) BoolDefault(defaultValue bool) iacTypes.BoolValue {
	return iacTypes.BoolDefault(defaultValue, p.Metadata())
}

func (p *Property) IntDefault(defaultValue int) iacTypes.IntValue {
	return iacTypes.IntDefault(defaultValue, p.Metadata())
}

func (p *Property) GetProperty(path string) *Property {
	pathParts := strings.Split(path, ".")

	first := pathParts[0]
	property := p

	if p.isFunction() {
		property, _ = p.resolveValue()
	}

	if property.IsNotMap() {
		return nil
	}

	for n, p := range property.AsMap() {
		if n == first {
			property = p
			break
		}
	}

	if len(pathParts) == 1 || property == nil {
		return property
	}

	if nestedProperty := property.GetProperty(strings.Join(pathParts[1:], ".")); nestedProperty != nil {
		if nestedProperty.isFunction() {
			resolved, _ := nestedProperty.resolveValue()
			return resolved
		}
		return nestedProperty
	}

	return &Property{}
}

func (p *Property) deriveResolved(propType cftypes.CfType, propValue any) *Property {
	clone := p.clone()
	clone.Type = propType
	clone.Value = propValue
	return clone
}

func (p *Property) ParentRange() iacTypes.Range {
	return p.parentRange
}

func (p *Property) inferBool(prop *Property, defaultValue bool) iacTypes.BoolValue {
	if prop.IsString() {
		if prop.EqualTo("true", IgnoreCase) {
			return iacTypes.Bool(true, prop.Metadata())
		}
		if prop.EqualTo("yes", IgnoreCase) {
			return iacTypes.Bool(true, prop.Metadata())
		}
		if prop.EqualTo("1", IgnoreCase) {
			return iacTypes.Bool(true, prop.Metadata())
		}
		if prop.EqualTo("false", IgnoreCase) {
			return iacTypes.Bool(false, prop.Metadata())
		}
		if prop.EqualTo("no", IgnoreCase) {
			return iacTypes.Bool(false, prop.Metadata())
		}
		if prop.EqualTo("0", IgnoreCase) {
			return iacTypes.Bool(false, prop.Metadata())
		}
	}

	if prop.IsInt() {
		if prop.EqualTo(0) {
			return iacTypes.Bool(false, prop.Metadata())
		}
		if prop.EqualTo(1) {
			return iacTypes.Bool(true, prop.Metadata())
		}
	}

	return p.BoolDefault(defaultValue)
}

func (p *Property) String() string {
	r := ""
	switch p.Type {
	case cftypes.String:
		r = p.AsString()
	case cftypes.Int:
		r = strconv.Itoa(p.AsInt())
	}
	return r
}

func (p *Property) GetJsonBytes(squashList ...bool) []byte {
	if p.IsNil() {
		return []byte{}
	}
	lines, err := p.AsRawStrings()
	if err != nil {
		return nil
	}
	if p.ctx.SourceFormat == JsonSourceFormat {
		return []byte(strings.Join(lines, " "))
	}

	if len(squashList) > 0 {
		lines[0] = strings.Replace(lines[0], "-", " ", 1)
	}

	lines = removeLeftMargin(lines)

	yamlContent := strings.Join(lines, "\n")
	var body any
	if err := yaml.Unmarshal([]byte(yamlContent), &body); err != nil {
		return nil
	}
	jsonBody := convert(body)
	policyJson, err := json.Marshal(jsonBody)
	if err != nil {
		return nil
	}
	return policyJson
}

func (p *Property) GetJsonBytesAsString(squashList ...bool) string {
	return string(p.GetJsonBytes(squashList...))
}

func removeLeftMargin(lines []string) []string {
	if len(lines) == 0 {
		return lines
	}
	prefixSpace := len(lines[0]) - len(strings.TrimLeft(lines[0], " "))

	for i, line := range lines {
		if len(line) >= prefixSpace {
			lines[i] = line[prefixSpace:]
		}
	}
	return lines
}

func convert(input any) any {
	switch x := input.(type) {
	case map[any]any:
		outpMap := make(map[string]any)
		for k, v := range x {
			outpMap[k.(string)] = convert(v)
		}
		return outpMap
	case []any:
		for i, v := range x {
			x[i] = convert(v)
		}
	}
	return input
}

func (p *Property) inferType() {
	typ := cftypes.TypeFromGoValue(p.Value)
	if typ == cftypes.Unknown {
		return
	}
	p.Type = typ
}

func (p *Property) walk(fn func(*Property) bool) {
	if fn == nil {
		return
	}

	if !fn(p) {
		return
	}

	switch v := p.Value.(type) {
	case map[string]*Property:
		for _, child := range v {
			if child != nil {
				child.walk(fn)
			}
		}
	case []*Property:
		for _, child := range v {
			if child != nil {
				child.walk(fn)
			}
		}
	}
}
