package cautils

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"

	"github.com/armosec/armoapi-go/armotypes"
	apisv1 "github.com/armosec/opa-utils/httpserver/apis/v1"

	giturl "github.com/armosec/go-git-url"
	"github.com/armosec/k8s-interface/k8sinterface"
	"github.com/armosec/kubescape/v2/core/cautils/getter"
	"github.com/armosec/opa-utils/reporthandling"
	reporthandlingv2 "github.com/armosec/opa-utils/reporthandling/v2"
	logger "github.com/dwertent/go-logger"
	"github.com/dwertent/go-logger/helpers"

	"github.com/google/uuid"
)

type ScanningContext string

const (
	ContextCluster  ScanningContext = "cluster"
	ContextFile     ScanningContext = "single-file"
	ContextDir      ScanningContext = "local-dir"
	ContextGitURL   ScanningContext = "git-url"
	ContextGitLocal ScanningContext = "git-local"
)

const ( // deprecated
	ScopeCluster = "cluster"
	ScopeYAML    = "yaml"
)
const (
	// ScanCluster                string = "cluster"
	// ScanLocalFiles             string = "yaml"
	localControlInputsFilename string = "controls-inputs.json"
	localExceptionsFilename    string = "exceptions.json"
)

type BoolPtrFlag struct {
	valPtr *bool
}

func NewBoolPtr(b *bool) BoolPtrFlag {
	return BoolPtrFlag{valPtr: b}
}

func (bpf *BoolPtrFlag) Type() string {
	return "bool"
}

func (bpf *BoolPtrFlag) String() string {
	if bpf.valPtr != nil {
		return fmt.Sprintf("%v", *bpf.valPtr)
	}
	return ""
}
func (bpf *BoolPtrFlag) Get() *bool {
	return bpf.valPtr
}
func (bpf *BoolPtrFlag) GetBool() bool {
	if bpf.valPtr == nil {
		return false
	}
	return *bpf.valPtr
}

func (bpf *BoolPtrFlag) SetBool(val bool) {
	bpf.valPtr = &val
}

func (bpf *BoolPtrFlag) Set(val string) error {
	switch val {
	case "true":
		bpf.SetBool(true)
	case "false":
		bpf.SetBool(false)
	}
	return nil
}

// TODO - UPDATE
type ViewTypes string

const (
	ResourceViewType ViewTypes = "resource"
	ControlViewType  ViewTypes = "control"
)

type PolicyIdentifier struct {
	Name        string                        // policy name e.g. nsa,mitre,c-0012
	Kind        apisv1.NotificationPolicyKind // policy kind e.g. Framework,Control,Rule
	Designators armotypes.PortalDesignator
}

type ScanInfo struct {
	Getters                               // TODO - remove from object
	PolicyIdentifier   []PolicyIdentifier // TODO - remove from object
	UseExceptions      string             // Load file with exceptions configuration
	ControlsInputs     string             // Load file with inputs for controls
	UseFrom            []string           // Load framework from local file (instead of download). Use when running offline
	UseDefault         bool               // Load framework from cached file (instead of download). Use when running offline
	UseArtifactsFrom   string             // Load artifacts from local path. Use when running offline
	VerboseMode        bool               // Display all of the input resources and not only failed resources
	View               string             // Display all of the input resources and not only failed resources
	Format             string             // Format results (table, json, junit ...)
	Output             string             // Store results in an output file, Output file name
	FormatVersion      string             // Output object can be differnet between versions, this is for testing and backward compatibility
	ExcludedNamespaces string             // used for host scanner namespace
	IncludeNamespaces  string             //
	InputPatterns      []string           // Yaml files input patterns
	Silent             bool               // Silent mode - Do not print progress logs
	FailThreshold      float32            // Failure score threshold
	Submit             bool               // Submit results to Armo BE
	ScanID             string             // Report id of the current scan
	HostSensorEnabled  BoolPtrFlag        // Deploy ARMO K8s host scanner to collect data from certain controls
	HostSensorYamlPath string             // Path to hostsensor file
	Local              bool               // Do not submit results
	Credentials        Credentials        // account ID
	KubeContext        string             // context name
	FrameworkScan      bool               // false if scanning control
	ScanAll            bool               // true if scan all frameworks
}

