package pom

import (
	"encoding/xml"
	"fmt"
	"io"
	"reflect"
	"strings"

	"github.com/samber/lo"
	"golang.org/x/xerrors"

	"github.com/aquasecurity/trivy/pkg/dependency/parser/utils"
	ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
	"github.com/aquasecurity/trivy/pkg/set"
	"github.com/aquasecurity/trivy/pkg/x/slices"
)

type pom struct {
	filePath string
	content  *pomXML
}

func (p *pom) nil() bool {
	return p == nil || p.content == nil
}

func (p *pom) inherit(parent *pom) {
	if parent == nil {
		return
	}
	// Merge properties
	p.content.Properties = utils.MergeMaps(parent.properties(), p.content.Properties)

	art := p.artifact().Inherit(parent.artifact())

	p.content.GroupId = art.GroupID
	p.content.ArtifactId = art.ArtifactID
	p.content.Licenses = art.ToPOMLicenses()

	if isProperty(art.Version.String()) {
		p.content.Version = evaluateVariable(art.Version.String(), p.content.Properties, nil)
	} else {
		p.content.Version = art.Version.String()
	}
}

func (p *pom) properties() properties {
	props := p.content.Properties
	return utils.MergeMaps(props, p.projectProperties())
}

func (p *pom) projectProperties() map[string]string {
	val := reflect.ValueOf(p.content).Elem()
	props := p.listProperties(val)

	// "version" and "groupId" elements could be inherited from parent.
	// https://maven.apache.org/pom.html#inheritance
	props["groupId"] = p.content.GroupId
	props["version"] = p.content.Version

	// https://maven.apache.org/pom.html#properties
	projectProperties := make(map[string]string)
	for k, v := range props {
		if strings.HasPrefix(k, "project.") {
			continue
		}

		// e.g. ${project.groupId}
		key := fmt.Sprintf("project.%s", k)
		projectProperties[key] = v

		// It is deprecated, but still available.
		// e.g. ${groupId}
		projectProperties[k] = v
	}

	return projectProperties
}

func (p *pom) listProperties(val reflect.Value) map[string]string {
	props := make(map[string]string)
	for i := 0; i < val.NumField(); i++ {
		f := val.Type().Field(i)

		tag, ok := f.Tag.Lookup("xml")
		if !ok || strings.Contains(tag, ",") {
			// e.g. ",chardata"
			continue
		}

		switch f.Type.Kind() {
		case reflect.Slice:
			continue
		case reflect.Map:
			m := val.Field(i)
			for _, e := range m.MapKeys() {
				v := m.MapIndex(e)
				props[e.String()] = v.String()
			}
		case reflect.Struct:
			nestedProps := p.listProperties(val.Field(i))
			for k, v := range nestedProps {
				key := fmt.Sprintf("%s.%s", tag, k)
				props[key] = v
			}
		default:
			props[tag] = val.Field(i).String()
		}
	}
	return props
}

func (p *pom) artifact() artifact {
	return newArtifact(p.content.GroupId, p.content.ArtifactId, p.content.Version, p.licenses(), p.content.Properties)
}

func (p *pom) licenses() []string {
	return slices.ZeroToNil(lo.FilterMap(p.content.Licenses.License, func(lic pomLicense, _ int) (string, bool) {
		return lic.Name, lic.Name != ""
	}))
}

func (p *pom) repositories(servers []Server) []repository {
	return resolvePomRepos(servers, p.content.Repositories)
}

type pomXML struct {
	Parent     pomParent   `xml:"parent"`
	GroupId    string      `xml:"groupId"`
	ArtifactId string      `xml:"artifactId"`
	Version    string      `xml:"version"`
	Licenses   pomLicenses `xml:"licenses"`
	Modules    struct {
		Text   string   `xml:",chardata"`
		Module []string `xml:"module"`
	} `xml:"modules"`
	Properties           properties `xml:"properties"`
	DependencyManagement struct {
		Text         string          `xml:",chardata"`
		Dependencies pomDependencies `xml:"dependencies"`
	} `xml:"dependencyManagement"`
	Dependencies pomDependencies `xml:"dependencies"`
	Repositories []pomRepository `xml:"repositories>repository"`
}

type pomParent struct {
	GroupId      string `xml:"groupId"`
	ArtifactId   string `xml:"artifactId"`
	Version      string `xml:"version"`
	RelativePath string `xml:"relativePath"`
}

type pomLicenses struct {
	Text    string       `xml:",chardata"`
	License []pomLicense `xml:"license"`
}

type pomLicense struct {
	Name string `xml:"name"`
}

type pomDependencies struct {
	Text       string          `xml:",chardata"`
	Dependency []pomDependency `xml:"dependency"`
}

type pomDependency struct {
	Text       string        `xml:",chardata"`
	GroupID    string        `xml:"groupId"`
	ArtifactID string        `xml:"artifactId"`
	Version    string        `xml:"version"`
	Scope      string        `xml:"scope"`
	Optional   bool          `xml:"optional"`
	Exclusions pomExclusions `xml:"exclusions"`
	StartLine  int
	EndLine    int
}

type pomExclusions struct {
	Text      string         `xml:",chardata"`
	Exclusion []pomExclusion `xml:"exclusion"`
}

// ref. https://maven.apache.org/guides/introduction/introduction-to-optional-and-excludes-dependencies.html
type pomExclusion struct {
	GroupID    string `xml:"groupId"`
	ArtifactID string `xml:"artifactId"`
}

