package analyzer

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

	"text/template"

	"github.com/pkg/errors"
	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

	packagesFileName := "host-collectors/system/packages.json"
	if a.hostAnalyzer.CollectorName != "" {
		packagesFileName = fmt.Sprintf("host-collectors/system/%s-packages.json", a.hostAnalyzer.CollectorName)
	}

	contents, err := getCollectedFileContents(packagesFileName)
	if err != nil {
		return nil, errors.Wrap(err, "failed to get collected file")
	}

	var info collect.SystemPackagesInfo
	if err := json.Unmarshal(contents, &info); err != nil {
		return nil, errors.Wrap(err, "failed to unmarshal system packages info")
	}

	allResults := []*AnalyzeResult{}

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

		for _, outcome := range hostAnalyzer.Outcomes {
			r := AnalyzeResult{}
			when := ""

			if outcome.Fail != nil {
				r.IsFail = true
				r.Message = outcome.Fail.Message
				r.URI = outcome.Fail.URI
				when = outcome.Fail.When
			} else if outcome.Warn != nil {
				r.IsWarn = true
				r.Message = outcome.Warn.Message
				r.URI = outcome.Warn.URI
				when = outcome.Warn.When
			} else if outcome.Pass != nil {
				r.IsPass = true
				r.Message = outcome.Pass.Message
				r.URI = outcome.Pass.URI
				when = outcome.Pass.When
			} else {
				println("error: found an empty outcome in a systemPackages analyzer") // don't stop
				continue
			}

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

			if !match {
				continue
			}

			tmpl := template.New("package")

			// template the title
			titleTmpl, err := tmpl.Parse(a.Title())
			if err != nil {
				return nil, errors.Wrap(err, "failed to create new title template")
			}
			var t bytes.Buffer
			err = titleTmpl.Execute(&t, templateMap)
			if err != nil {
				return nil, errors.Wrap(err, "failed to execute title template")
			}
			r.Title = t.String()

			// template the message
			msgTmpl, err := tmpl.Parse(r.Message)
			if err != nil {
				return nil, errors.Wrap(err, "failed to create new message template")
			}
			var m bytes.Buffer
			err = msgTmpl.Execute(&m, templateMap)
			if err != nil {
				return nil, errors.Wrap(err, "failed to execute message template")
			}
			r.Message = m.String()

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

	return allResults, 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
	}

	tmpl := template.New("conditional")

	conditionalTmpl, err := tmpl.Parse(conditional)
	if err != nil {
		return false, errors.Wrap(err, "failed to create new when template")
	}

	var when bytes.Buffer
	err = conditionalTmpl.Execute(&when, templateMap)
	if err != nil {
		return false, errors.Wrap(err, "failed to execute when template")
	}

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

	return t, nil
}