type Getters struct {
	ExceptionsGetter     getter.IExceptionsGetter
	ControlsInputsGetter getter.IControlsInputsGetter
	PolicyGetter         getter.IPolicyGetter
}

func (scanInfo *ScanInfo) Init() {
	scanInfo.setUseFrom()
	scanInfo.setOutputFile()
	scanInfo.setUseArtifactsFrom()
	if scanInfo.ScanID == "" {
		scanInfo.ScanID = uuid.NewString()
	}

}

func (scanInfo *ScanInfo) setUseArtifactsFrom() {
	if scanInfo.UseArtifactsFrom == "" {
		return
	}
	// UseArtifactsFrom must be a path without a filename
	dir, file := filepath.Split(scanInfo.UseArtifactsFrom)
	if dir == "" {
		scanInfo.UseArtifactsFrom = file
	} else if strings.Contains(file, ".json") {
		scanInfo.UseArtifactsFrom = dir
	}
	// set frameworks files
	files, err := ioutil.ReadDir(scanInfo.UseArtifactsFrom)
	if err != nil {
		logger.L().Fatal("failed to read files from directory", helpers.String("dir", scanInfo.UseArtifactsFrom), helpers.Error(err))
	}
	framework := &reporthandling.Framework{}
	for _, f := range files {
		filePath := filepath.Join(scanInfo.UseArtifactsFrom, f.Name())
		file, err := os.ReadFile(filePath)
		if err == nil {
			if err := json.Unmarshal(file, framework); err == nil {
				scanInfo.UseFrom = append(scanInfo.UseFrom, filepath.Join(scanInfo.UseArtifactsFrom, f.Name()))
			}
		}
	}
	// set config-inputs file
	scanInfo.ControlsInputs = filepath.Join(scanInfo.UseArtifactsFrom, localControlInputsFilename)
	// set exceptions
	scanInfo.UseExceptions = filepath.Join(scanInfo.UseArtifactsFrom, localExceptionsFilename)
}

func (scanInfo *ScanInfo) setUseFrom() {
	if scanInfo.UseDefault {
		for _, policy := range scanInfo.PolicyIdentifier {
			scanInfo.UseFrom = append(scanInfo.UseFrom, getter.GetDefaultPath(policy.Name+".json"))
		}
	}
}

func (scanInfo *ScanInfo) setOutputFile() {
	if scanInfo.Output == "" {
		return
	}
	if scanInfo.Format == "json" {
		if filepath.Ext(scanInfo.Output) != ".json" {
			scanInfo.Output += ".json"
		}
	}
	if scanInfo.Format == "junit" {
		if filepath.Ext(scanInfo.Output) != ".xml" {
			scanInfo.Output += ".xml"
		}
	}
	if scanInfo.Format == "pdf" {
		if filepath.Ext(scanInfo.Output) != ".pdf" {
			scanInfo.Output += ".pdf"
		}
	}
}

// func (scanInfo *ScanInfo) GetScanningEnvironment() string {
// 	if len(scanInfo.InputPatterns) != 0 {
// 		return ScanLocalFiles
// 	}
// 	return ScanCluster
// }

func (scanInfo *ScanInfo) SetPolicyIdentifiers(policies []string, kind apisv1.NotificationPolicyKind) {
	for _, policy := range policies {
		if !scanInfo.contains(policy) {
			newPolicy := PolicyIdentifier{}
			newPolicy.Kind = kind
			newPolicy.Name = policy
			scanInfo.PolicyIdentifier = append(scanInfo.PolicyIdentifier, newPolicy)
		}
	}
}

