package resourcehandler

import (
	"context"
	"fmt"
	"maps"
	"path/filepath"
	"strings"

	"github.com/kubescape/go-logger"
	"github.com/kubescape/go-logger/helpers"
	"github.com/kubescape/k8s-interface/k8sinterface"
	"github.com/kubescape/k8s-interface/workloadinterface"
	"github.com/kubescape/kubescape/v3/core/cautils"
	"github.com/kubescape/opa-utils/reporthandling"
	"k8s.io/apimachinery/pkg/version"
)

// FileResourceHandler handle resources from files and URLs
type FileResourceHandler struct{}

func NewFileResourceHandler() *FileResourceHandler {
	k8sinterface.InitializeMapResourcesMock() // initialize the resource map
	return &FileResourceHandler{}
}

func (fileHandler *FileResourceHandler) GetResources(ctx context.Context, sessionObj *cautils.OPASessionObj, scanInfo *cautils.ScanInfo) (cautils.K8SResources, map[string]workloadinterface.IMetadata, cautils.ExternalResources, map[string]bool, error) {
	allResources := map[string]workloadinterface.IMetadata{}
	externalResources := cautils.ExternalResources{}

	if len(scanInfo.InputPatterns) == 0 {
		return nil, nil, nil, nil, fmt.Errorf("missing input")
	}

	logger.L().Start("Accessing local objects...")

	// load resources from all input paths
	mappedResources := map[string][]workloadinterface.IMetadata{}
	for path := range scanInfo.InputPatterns {
		var workloadIDToSource map[string]reporthandling.Source
		var workloads []workloadinterface.IMetadata
		var err error

		helmValueOpts := helmValueOptionsFromScanInfo(scanInfo)
		if scanInfo.ChartPath != "" && scanInfo.FilePath != "" {
			workloadIDToSource, workloads, err = getWorkloadFromHelmChart(ctx, scanInfo.InputPatterns[path], scanInfo.ChartPath, scanInfo.FilePath, helmValueOpts)
		} else {
			workloadIDToSource, workloads, err = getResourcesFromPath(ctx, scanInfo.InputPatterns[path], helmValueOpts)
		}
		if err != nil {
			return nil, allResources, nil, nil, err
		}
		if len(workloads) == 0 {
			continue
		}

		maps.Copy(sessionObj.ResourceSource, workloadIDToSource)

		// map all resources: map["/apiVersion/version/kind"][]<k8s workloads>
		addWorkloadsToResourcesMap(mappedResources, workloads)
	}

	// locate input k8s object in the mapped resources - if not found or not a valid resource, return error
	var err error
	if sessionObj.SingleResourceScan, err = findScanObjectResource(mappedResources, scanInfo.ScanObject); err != nil {
		return nil, nil, nil, nil, err
	}

	if sessionObj.SingleResourceScan != nil && k8sinterface.WorkloadHasParent(sessionObj.SingleResourceScan) {
		return nil, nil, nil, nil, fmt.Errorf("resource %s has a parent and cannot be scanned", sessionObj.SingleResourceScan.GetID())
	}

	scanningScope := cautils.GetScanningScope(sessionObj.Metadata.ContextMetadata)

	// build a resources map, based on the policies
	// map resources based on framework required resources: map["/group/version/kind"][]<k8s workloads ids>
	resourceToQuery, excludedRulesMap := getQueryableResourceMapFromPolicies(sessionObj.Policies, sessionObj.SingleResourceScan, scanningScope)
	k8sResources := resourceToQuery.ToK8sResourceMap()

	// save only relevant resources
	for i := range mappedResources {
		if _, ok := k8sResources[i]; ok {
			var ids []string
			for j := range mappedResources[i] {
				ids = append(ids, mappedResources[i][j].GetID())
				allResources[mappedResources[i][j].GetID()] = mappedResources[i][j]
			}
			k8sResources[i] = append(k8sResources[i], ids...)
		}
	}

	logger.L().StopSuccess("Done accessing local objects")
	// save input resource in resource maps
	addSingleResourceToResourceMaps(k8sResources, allResources, sessionObj.SingleResourceScan)

	return k8sResources, allResources, externalResources, excludedRulesMap, nil
}

func (fileHandler *FileResourceHandler) GetCloudProvider() string {
	return ""
}

// helmValueOptionsFromScanInfo extracts the user-supplied Helm value/release flags from ScanInfo
// into the cautils.HelmValueOptions used by chart rendering.
func helmValueOptionsFromScanInfo(scanInfo *cautils.ScanInfo) cautils.HelmValueOptions {
	if scanInfo == nil {
		return cautils.HelmValueOptions{}
	}
	return cautils.HelmValueOptions{
		ValueFiles:       scanInfo.HelmValueFiles,
		Values:           scanInfo.HelmSetValues,
		StringValues:     scanInfo.HelmSetStringValues,
		FileValues:       scanInfo.HelmSetFileValues,
		ReleaseName:      scanInfo.HelmReleaseName,
		ReleaseNamespace: scanInfo.HelmReleaseNamespace,
	}
}

