package analyzer

import (
	"encoding/json"
	"fmt"
	"slices"
	"strconv"
	"strings"

	"github.com/pkg/errors"
	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
	"github.com/replicatedhq/troubleshoot/pkg/collect"
)

// microarchs holds a list of features present in each microarchitecture.
// ref: https://gitlab.com/x86-psABIs/x86-64-ABI
// ref: https://developers.redhat.com/blog/2021/01/05/building-red-hat-enterprise-linux-9-for-the-x86-64-v2-microarchitecture-level
var microarchs = map[string][]string{
	"x86-64":    {"cmov", "cx8", "fpu", "fxsr", "mmx", "syscall", "sse", "sse2"},
	"x86-64-v2": {"cx16", "lahf_lm", "popcnt", "ssse3", "sse4_1", "sse4_2", "ssse3"},
	"x86-64-v3": {"avx", "avx2", "bmi1", "bmi2", "f16c", "fma", "lzcnt", "movbe", "xsave"},
	"x86-64-v4": {"avx512f", "avx512bw", "avx512cd", "avx512dq", "avx512vl"},
}

type AnalyzeHostCPU struct {
	hostAnalyzer *troubleshootv1beta2.CPUAnalyze
}

func (a *AnalyzeHostCPU) Title() string {
	return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Number of CPUs")
}

func (a *AnalyzeHostCPU) IsExcluded() (bool, error) {
	return isExcluded(a.hostAnalyzer.Exclude)
}

func (a *AnalyzeHostCPU) CheckCondition(when string, data []byte) (bool, error) {

	cpuInfo := collect.CPUInfo{}
	if err := json.Unmarshal(data, &cpuInfo); err != nil {
		return false, fmt.Errorf("failed to unmarshal data into CPUInfo: %v", err)
	}

	return compareHostCPUConditionalToActual(when, cpuInfo.LogicalCount, cpuInfo.PhysicalCount, cpuInfo.Flags, cpuInfo.MachineArch)

}

func (a *AnalyzeHostCPU) Analyze(
	getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
) ([]*AnalyzeResult, error) {
	result := AnalyzeResult{Title: a.Title()}

	// Use the generic function to collect both local and remote data
	collectedContents, err := retrieveCollectedContents(
		getCollectedFileContents,
		collect.HostCPUPath,     // Local path
		collect.NodeInfoBaseDir, // Remote base directory
		collect.HostCPUFileName, // Remote file name
	)
	if err != nil {
		return []*AnalyzeResult{&result}, err
	}

	results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title())
	if err != nil {
		return nil, errors.Wrap(err, "failed to analyze OS version")
	}

	return results, nil

}

func doCompareHostCPUMicroArchitecture(microarch string, flags []string) (res bool, err error) {
	specifics := make([]string, 0)
	switch microarch {
	case "x86-64-v4":
		specifics = append(specifics, microarchs["x86-64-v4"]...)
		fallthrough
	case "x86-64-v3":
		specifics = append(specifics, microarchs["x86-64-v3"]...)
		fallthrough
	case "x86-64-v2":
		specifics = append(specifics, microarchs["x86-64-v2"]...)
		fallthrough
	case "x86-64":
		specifics = append(specifics, microarchs["x86-64"]...)
	default:
		return false, errors.Errorf("troubleshoot does not yet support microarchitecture %q", microarch)
	}

	for _, flag := range specifics {
		if slices.Contains(flags, flag) {
			continue
		}
		return false, nil
	}
	return true, nil
}

func doCompareHostCPUFlags(expected string, flags []string) (res bool, err error) {
	expectedFlags := strings.Split(expected, ",")
	if len(expectedFlags) == 0 {
		return false, errors.New("expected flags cannot be empty")
	}
	for _, flag := range expectedFlags {
		if slices.Contains(flags, flag) {
			continue
		}
		return false, nil
	}
	return true, nil
}

func compareHostCPUConditionalToActual(conditional string, logicalCount int, physicalCount int, flags []string, machineArch string) (res bool, err error) {
	compareLogical := false
	comparePhysical := false
	compareUnspecified := false
	compareMachineArch := false

	comparator := ""
	desired := ""

	/* When the conditional is in the format of "logical <comparator> <desired>"
	   example: when: "count < 2"
	*/

	parts := strings.Split(conditional, " ")
	if len(parts) == 3 {
		comparator = parts[1]
		desired = parts[2]
		if strings.ToLower(parts[0]) == "logical" {
			compareLogical = true
		} else if strings.ToLower(parts[0]) == "physical" {
			comparePhysical = true
		} else if strings.ToLower(parts[0]) == "count" {
			compareUnspecified = true
		} else if strings.ToLower(parts[0]) == "machinearch" {
			compareMachineArch = true
		}
	} else if len(parts) == 2 {
		compareUnspecified = true
		comparator = parts[0]
		desired = parts[1]
	}

	// analyze if the cpu supports a specific set of features, aka as micrarchitecture.
	if strings.ToLower(comparator) == "supports" {
		return doCompareHostCPUMicroArchitecture(desired, flags)
	}

	// hasFlags allows users to query for specific flags on the CPU.
	if strings.ToLower(comparator) == "hasflags" {
		return doCompareHostCPUFlags(desired, flags)
	}

	if !compareLogical && !comparePhysical && !compareUnspecified && !compareMachineArch {
		return false, errors.New("unable to parse conditional")
	}

	if compareLogical {
		return doCompareHostCPU(comparator, desired, logicalCount)
	} else if comparePhysical {
		return doCompareHostCPU(comparator, desired, physicalCount)
	} else if compareMachineArch {
		return doCompareMachineArch(comparator, desired, machineArch)
	} else {
		actual := logicalCount
		if physicalCount > logicalCount {
			actual = physicalCount
		}

		return doCompareHostCPU(comparator, desired, actual)
	}
}

func doCompareMachineArch(operator string, desired string, actual string) (bool, error) {
	switch operator {
	case "=", "==", "===":
		return actual == desired, nil
	case "!=", "!==":
		return actual != desired, nil
	}
	return false, errors.New("unknown operator")
}

func doCompareHostCPU(operator string, desired string, actual int) (bool, error) {
	desiredInt, err := strconv.ParseInt(desired, 10, 64)
	if err != nil {
		return false, errors.Wrap(err, "failed to parse")
	}

	switch operator {
	case "<":
		return actual < int(desiredInt), nil
	case "<=":
		return actual <= int(desiredInt), nil
	case ">":
		return actual > int(desiredInt), nil
	case ">=":
		return actual >= int(desiredInt), nil
	case "=", "==", "===":
		return actual == int(desiredInt), nil
	case "!=", "!==":
		return actual != int(desiredInt), nil
	}

	return false, errors.New("unknown operator")
}