func (scanInfo *ScanInfo) contains(policyName string) bool {
	for _, policy := range scanInfo.PolicyIdentifier {
		if policy.Name == policyName {
			return true
		}
	}
	return false
}

func scanInfoToScanMetadata(scanInfo *ScanInfo) *reporthandlingv2.Metadata {
	metadata := &reporthandlingv2.Metadata{}

	metadata.ScanMetadata.Format = scanInfo.Format
	metadata.ScanMetadata.FormatVersion = scanInfo.FormatVersion
	metadata.ScanMetadata.Submit = scanInfo.Submit

	// TODO - Add excluded and included namespaces
	// if len(scanInfo.ExcludedNamespaces) > 1 {
	// 	opaSessionObj.Metadata.ScanMetadata.ExcludedNamespaces = strings.Split(scanInfo.ExcludedNamespaces[1:], ",")
	// }
	// if len(scanInfo.IncludeNamespaces) > 1 {
	// 	opaSessionObj.Metadata.ScanMetadata.IncludeNamespaces = strings.Split(scanInfo.IncludeNamespaces[1:], ",")
	// }

	// scan type
	if len(scanInfo.PolicyIdentifier) > 0 {
		metadata.ScanMetadata.TargetType = string(scanInfo.PolicyIdentifier[0].Kind)
	}
	// append frameworks
	for _, policy := range scanInfo.PolicyIdentifier {
		metadata.ScanMetadata.TargetNames = append(metadata.ScanMetadata.TargetNames, policy.Name)
	}

	metadata.ScanMetadata.KubescapeVersion = BuildNumber
	metadata.ScanMetadata.VerboseMode = scanInfo.VerboseMode
	metadata.ScanMetadata.FailThreshold = scanInfo.FailThreshold
	metadata.ScanMetadata.HostScanner = scanInfo.HostSensorEnabled.GetBool()
	metadata.ScanMetadata.VerboseMode = scanInfo.VerboseMode
	metadata.ScanMetadata.ControlsInputs = scanInfo.ControlsInputs

	inputFiles := ""
	if len(scanInfo.InputPatterns) > 0 {
		inputFiles = scanInfo.InputPatterns[0]
	}
	switch GetScanningContext(inputFiles) {
	case ContextCluster:
		// cluster
		metadata.ScanMetadata.ScanningTarget = reporthandlingv2.Cluster
	case ContextFile:
		// local file
		metadata.ScanMetadata.ScanningTarget = reporthandlingv2.File
	case ContextGitURL:
		// url
		metadata.ScanMetadata.ScanningTarget = reporthandlingv2.Repo
	case ContextGitLocal:
		// local-git
		metadata.ScanMetadata.ScanningTarget = reporthandlingv2.GitLocal
	case ContextDir:
		// directory
		metadata.ScanMetadata.ScanningTarget = reporthandlingv2.Directory

	}

	setContextMetadata(&metadata.ContextMetadata, inputFiles)

	return metadata
}

func (scanInfo *ScanInfo) GetScanningContext() ScanningContext {
	input := ""
	if len(scanInfo.InputPatterns) > 0 {
		input = scanInfo.InputPatterns[0]
	}
	return GetScanningContext(input)
}

