package cargo

import (
	"context"
	"io"
	"sort"
	"strings"

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

	"github.com/aquasecurity/trivy/pkg/dependency"
	ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
	"github.com/aquasecurity/trivy/pkg/log"
	xio "github.com/aquasecurity/trivy/pkg/x/io"
)

type cargoPkg struct {
	Name         string   `toml:"name"`
	Version      string   `toml:"version"`
	Source       string   `toml:"source,omitempty"`
	Dependencies []string `toml:"dependencies,omitempty"`
}
type Lockfile struct {
	Packages []cargoPkg `toml:"package"`
}

type Parser struct {
	logger *log.Logger
}

func NewParser() *Parser {
	return &Parser{
		logger: log.WithPrefix("cargo"),
	}
}

func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
	var lockfile Lockfile
	decoder := toml.NewDecoder(r)
	if _, err := decoder.Decode(&lockfile); err != nil {
		return nil, nil, xerrors.Errorf("decode error: %w", err)
	}

	if _, err := r.Seek(0, io.SeekStart); err != nil {
		return nil, nil, xerrors.Errorf("seek error: %w", err)
	}

	// naive parser to get line numbers by package from lock file
	pkgParser := naivePkgParser{r: r}
	lineNumIdx := pkgParser.parse()

	// We need to get version for unique dependencies for lockfile v3 from lockfile.Packages
	pkgMap := lo.SliceToMap(lockfile.Packages, func(pkg cargoPkg) (string, cargoPkg) {
		return pkg.Name, pkg
	})

	var pkgs ftypes.Packages
	var deps ftypes.Dependencies
	for _, lpkg := range lockfile.Packages {
		pkgID := packageID(lpkg.Name, lpkg.Version)
		pkg := ftypes.Package{
			ID:      pkgID,
			Name:    lpkg.Name,
			Version: lpkg.Version,
		}
		if pos, ok := lineNumIdx[pkgID]; ok {
			pkg.Locations = []ftypes.Location{
				{
					StartLine: pos.start,
					EndLine:   pos.end,
				},
			}
		}

		pkgs = append(pkgs, pkg)
		dep := p.parseDependencies(pkgID, lpkg, pkgMap)
		if dep != nil {
			deps = append(deps, *dep)
		}
	}
	sort.Sort(pkgs)
	sort.Sort(deps)
	return pkgs, deps, nil
}
func (p *Parser) parseDependencies(pkgId string, pkg cargoPkg, pkgs map[string]cargoPkg) *ftypes.Dependency {
	var dependOn []string

	for _, pkgDep := range pkg.Dependencies {
		/*
			Dependency entries look like:
			old Cargo.lock - https://github.com/rust-lang/cargo/blob/46bac2dc448ab12fe0f182bee8d35cc804d9a6af/tests/testsuite/lockfile_compat.rs#L48-L50
				"unsafe-any 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)"
			new Cargo.lock -https://github.com/rust-lang/cargo/blob/46bac2dc448ab12fe0f182bee8d35cc804d9a6af/tests/testsuite/lockfile_compat.rs#L39-L41
				"unsafe-any" - if lock file contains only 1 version of dependency
				"unsafe-any 0.4.2" if lock file contains more than 1 version of dependency
		*/
		fields := strings.Fields(pkgDep)
		switch len(fields) {
		// unique dependency in new lock file
		case 1:
			name := fields[0]
			version, ok := pkgs[name]
			if !ok {
				p.logger.Debug("Cannot find version", log.String("name", name))
				continue
			}
			dependOn = append(dependOn, packageID(name, version.Version))
		// 2: non-unique dependency in new lock file
		// 3: old lock file
		case 2, 3:
			dependOn = append(dependOn, packageID(fields[0], fields[1]))
		default:
			p.logger.Debug("Wrong dependency format", log.String("dep", pkgDep))
			continue
		}
	}
	if len(dependOn) > 0 {
		sort.Strings(dependOn)
		return &ftypes.Dependency{
			ID:        pkgId,
			DependsOn: dependOn,
		}
	}
	return nil
}

func packageID(name, version string) string {
	return dependency.ID(ftypes.Cargo, name, version)
}
