package composer

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"slices"
	"sort"
	"strings"

	"golang.org/x/xerrors"

	"github.com/aquasecurity/trivy/pkg/dependency/parser/php/composer"
	"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
	"github.com/aquasecurity/trivy/pkg/fanal/analyzer/language"
	"github.com/aquasecurity/trivy/pkg/fanal/types"
	"github.com/aquasecurity/trivy/pkg/log"
	"github.com/aquasecurity/trivy/pkg/utils/fsutils"
)

func init() {
	analyzer.RegisterPostAnalyzer(analyzer.TypeComposer, newComposerAnalyzer)
}

const composerAnalyzerVersion = 1

var requiredFiles = []string{
	types.ComposerLock,
	types.ComposerJson,
}

type composerAnalyzer struct {
	lockParser language.Parser
}

func newComposerAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) {
	return &composerAnalyzer{
		lockParser: composer.NewParser(),
	}, nil
}

func (a composerAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) {
	var apps []types.Application

	required := func(path string, _ fs.DirEntry) bool {
		return filepath.Base(path) == types.ComposerLock || input.FilePatterns.Match(path)
	}

	err := fsutils.WalkDir(input.FS, ".", required, func(path string, _ fs.DirEntry, r io.Reader) error {
		// Parse composer.lock
		app, err := a.parseComposerLock(ctx, path, r)
		if err != nil {
			return xerrors.Errorf("parse error: %w", err)
		} else if app == nil {
			return nil
		}

		// Parse composer.json alongside composer.lock to identify the direct dependencies
		if err = a.mergeComposerJson(input.FS, filepath.Dir(path), app); err != nil {
			log.Warn("Unable to parse composer.json to identify direct dependencies",
				log.FilePath(filepath.Join(filepath.Dir(path), types.ComposerJson)), log.Err(err))
		}
		sort.Sort(app.Packages)
		apps = append(apps, *app)

		return nil
	})
	if err != nil {
		return nil, xerrors.Errorf("composer walk error: %w", err)
	}

	return &analyzer.AnalysisResult{
		Applications: apps,
	}, nil
}

func (a composerAnalyzer) Required(filePath string, _ os.FileInfo) bool {
	fileName := filepath.Base(filePath)
	if !slices.Contains(requiredFiles, fileName) {
		return false
	}

	// Skip `composer.lock` inside `vendor` folder
	if slices.Contains(strings.Split(filePath, "/"), "vendor") {
		return false
	}
	return true
}

func (a composerAnalyzer) Type() analyzer.Type {
	return analyzer.TypeComposer
}

func (a composerAnalyzer) Version() int {
	return composerAnalyzerVersion
}

func (a composerAnalyzer) parseComposerLock(ctx context.Context, path string, r io.Reader) (*types.Application, error) {
	return language.Parse(ctx, types.Composer, path, r, a.lockParser)
}

func (a composerAnalyzer) mergeComposerJson(fsys fs.FS, dir string, app *types.Application) error {
	// Parse composer.json to identify the direct dependencies
	path := filepath.Join(dir, types.ComposerJson)
	cj, err := a.parseComposerJson(fsys, path)
	if errors.Is(err, fs.ErrNotExist) {
		// Assume all the packages are direct dependencies as it cannot identify them from composer.lock
		log.Debug("Unable to determine the direct dependencies, composer.json not found", log.FilePath(path))
		return nil
	} else if err != nil {
		return xerrors.Errorf("unable to parse %s: %w", path, err)
	}

	for i, pkg := range app.Packages {
		// Identify the direct/transitive dependencies
		if _, ok := cj.Require[pkg.Name]; ok {
			app.Packages[i].Relationship = types.RelationshipDirect
		} else if _, ok := cj.RequireDev[pkg.Name]; ok {
			app.Packages[i].Relationship = types.RelationshipDirect
		} else {
			app.Packages[i].Indirect = true
			app.Packages[i].Relationship = types.RelationshipIndirect
		}
	}

	return nil
}

type composerJson struct {
	Require    map[string]string `json:"require"`
	RequireDev map[string]string `json:"require-dev"`
}

func (a composerAnalyzer) parseComposerJson(fsys fs.FS, path string) (composerJson, error) {
	// Parse composer.json
	f, err := fsys.Open(path)
	if err != nil {
		return composerJson{}, xerrors.Errorf("file open error: %w", err)
	}
	defer func() { _ = f.Close() }()

	var jsonFile composerJson
	err = json.NewDecoder(f).Decode(&jsonFile)
	if err != nil {
		return composerJson{}, xerrors.Errorf("json decode error: %w", err)
	}
	return jsonFile, nil
}
