package java

import (
	"context"
	"errors"
	"strings"

	"github.com/anchore/syft/internal"
	"github.com/anchore/syft/internal/log"
	"github.com/anchore/syft/internal/unknown"
	"github.com/anchore/syft/syft/artifact"
	"github.com/anchore/syft/syft/file"
	"github.com/anchore/syft/syft/pkg"
	"github.com/anchore/syft/syft/pkg/cataloger/internal/licenses"
	"github.com/anchore/syft/syft/pkg/cataloger/java/internal/maven"
)

const (
	pomXMLGlob       = "**/*pom.xml"
	pomCatalogerName = "java-pom-cataloger"
)

type pomXMLCataloger struct {
	cfg ArchiveCatalogerConfig
}

func (p pomXMLCataloger) Name() string {
	return pomCatalogerName
}

func (p pomXMLCataloger) Catalog(ctx context.Context, fileResolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
	locations, err := fileResolver.FilesByGlob("**/pom.xml")
	if err != nil {
		return nil, nil, err
	}

	r := maven.NewResolver(fileResolver, p.cfg.mavenConfig())

	var errs error
	var poms []*maven.Project
	pomLocations := map[*maven.Project]file.Location{}
	for _, pomLocation := range locations {
		pom, err := readPomFromLocation(fileResolver, pomLocation)
		if err != nil || pom == nil {
			log.WithFields("error", err, "pomLocation", pomLocation).Debug("error while reading pom")
			errs = unknown.Appendf(errs, pomLocation, "error reading pom.xml: %w", err)
			continue
		}

		poms = append(poms, pom)
		pomLocations[pom] = pomLocation
		r.AddPom(ctx, pom, pomLocation)
	}

	var pkgs []pkg.Package
	var relationships []artifact.Relationship
	resolved := map[maven.ID]*pkg.Package{}

	// catalog all the main packages first so these can be referenced later when building the dependency graph
	for _, pom := range poms {
		location := pomLocations[pom] // should always exist

		id := r.ResolveID(ctx, pom)
		mainPkg := newPackageFromMavenPom(ctx, r, pom, location)
		if mainPkg == nil {
			continue
		}
		resolved[id] = mainPkg
		pkgs = append(pkgs, licenses.RelativeToPackage(ctx, fileResolver, *mainPkg))
	}

	// catalog all dependencies
	for _, pom := range poms {
		location := pomLocations[pom] // should always exist

		id := r.ResolveID(ctx, pom)
		mainPkg := resolved[id]

		newPkgs, newRelationships, newErrs := collectDependencies(ctx, r, resolved, mainPkg, pom, location, p.cfg.ResolveTransitiveDependencies)
		pkgs = append(pkgs, newPkgs...)
		relationships = append(relationships, newRelationships...)
		errs = unknown.Join(errs, newErrs)
	}

	return pkgs, relationships, errs
}

func readPomFromLocation(fileResolver file.Resolver, pomLocation file.Location) (*maven.Project, error) {
	contents, err := fileResolver.FileContentsByLocation(pomLocation)
	if err != nil {
		return nil, err
	}
	defer internal.CloseAndLogError(contents, pomLocation.RealPath)
	return maven.ParsePomXML(contents)
}

// newPackageFromMavenPom processes a single Maven POM for a given parent package, returning only the main package from the pom
func newPackageFromMavenPom(ctx context.Context, r *maven.Resolver, pom *maven.Project, location file.Location) *pkg.Package {
	id := r.ResolveID(ctx, pom)
	parent, err := r.ResolveParent(ctx, pom)
	if err != nil {
		// this is expected in many cases, there will be no network access and the maven resolver is unable to
		// look up information, so we can continue with what little information we have
		log.Tracef("unable to resolve parent due to: %v", err)
	}

	var javaPomParent *pkg.JavaPomParent
	if parent != nil { // parent is returned in both cases: when it is resolved or synthesized from the pom.parent info
		parentID := r.ResolveID(ctx, parent)
		javaPomParent = &pkg.JavaPomParent{
			GroupID:    parentID.GroupID,
			ArtifactID: parentID.ArtifactID,
			Version:    parentID.Version,
		}
	}

	pomLicenses, err := r.ResolveLicenses(ctx, pom)
	if err != nil {
		log.Tracef("error resolving licenses: %v", err)
	}
	pkgLicenses := toPkgLicenses(ctx, &location, pomLicenses)

	m := pkg.JavaArchive{
		PomProject: &pkg.JavaPomProject{
			Parent:      javaPomParent,
			GroupID:     id.GroupID,
			ArtifactID:  id.ArtifactID,
			Version:     id.Version,
			Name:        r.ResolveProperty(ctx, pom, pom.Name),
			Description: r.ResolveProperty(ctx, pom, pom.Description),
			URL:         r.ResolveProperty(ctx, pom, pom.URL),
		},
	}

	p := &pkg.Package{
		Name:    id.ArtifactID,
		Version: id.Version,
		Locations: file.NewLocationSet(
			location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
		),
		Licenses: pkg.NewLicenseSet(pkgLicenses...),
		Language: pkg.Java,
		Type:     pkg.JavaPkg,
		FoundBy:  pomCatalogerName,
		PURL:     packageURL(id.ArtifactID, id.Version, m),
		Metadata: m,
	}

	finalizePackage(p)

	return p
}