// GetScanningContext get scanning context from the input param
func GetScanningContext(input string) ScanningContext {
	//  cluster
	if input == "" {
		return ContextCluster
	}

	// url
	if _, err := giturl.NewGitURL(input); err == nil {
		return ContextGitURL
	}

	if !filepath.IsAbs(input) { // parse path
		if o, err := os.Getwd(); err == nil {
			input = filepath.Join(o, input)
		}
	}

	// local git repo
	if _, err := NewLocalGitRepository(input); err == nil {
		return ContextGitLocal
	}

	//  single file
	if IsFile(input) {
		return ContextFile
	}

	//  dir/glob
	return ContextDir
}
func setContextMetadata(contextMetadata *reporthandlingv2.ContextMetadata, input string) {
	switch GetScanningContext(input) {
	case ContextCluster:
		contextMetadata.ClusterContextMetadata = &reporthandlingv2.ClusterMetadata{
			ContextName: k8sinterface.GetContextName(),
		}
	case ContextGitURL:
		// url
		context, err := metadataGitURL(input)
		if err != nil {
			logger.L().Warning("in setContextMetadata", helpers.Interface("case", ContextGitURL), helpers.Error(err))
		}
		contextMetadata.RepoContextMetadata = context
	case ContextDir:
		contextMetadata.DirectoryContextMetadata = &reporthandlingv2.DirectoryContextMetadata{
			BasePath: getAbsPath(input),
			HostName: getHostname(),
		}
	case ContextFile:
		contextMetadata.FileContextMetadata = &reporthandlingv2.FileContextMetadata{
			FilePath: getAbsPath(input),
			HostName: getHostname(),
		}
	case ContextGitLocal:
		// local
		context, err := metadataGitLocal(input)
		if err != nil {
			logger.L().Warning("in setContextMetadata", helpers.Interface("case", ContextGitURL), helpers.Error(err))
		}
		contextMetadata.RepoContextMetadata = context
	}
}

func metadataGitURL(input string) (*reporthandlingv2.RepoContextMetadata, error) {
	context := &reporthandlingv2.RepoContextMetadata{}
	gitParser, err := giturl.NewGitAPI(input)
	if err != nil {
		return context, fmt.Errorf("%w", err)
	}
	if gitParser.GetBranchName() == "" {
		gitParser.SetDefaultBranchName()
	}
	context.Provider = gitParser.GetProvider()
	context.Repo = gitParser.GetRepoName()
	context.Owner = gitParser.GetOwnerName()
	context.Branch = gitParser.GetBranchName()
	context.RemoteURL = gitParser.GetURL().String()

	commit, err := gitParser.GetLatestCommit()
	if err != nil {
		return context, fmt.Errorf("%w", err)
	}

	context.LastCommit = reporthandling.LastCommit{
		Hash:          commit.SHA,
		Date:          commit.Committer.Date,
		CommitterName: commit.Committer.Name,
	}

	return context, nil
}

func metadataGitLocal(input string) (*reporthandlingv2.RepoContextMetadata, error) {
	gitParser, err := NewLocalGitRepository(input)
	if err != nil {
		return nil, fmt.Errorf("%w", err)
	}
	remoteURL, err := gitParser.GetRemoteUrl()
	if err != nil {
		return nil, fmt.Errorf("%w", err)
	}
	context := &reporthandlingv2.RepoContextMetadata{}
	gitParserURL, err := giturl.NewGitURL(remoteURL)
	if err != nil {
		return context, fmt.Errorf("%w", err)
	}
	gitParserURL.SetBranchName(gitParser.GetBranchName())

	context.Provider = gitParserURL.GetProvider()
	context.Repo = gitParserURL.GetRepoName()
	context.Owner = gitParserURL.GetOwnerName()
	context.Branch = gitParserURL.GetBranchName()
	context.RemoteURL = gitParserURL.GetURL().String()

	commit, err := gitParser.GetLastCommit()
	if err != nil {
		return context, fmt.Errorf("%w", err)
	}
	context.LastCommit = reporthandling.LastCommit{
		Hash:          commit.SHA,
		Date:          commit.Committer.Date,
		CommitterName: commit.Committer.Name,
	}

	return context, nil
}
func getHostname() string {
	if h, e := os.Hostname(); e == nil {
		return h
	}
	return ""
}

func getAbsPath(p string) string {
	if !filepath.IsAbs(p) { // parse path
		if o, err := os.Getwd(); err == nil {
			return filepath.Join(o, p)
		}
	}
	return p
}

// ScanningContextToScanningScope convert the context to the deprecated scope
func ScanningContextToScanningScope(scanningContext ScanningContext) string {
	if scanningContext == ContextCluster {
		return ScopeCluster
	}
	return ScopeYAML
}