func (d pomDependency) Name() string {
	return fmt.Sprintf("%s:%s", d.GroupID, d.ArtifactID)
}

// Resolve evaluates variables in the dependency and inherit some fields from dependencyManagement to the dependency.
func (d pomDependency) Resolve(props map[string]string, depManagement, rootDepManagement []pomDependency) pomDependency {
	// Evaluate variables
	dep := pomDependency{
		Text:       d.Text,
		GroupID:    evaluateVariable(d.GroupID, props, nil),
		ArtifactID: evaluateVariable(d.ArtifactID, props, nil),
		Version:    evaluateVariable(d.Version, props, nil),
		Scope:      evaluateVariable(d.Scope, props, nil),
		Optional:   d.Optional,
		Exclusions: d.Exclusions,
		StartLine:  d.StartLine,
		EndLine:    d.EndLine,
	}

	// Field resolution order:
	// 1. Fill empty fields with values from dependencyManagement.
	// 2. Overwrite non-empty fields with values from the root dependencyManagement.
	//    2.1. Exception: if a dependency has the `test` scope (either defined explicitly
	//         or inherited from dependencyManagement), we must NOT overwrite it
	//         with the value from the root dependencyManagement.
	//
	// See the test "don't overwrite test scope from upper depManagement" for details.

	// Inherit version, scope and optional from dependencyManagement if empty
	if managed, found := findDep(dep.Name(), depManagement); found { // dependencyManagement from parent
		if dep.Version == "" {
			dep.Version = evaluateVariable(managed.Version, props, nil)
		}
		if dep.Scope == "" {
			dep.Scope = evaluateVariable(managed.Scope, props, nil)
		}
		// TODO: need to check the behavior
		if !dep.Optional {
			dep.Optional = managed.Optional
		}
		// `mvn` always merges exceptions for pom and parent
		dep.Exclusions.Exclusion = append(dep.Exclusions.Exclusion, managed.Exclusions.Exclusion...)
	}

	// If this dependency is managed in the root POM,
	// we need to overwrite fields according to the managed dependency.
	if managed, found := findDep(dep.Name(), rootDepManagement); found { // dependencyManagement from the root POM
		if managed.Version != "" {
			dep.Version = evaluateVariable(managed.Version, props, nil)
		}

		if managed.Scope != "" && dep.Scope != "test" {
			dep.Scope = evaluateVariable(managed.Scope, props, nil)
		}

		if managed.Optional {
			dep.Optional = managed.Optional
		}
		if len(managed.Exclusions.Exclusion) != 0 {
			dep.Exclusions = managed.Exclusions
		}
		return dep
	}
	return dep
}

// ToArtifact converts dependency to artifact.
// It should be called after calling Resolve() so that variables can be evaluated.
func (d pomDependency) ToArtifact(opts analysisOptions) artifact {
	// To avoid shadow adding exclusions to top pom's,
	// we need to initialize a new map for each new artifact
	// See `exclusions in child` test for more information
	exclusions := set.New[string]()
	if opts.exclusions != nil {
		exclusions = opts.exclusions.Clone()
	}
	for _, e := range d.Exclusions.Exclusion {
		exclusions.Append(fmt.Sprintf("%s:%s", e.GroupID, e.ArtifactID))
	}

	var locations ftypes.Locations
	if d.StartLine != 0 && d.EndLine != 0 {
		locations = ftypes.Locations{
			{
				StartLine: d.StartLine,
				EndLine:   d.EndLine,
			},
		}
	}

	return artifact{
		GroupID:      d.GroupID,
		ArtifactID:   d.ArtifactID,
		Version:      newVersion(d.Version),
		Exclusions:   exclusions,
		Locations:    locations,
		Relationship: ftypes.RelationshipIndirect, // default
	}
}

type properties map[string]string

type property struct {
	XMLName xml.Name
	Value   string `xml:",chardata"`
}

func (props *properties) UnmarshalXML(d *xml.Decoder, _ xml.StartElement) error {
	*props = properties{}
	for {
		var p property
		err := d.Decode(&p)
		if err == io.EOF {
			break
		} else if err != nil {
			return xerrors.Errorf("XML decode error: %w", err)
		}

		(*props)[p.XMLName.Local] = p.Value
	}
	return nil
}

func (deps *pomDependencies) UnmarshalXML(d *xml.Decoder, _ xml.StartElement) error {
	for {
		token, err := d.Token()
		if err == io.EOF {
			break
		} else if err != nil {
			return xerrors.Errorf("XML decode error: %w", err)
		}

		t, ok := token.(xml.StartElement)
		if !ok {
			continue
		}

		if t.Name.Local == "dependency" {
			var dep pomDependency
			dep.StartLine, _ = d.InputPos() // <dependency> tag starts

			// Decode the <dependency> element
			err = d.DecodeElement(&dep, &t)
			if err != nil {
				return xerrors.Errorf("Error decoding dependency: %w", err)
			}

			dep.EndLine, _ = d.InputPos() // <dependency> tag ends
			deps.Dependency = append(deps.Dependency, dep)
		}
	}
	return nil
}

func findDep(name string, depManagement []pomDependency) (pomDependency, bool) {
	return lo.Find(depManagement, func(item pomDependency) bool {
		return item.Name() == name
	})
}

type pomRepository struct {
	ID               string `xml:"id"`
	Name             string `xml:"name"`
	URL              string `xml:"url"`
	ReleasesEnabled  string `xml:"releases>enabled"`
	SnapshotsEnabled string `xml:"snapshots>enabled"`
}