func collectDependencies(ctx context.Context, r *maven.Resolver, resolved map[maven.ID]*pkg.Package, parentPkg *pkg.Package, pom *maven.Project, loc file.Location, includeTransitiveDependencies bool) ([]pkg.Package, []artifact.Relationship, error) {
	var errs error
	var pkgs []pkg.Package
	var relationships []artifact.Relationship

	pomID := r.ResolveID(ctx, pom)
	for _, dep := range maven.DirectPomDependencies(pom) {
		depID := r.ResolveDependencyID(ctx, pom, dep)
		log.WithFields("pomLocation", loc, "mavenID", pomID, "dependencyID", depID).Trace("adding maven pom dependency")

		// we may have a reference to a package pointing to an existing pom on the filesystem, but we don't want to duplicate these entries
		depPkg := resolved[depID]
		if depPkg == nil {
			p, err := newPackageFromDependency(
				ctx,
				r,
				pom,
				dep,
				loc.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
			)
			if err != nil {
				log.WithFields("error", err, "pomLocation", loc, "mavenID", pomID, "dependencyID", depID).Debugf("error adding dependency")
			}

			if p == nil {
				// we don't have a valid package, just continue to the next dependency
				continue
			}
			depPkg = p
			resolved[depID] = depPkg

			// only resolve transitive dependencies if we're not already looking these up for the specific package
			if includeTransitiveDependencies && depID.Valid() {
				depPom, err := r.FindPom(ctx, depID.GroupID, depID.ArtifactID, depID.Version)
				if err != nil {
					log.WithFields("mavenID", depID, "error", err).Debug("error finding pom")
				}
				if depPom != nil {
					transitivePkgs, transitiveRelationships, transitiveErrs := collectDependencies(ctx, r, resolved, depPkg, depPom, loc, includeTransitiveDependencies)
					pkgs = append(pkgs, transitivePkgs...)
					relationships = append(relationships, transitiveRelationships...)
					errs = unknown.Join(errs, transitiveErrs)
				}
			}
		}

		pkgs = append(pkgs, *depPkg)
		if parentPkg != nil {
			relationships = append(relationships, artifact.Relationship{
				From: *depPkg,
				To:   *parentPkg,
				Type: artifact.DependencyOfRelationship,
			})
		}
	}

	return pkgs, relationships, errs
}

func newPomProject(ctx context.Context, r *maven.Resolver, path string, pom *maven.Project) *pkg.JavaPomProject {
	id := r.ResolveID(ctx, pom)
	name := r.ResolveProperty(ctx, pom, pom.Name)
	projectURL := r.ResolveProperty(ctx, pom, pom.URL)

	log.WithFields("path", path, "artifactID", id.ArtifactID, "name", name, "projectURL", projectURL).Trace("parsing pom.xml")
	return &pkg.JavaPomProject{
		Path:        path,
		Parent:      pomParent(ctx, r, pom),
		GroupID:     id.GroupID,
		ArtifactID:  id.ArtifactID,
		Version:     id.Version,
		Name:        name,
		Description: cleanDescription(r.ResolveProperty(ctx, pom, pom.Description)),
		URL:         projectURL,
	}
}

func newPackageFromDependency(ctx context.Context, r *maven.Resolver, pom *maven.Project, dep maven.Dependency, locations ...file.Location) (*pkg.Package, error) {
	id := r.ResolveDependencyID(ctx, pom, dep)

	var err error
	var pkgLicenses []pkg.License
	dependencyPom, depErr := r.FindPom(ctx, id.GroupID, id.ArtifactID, id.Version)
	if depErr != nil {
		err = errors.Join(err, depErr)
	}

	var pomProject *pkg.JavaPomProject
	if dependencyPom != nil {
		depLicenses, _ := r.ResolveLicenses(ctx, dependencyPom)
		pkgLicenses = append(pkgLicenses, toPkgLicenses(ctx, nil, depLicenses)...)
		pomProject = &pkg.JavaPomProject{
			Parent:      pomParent(ctx, r, dependencyPom),
			GroupID:     id.GroupID,
			ArtifactID:  id.ArtifactID,
			Version:     id.Version,
			Name:        r.ResolveProperty(ctx, pom, pom.Name),
			Description: r.ResolveProperty(ctx, pom, pom.Description),
			URL:         r.ResolveProperty(ctx, pom, pom.URL),
		}
	}

	m := pkg.JavaArchive{
		PomProperties: &pkg.JavaPomProperties{
			GroupID:    id.GroupID,
			ArtifactID: id.ArtifactID,
			Scope:      r.ResolveProperty(ctx, pom, dep.Scope),
		},
		PomProject: pomProject,
	}

	p := &pkg.Package{
		Name:      id.ArtifactID,
		Version:   id.Version,
		Locations: file.NewLocationSet(locations...),
		Licenses:  pkg.NewLicenseSet(pkgLicenses...),
		PURL:      packageURL(id.ArtifactID, id.Version, m),
		Language:  pkg.Java,
		Type:      pkg.JavaPkg, // TODO: should we differentiate between packages from jar/war/zip versus packages from a pom.xml that were not installed yet?
		FoundBy:   pomCatalogerName,
		Metadata:  m,
	}

	finalizePackage(p)

	return p, err
}

func pomParent(ctx context.Context, r *maven.Resolver, pom *maven.Project) *pkg.JavaPomParent {
	if pom == nil || pom.Parent == nil {
		return nil
	}

	groupID := r.ResolveProperty(ctx, pom, pom.Parent.GroupID)
	artifactID := r.ResolveProperty(ctx, pom, pom.Parent.ArtifactID)
	version := r.ResolveProperty(ctx, pom, pom.Parent.Version)

	if groupID == "" && artifactID == "" && version == "" {
		return nil
	}

	return &pkg.JavaPomParent{
		GroupID:    groupID,
		ArtifactID: artifactID,
		Version:    version,
	}
}

func cleanDescription(original string) (cleaned string) {
	descriptionLines := strings.Split(original, "\n")
	for _, line := range descriptionLines {
		line = strings.TrimSpace(line)
		if len(line) == 0 {
			continue
		}
		cleaned += line + " "
	}
	return strings.TrimSpace(cleaned)
}