func getWorkloadFromHelmChart(ctx context.Context, path, helmPath, workloadPath string, helmValueOpts cautils.HelmValueOptions) (map[string]reporthandling.Source, []workloadinterface.IMetadata, error) {
	clonedRepo := cautils.GetClonedPath(path)

	if clonedRepo != "" {
		// if the repo was cloned, add the workload path to the cloned repo
		workloadPath = filepath.Join(clonedRepo, workloadPath)
	} else {
		// if the repo was not cloned
		clonedRepo = path
	}

	// Get repo root
	repoRoot, gitRepo := extractGitRepo(clonedRepo)

	helmSourceToWorkloads, helmSourceToChart, err := cautils.LoadResourcesFromHelmCharts(ctx, helmPath, helmValueOpts)
	if err != nil {
		return nil, nil, err
	}

	wlSource, ok := helmSourceToWorkloads[workloadPath]
	if !ok {
		return nil, nil, fmt.Errorf("workload %s not found in chart %s", workloadPath, helmPath)
	}

	if len(wlSource) != 1 {
		return nil, nil, fmt.Errorf("workload %s found multiple times in chart %s", workloadPath, helmPath)
	}

	helmChart, ok := helmSourceToChart[workloadPath]
	if !ok {
		return nil, nil, fmt.Errorf("helmChart not found for workload %s", workloadPath)
	}

	workloadSource := getWorkloadSourceHelmChart(repoRoot, helmPath, gitRepo, helmChart)

	workloadIDToSource := make(map[string]reporthandling.Source, 1)
	workloadIDToSource[wlSource[0].GetID()] = workloadSource

	var workloads []workloadinterface.IMetadata
	workloads = append(workloads, wlSource...)

	return workloadIDToSource, workloads, nil

}

func getWorkloadSourceHelmChart(repoRoot string, source string, gitRepo *cautils.LocalGitRepository, helmChart cautils.Chart) reporthandling.Source {
	relSource, err := filepath.Rel(repoRoot, source)
	if err == nil {
		source = relSource
	}

	var lastCommit reporthandling.LastCommit
	if gitRepo != nil {
		commitInfo, _ := gitRepo.GetFileLastCommit(source)
		if commitInfo != nil {
			lastCommit = reporthandling.LastCommit{
				Hash:           commitInfo.SHA,
				Date:           commitInfo.Author.Date,
				CommitterName:  commitInfo.Author.Name,
				CommitterEmail: commitInfo.Author.Email,
				Message:        commitInfo.Message,
			}
		}
	}

	return reporthandling.Source{
		Path:             repoRoot,
		HelmPath:         helmChart.Path,
		RelativePath:     source,
		FileType:         reporthandling.SourceTypeHelmChart,
		HelmChartName:    helmChart.Name,
		HelmTemplateFile: helmChart.Provenance.TemplateFile,
		HelmValuesPaths:  helmChart.Provenance.ValuesPaths,
		HelmTemplateLine: helmChart.Provenance.TemplateLine,
		LastCommit:       lastCommit,
	}
}

