package analyzer

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"unicode"

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

type AnalyzeHostSystemPackages struct {
	hostAnalyzer *troubleshootv1beta2.SystemPackagesAnalyze
}

func (a *AnalyzeHostSystemPackages) Title() string {
	return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "System Packages")
}

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

func (a *AnalyzeHostSystemPackages) Analyze(
	getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
) ([]*AnalyzeResult, error) {
	hostAnalyzer := a.hostAnalyzer
	collectorName := hostAnalyzer.CollectorName
	if collectorName == "" {
		collectorName = "packages"
	}

	localPath := fmt.Sprintf("%s/%s-packages.json", collect.NodeInfoBaseDir, collectorName)
	packagesFileName := fmt.Sprintf("%s-packages.json", collectorName)

	collectedContents, err := retrieveCollectedContents(
		getCollectedFileContents,
		localPath,
		collect.NodeInfoBaseDir,
		packagesFileName,
	)
	if err != nil {
		return []*AnalyzeResult{{Title: a.Title()}}, err
	}

	var results []*AnalyzeResult
	for _, content := range collectedContents {
		currentTitle := a.Title()
		if content.NodeName != "" {
			currentTitle = fmt.Sprintf("%s - Node %s", a.Title(), content.NodeName)
		}
		result, err := a.analyzeSingleNode(content, currentTitle)
		if err != nil {
			return nil, errors.Wrapf(err, "failed to analyze system packages for %s", currentTitle)
		}
		if result != nil {
			results = append(results, result...)
		}
	}

	return results, nil
}

func getSystemPackageTemplateMap(pkg collect.SystemPackage, osName string, osVersion string) map[string]interface{} {
	osVersionParts := strings.Split(osVersion, ".")
	osVersionMajor, _ := strconv.ParseInt(osVersionParts[0], 10, 64)

	var osVersionMinor int64
	if len(osVersionParts) > 1 {
		osVersionMinor, _ = strconv.ParseInt(osVersionParts[1], 10, 64)
	}

	m := map[string]interface{}{
		"OS":             osName,
		"OSVersion":      osVersion,
		"OSVersionMajor": osVersionMajor,
		"OSVersionMinor": osVersionMinor,
		"Name":           pkg.Name,
		"Error":          pkg.Error,
		"ExitCode":       pkg.ExitCode,
		"IsInstalled":    isSystemPackageInstalled(pkg),
	}

	for k, v := range getSystemPackageDetailsMap(pkg) {
		m[k] = v
	}

	return m
}

func isSystemPackageInstalled(pkg collect.SystemPackage) bool {
	if pkg.Error != "" {
		return false
	}
	if pkg.ExitCode != "0" {
		return false
	}
	if strings.Contains(pkg.Details, "not installed") {
		return false
	}
	if strings.Contains(pkg.Details, "No matching Packages") {
		return false
	}
	return true
}

func getSystemPackageDetailsMap(pkg collect.SystemPackage) map[string]string {
	// TODO: handle multiline values
	m := map[string]string{}

	buffer := bytes.NewBuffer([]byte(pkg.Details))
	scanner := bufio.NewScanner(buffer)

	for scanner.Scan() {
		line := scanner.Text()
		parts := strings.SplitN(line, ":", 2)

		if len(parts) != 2 {
			continue
		}

		key := parts[0]

		// key should start with an upper case
		if !unicode.IsUpper([]rune(key)[0]) {
			continue
		}

		// key shouldn't start with a space
		if unicode.IsSpace([]rune(key)[0]) {
			continue
		}

		// sanitize the key and value
		key = strings.ReplaceAll(key, " ", "")
		key = strings.ReplaceAll(key, "-", "")
		value := strings.TrimSpace(parts[1])

		if key == "" || value == "" {
			continue
		}

		m[key] = value
	}

	return m
}

func compareSystemPackagesConditionalToActual(conditional string, templateMap map[string]interface{}) (res bool, err error) {
	if conditional == "" {
		return true, nil
	}

	when, err := util.RenderTemplate(conditional, templateMap)
	if err != nil {
		return false, errors.Wrap(err, "failed to render when template")
	}

	t, err := strconv.ParseBool(when)
	if err != nil {
		return false, errors.Wrap(err, "failed to parse templated when expression as a boolean")
	}

	return t, nil
}

func (a *AnalyzeHostSystemPackages) analyzeSingleNode(content collectedContent, currentTitle string) ([]*AnalyzeResult, error) {
	hostAnalyzer := a.hostAnalyzer
	var info collect.SystemPackagesInfo
	if err := json.Unmarshal(content.Data, &info); err != nil {
		return nil, errors.Wrapf(err, "failed to unmarshal system packages info from %s", currentTitle)
	}

	allResults := []*AnalyzeResult{}

	for _, pkg := range info.Packages {
		templateMap := getSystemPackageTemplateMap(pkg, info.OS, info.OSVersion)

		for _, outcome := range hostAnalyzer.Outcomes {
			r := AnalyzeResult{}
			when := ""
			switch {
			case outcome.Fail != nil:
				r.IsFail = true
				r.Message = outcome.Fail.Message
				r.URI = outcome.Fail.URI
				when = outcome.Fail.When
			case outcome.Warn != nil:
				r.IsWarn = true
				r.Message = outcome.Warn.Message
				r.URI = outcome.Warn.URI
				when = outcome.Warn.When
			case outcome.Pass != nil:
				r.IsPass = true
				r.Message = outcome.Pass.Message
				r.URI = outcome.Pass.URI
				when = outcome.Pass.When
			default:
				return nil, errors.New("invalid outcome")
			}

			match, err := compareSystemPackagesConditionalToActual(when, templateMap)
			if err != nil {
				return nil, errors.Wrapf(err, "failed to compare %s", when)
			}

			if !match {
				continue
			}

			r.Title = currentTitle

			// template the message
			renderedMsg, err := util.RenderTemplate(r.Message, templateMap)
			if err != nil {
				return nil, errors.Wrap(err, "failed to create new message template")
			}
			r.Message = renderedMsg

			// add to results, break and check the next pod
			allResults = append(allResults, &r)
			break
		}
	}

	return allResults, nil
}
