package gradle

import (
	"encoding/xml"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"runtime"
	"strings"

	"golang.org/x/net/html/charset"
	"golang.org/x/xerrors"

	"github.com/aquasecurity/trivy/pkg/log"
	"github.com/aquasecurity/trivy/pkg/utils/fsutils"
)

type pomXML struct {
	GroupId      string       `xml:"groupId"`
	ArtifactId   string       `xml:"artifactId"`
	Version      string       `xml:"version"`
	Properties   Properties   `xml:"properties"`
	Dependencies Dependencies `xml:"dependencies"`
	Licenses     Licenses     `xml:"licenses"`
}
type Dependencies struct {
	Dependency []Dependency `xml:"dependency"`
}

type Dependency struct {
	GroupID    string `xml:"groupId"`
	ArtifactID string `xml:"artifactId"`
	Version    string `xml:"version"`
}

type Licenses struct {
	License []License `xml:"license"`
}

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

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 (a gradleLockAnalyzer) parsePoms() (map[string]pomXML, error) {
	cacheDir := a.detectCacheDir()
	// Cache dir is not found
	if cacheDir == "" {
		return nil, nil
	}

	required := func(path string, d fs.DirEntry) bool {
		return filepath.Ext(path) == ".pom"
	}

	var poms = make(map[string]pomXML)
	err := fsutils.WalkDir(os.DirFS(cacheDir), ".", required, func(path string, _ fs.DirEntry, r io.Reader) error {
		pom, err := parsePom(r, path)
		if err != nil {
			a.logger.Debug("Unable to parse pom", log.FilePath(path), log.Err(err))
			return nil
		}

		if pom.ArtifactId != "" {
			poms[packageID(pom.GroupId, pom.ArtifactId, pom.Version)] = pom
		}
		return nil
	})
	if err != nil {
		return nil, xerrors.Errorf("gradle licenses walk error: %w", err)
	}

	return poms, nil
}

func parsePom(r io.Reader, path string) (pomXML, error) {
	pom := pomXML{}
	decoder := xml.NewDecoder(r)
	decoder.CharsetReader = charset.NewReaderLabel
	if err := decoder.Decode(&pom); err != nil {
		return pomXML{}, xerrors.Errorf("xml decode error: %w", err)
	}

	// We only need pom's with licenses or dependencies
	if len(pom.Licenses.License) == 0 && len(pom.Dependencies.Dependency) == 0 {
		return pomXML{}, nil
	}

	// If pom file doesn't contain GroupID or Version:
	// find these values from filepath
	// e.g. caches/modules-2/files-2.1/com.google.code.gson/gson/2.9.1/f0cf3edcef8dcb74d27cb427544a309eb718d772/gson-2.9.1.pom
	dirs := strings.Split(filepath.ToSlash(path), "/")
	if pom.GroupId == "" {
		pom.GroupId = dirs[len(dirs)-5]
	}
	if pom.Version == "" {
		pom.Version = dirs[len(dirs)-3]
	}

	if err := pom.resolveDependencyVersions(); err != nil {
		return pomXML{}, xerrors.Errorf("unable to resolve dependency version: %w", err)
	}

	return pom, nil
}

// resolveDependencyVersions resolves versions from properties
func (pom *pomXML) resolveDependencyVersions() error {
	for i, dep := range pom.Dependencies.Dependency {
		if strings.HasPrefix(dep.Version, "${") && strings.HasSuffix(dep.Version, "}") {
			dep.Version = strings.TrimPrefix(strings.TrimSuffix(dep.Version, "}"), "${")
			if resolvedVer, ok := pom.Properties[dep.Version]; ok {
				pom.Dependencies.Dependency[i].Version = resolvedVer
			} else if dep.Version == "${project.version}" {
				pom.Dependencies.Dependency[i].Version = dep.Version
			} else {
				// We use simplified logic to resolve properties.
				// If necessary, update and use the logic for maven pom's
				return xerrors.Errorf("Unable to resolve %q version. Please open a new discussion to update the Trivy logic.", dep.Version)
			}
		}
	}
	return nil
}

func (a gradleLockAnalyzer) detectCacheDir() string {
	// https://docs.gradle.org/current/userguide/directory_layout.html
	dir := os.Getenv("GRADLE_USER_HOME")
	if dir == "" {
		if runtime.GOOS == "windows" {
			dir = filepath.Join(os.Getenv("%HOMEPATH%"), ".gradle")
		} else {
			dir = filepath.Join(os.Getenv("HOME"), ".gradle")
		}
	}
	dir = filepath.Join(dir, "caches")

	if !fsutils.DirExists(dir) {
		a.logger.Debug("Unable to get licenses and dependencies. Gradle cache dir doesn't exist.")
		return ""
	}
	return dir
}
