package bundler

import (
	"bufio"
	"context"
	"sort"
	"strings"

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

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

type Parser struct{}

func NewParser() *Parser {
	return &Parser{}
}

func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
	pkgs := make(map[string]ftypes.Package)
	var dependsOn, directDeps []string
	var deps []ftypes.Dependency
	var pkgID string

	lineNum := 1
	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		line := scanner.Text()

		// Parse dependencies
		if countLeadingSpace(line) == 4 {
			if len(dependsOn) > 0 {
				deps = append(deps, ftypes.Dependency{
					ID:        pkgID,
					DependsOn: dependsOn,
				})
			}
			dependsOn = make([]string, 0) // re-initialize
			line = strings.TrimSpace(line)
			s := strings.Fields(line)
			if len(s) != 2 {
				continue
			}
			version := strings.Trim(s[1], "()")          // drop parentheses
			version = strings.SplitN(version, "-", 2)[0] // drop platform (e.g. 1.13.6-x86_64-linux => 1.13.6)
			name := s[0]
			pkgID = packageID(name, version)
			pkgs[name] = ftypes.Package{
				ID:           pkgID,
				Name:         name,
				Version:      version,
				Relationship: ftypes.RelationshipIndirect,
				Locations: []ftypes.Location{
					{
						StartLine: lineNum,
						EndLine:   lineNum,
					},
				},
			}
		}
		// Parse dependency graph
		if countLeadingSpace(line) == 6 {
			line = strings.TrimSpace(line)
			s := strings.Fields(line)
			dependsOn = append(dependsOn, s[0]) // store name only for now
		}
		lineNum++

		// Parse direct dependencies
		if line == "DEPENDENCIES" {
			directDeps = parseDirectDeps(scanner)
		}
	}
	// append last dependency (if any)
	if len(dependsOn) > 0 {
		deps = append(deps, ftypes.Dependency{
			ID:        pkgID,
			DependsOn: dependsOn,
		})
	}

	// Identify which are direct dependencies
	for _, d := range directDeps {
		if l, ok := pkgs[d]; ok {
			l.Relationship = ftypes.RelationshipDirect
			pkgs[d] = l
		}
	}

	for i, dep := range deps {
		dependsOn = make([]string, 0)
		for _, pkgName := range dep.DependsOn {
			if pkg, ok := pkgs[pkgName]; ok {
				dependsOn = append(dependsOn, packageID(pkgName, pkg.Version))
			}
		}
		deps[i].DependsOn = dependsOn
	}
	if err := scanner.Err(); err != nil {
		return nil, nil, xerrors.Errorf("scan error: %w", err)
	}

	pkgSlice := lo.Values(pkgs)
	sort.Sort(ftypes.Packages(pkgSlice))
	return pkgSlice, deps, nil
}

func countLeadingSpace(line string) int {
	i := 0
	for _, runeValue := range line {
		if runeValue != ' ' {
			break
		}
		i++
	}
	return i
}

// Parse "DEPENDENCIES"
func parseDirectDeps(scanner *bufio.Scanner) []string {
	var deps []string
	for scanner.Scan() {
		line := scanner.Text()
		if countLeadingSpace(line) != 2 {
			// Reach another section
			break
		}
		ss := strings.Fields(line)
		if len(ss) == 0 {
			continue
		}
		deps = append(deps, ss[0])
	}
	return deps
}

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