func getResourcesFromPath(ctx context.Context, path string, helmValueOpts cautils.HelmValueOptions) (map[string]reporthandling.Source, []workloadinterface.IMetadata, error) {
	workloadIDToSource := make(map[string]reporthandling.Source)
	var workloads []workloadinterface.IMetadata

	clonedRepo := cautils.GetClonedPath(path)
	if clonedRepo != "" {
		// if the repo was cloned, add the workload path to the cloned repo
		path = clonedRepo
	}

	// Get repo root
	repoRoot, gitRepo := extractGitRepo(path)

	// when scanning a single file, we consider the repository root to be
	// the directory of the scanned file
	if cautils.IsYaml(repoRoot) {
		repoRoot = filepath.Dir(repoRoot)
	}

	// load resource from local file system
	sourceToWorkloads := cautils.LoadResourcesFromFiles(ctx, path, repoRoot)

	// update workloads and workloadIDToSource
	var warnIssued bool
	for source, ws := range sourceToWorkloads {
		workloads = append(workloads, ws...)

		relSource, err := filepath.Rel(repoRoot, source)

		if err == nil {
			source = relSource
		}

		var filetype string
		if cautils.IsYaml(source) {
			filetype = reporthandling.SourceTypeYaml
		} else if cautils.IsJson(source) {
			filetype = reporthandling.SourceTypeJson
		} else {
			continue
		}

		var lastCommit reporthandling.LastCommit
		if gitRepo != nil {
			commitInfo, err := gitRepo.GetFileLastCommit(source)
			if err != nil && !warnIssued {
				logger.L().Debug("Git scan skipped", helpers.Error(err))
				warnIssued = true // croak only once
			}

			if commitInfo != nil {
				lastCommit = reporthandling.LastCommit{
					Hash:           commitInfo.SHA,
					Date:           commitInfo.Author.Date,
					CommitterName:  commitInfo.Author.Name,
					CommitterEmail: commitInfo.Author.Email,
					Message:        commitInfo.Message,
				}
			}
		}

		var workloadSource reporthandling.Source
		if clonedRepo != "" {
			workloadSource = reporthandling.Source{
				Path:         "",
				RelativePath: relSource,
				FileType:     filetype,
				LastCommit:   lastCommit,
			}
		} else {
			workloadSource = reporthandling.Source{
				Path:         repoRoot,
				RelativePath: relSource,
				FileType:     filetype,
				LastCommit:   lastCommit,
			}
		}

		for i := range ws {
			workloadIDToSource[ws[i].GetID()] = workloadSource
		}
	}

	if len(workloads) == 0 {
		logger.L().Debug("files found in local storage", helpers.Int("files", len(sourceToWorkloads)), helpers.Int("workloads", len(workloads)))
	}

	// load resources from helm charts
	helmSourceToWorkloads, helmSourceToChart, err := cautils.LoadResourcesFromHelmCharts(ctx, path, helmValueOpts)
	if err != nil {
		return nil, nil, err
	}
	for source, ws := range helmSourceToWorkloads {
		workloads = append(workloads, ws...)
		helmChart := helmSourceToChart[source]

		if clonedRepo != "" && gitRepo != nil {
			url, err := gitRepo.GetRemoteUrl()
			if err != nil {
				logger.L().Warning("failed to get remote url", helpers.Error(err))
				break
			}
			helmChart.Path = strings.TrimSuffix(url, ".git")
			repoRoot = ""
			source = strings.TrimPrefix(source, fmt.Sprintf("%s/", clonedRepo))
		}

		workloadSource := getWorkloadSourceHelmChart(repoRoot, source, gitRepo, helmChart)

		for i := range ws {
			workloadIDToSource[ws[i].GetID()] = workloadSource
		}
	}

	if len(helmSourceToWorkloads) > 0 { // && len(helmSourceToNodes) > 0
		logger.L().Debug("helm templates found in local storage", helpers.Int("helmTemplates", len(helmSourceToWorkloads)), helpers.Int("workloads", len(workloads)))
	}

	//patch, get value from env
	// Load resources from Kustomize directory
	kustomizeSourceToWorkloads, kustomizeDirectoryName := cautils.LoadResourcesFromKustomizeDirectory(ctx, path) //?

	// update workloads and workloadIDToSource with workloads from Kustomize Directory
	for source, ws := range kustomizeSourceToWorkloads {
		workloads = append(workloads, ws...)
		relSource, err := filepath.Rel(repoRoot, source)

		if err == nil {
			source = relSource
		}

		var lastCommit reporthandling.LastCommit
		if gitRepo != nil {
			commitInfo, _ := gitRepo.GetFileLastCommit(source)
			if commitInfo != nil {
				lastCommit = reporthandling.LastCommit{
					Hash:           commitInfo.SHA,
					Date:           commitInfo.Author.Date,
					CommitterName:  commitInfo.Author.Name,
					CommitterEmail: commitInfo.Author.Email,
					Message:        commitInfo.Message,
				}
			}
		}

		workloadSource := reporthandling.Source{
			RelativePath:           source,
			FileType:               reporthandling.SourceTypeKustomizeDirectory,
			KustomizeDirectoryName: kustomizeDirectoryName,
			LastCommit:             lastCommit,
		}

		for i := range ws {
			workloadIDToSource[ws[i].GetID()] = workloadSource
		}
	}

	// backstop: drop cross-provider identity collisions the path-level kustomize skip can't see (e.g. helm + raw YAML)
	workloads, workloadIDToSource = dedupWorkloads(workloads, workloadIDToSource)

	return workloadIDToSource, workloads, nil
}

func extractGitRepo(path string) (string, *cautils.LocalGitRepository) {
	repoRoot := ""
	gitRepo, err := cautils.NewLocalGitRepository(path)
	if err == nil && gitRepo != nil {
		repoRoot, _ = gitRepo.GetRootDir()
	} else {
		repoRoot, _ = filepath.Abs(path)
	}
	return repoRoot, gitRepo
}

func (fileHandler *FileResourceHandler) GetClusterAPIServerInfo(_ context.Context) *version.Info {
	return nil
}
