package linter

import (
	"fmt"
	"maps"
	"strconv"
	"strings"

	"github.com/moby/buildkit/frontend/dockerfile/parser"
	"github.com/pkg/errors"
)

type Config struct {
	ExperimentalAll   bool
	ExperimentalRules []string
	ReturnAsError     bool
	SkipAll           bool
	SkipRules         []string
	Warn              LintWarnFunc
}

type Linter struct {
	CalledRules       *[]string
	ExperimentalAll   bool
	ExperimentalRules map[string]struct{}
	ReturnAsError     bool
	SkipAll           bool
	SkippedRules      map[string]struct{}
	Warn              LintWarnFunc
}

func New(config *Config) *Linter {
	toret := &Linter{
		SkippedRules:      map[string]struct{}{},
		ExperimentalRules: map[string]struct{}{},
		CalledRules:       new([]string),
		Warn:              config.Warn,
	}
	toret.SkipAll = config.SkipAll
	toret.ExperimentalAll = config.ExperimentalAll
	toret.ReturnAsError = config.ReturnAsError
	for _, rule := range config.SkipRules {
		toret.SkippedRules[rule] = struct{}{}
	}
	for _, rule := range config.ExperimentalRules {
		toret.ExperimentalRules[rule] = struct{}{}
	}
	return toret
}

func (lc *Linter) Run(rule LinterRuleI, location []parser.Range, txt ...string) {
	if lc == nil || lc.Warn == nil || rule.IsDeprecated() {
		return
	}

	rulename := rule.RuleName()
	if rule.IsExperimental() {
		_, experimentalOk := lc.ExperimentalRules[rulename]
		if !lc.ExperimentalAll && !experimentalOk {
			return
		}
	} else {
		_, skipOk := lc.SkippedRules[rulename]
		if lc.SkipAll || skipOk {
			return
		}
	}

	*lc.CalledRules = append(*lc.CalledRules, rulename)
	rule.Run(lc.Warn, location, txt...)
}

func (lc *Linter) WithMergedConfig(other *Config) *Linter {
	cloned := *lc
	if other.ExperimentalAll {
		cloned.ExperimentalAll = true
	}

	if len(other.ExperimentalRules) > 0 {
		cloned.ExperimentalRules = maps.Clone(cloned.ExperimentalRules)
		for _, rulename := range other.ExperimentalRules {
			cloned.ExperimentalRules[rulename] = struct{}{}
		}
	}

	if other.SkipAll {
		cloned.SkipAll = true
	}

	if len(other.SkipRules) > 0 {
		cloned.SkippedRules = maps.Clone(cloned.SkippedRules)
		for _, rulename := range other.SkipRules {
			cloned.SkippedRules[rulename] = struct{}{}
		}
	}
	return &cloned
}

func (lc *Linter) WithMergedConfigFromComments(comments []string) *Linter {
	if comments == nil {
		return lc
	}

	for _, comment := range comments {
		p := parser.DirectiveParser{}
		p.SetComment("")

		d, _ := p.ParseLine([]byte(comment))
		if d == nil || d.Name != "check" {
			continue
		}

		v, _, _ := strings.Cut(d.Value, " ")
		lintConfig, err := ParseLintOptions(v)
		if err != nil {
			return lc
		}
		return lc.WithMergedConfig(lintConfig)
	}
	return lc
}

func (lc *Linter) Error() error {
	if lc == nil || !lc.ReturnAsError {
		return nil
	}
	if len(*lc.CalledRules) == 0 {
		return nil
	}
	var rules []string
	uniqueRules := map[string]struct{}{}
	for _, r := range *lc.CalledRules {
		uniqueRules[r] = struct{}{}
	}
	for r := range uniqueRules {
		rules = append(rules, r)
	}
	return errors.Errorf("lint violation found for rules: %s", strings.Join(rules, ", "))
}

type LinterRuleI interface {
	RuleName() string
	Run(warn LintWarnFunc, location []parser.Range, txt ...string)
	IsDeprecated() bool
	IsExperimental() bool
}

type LinterRule[F any] struct {
	Name         string
	Description  string
	Deprecated   bool
	Experimental bool
	URL          string
	Format       F
}

func (rule *LinterRule[F]) RuleName() string {
	return rule.Name
}

func (rule *LinterRule[F]) Run(warn LintWarnFunc, location []parser.Range, txt ...string) {
	if len(txt) == 0 {
		txt = []string{rule.Description}
	}
	short := strings.Join(txt, " ")
	warn(rule.Name, rule.Description, rule.URL, short, location)
}

func (rule *LinterRule[F]) IsDeprecated() bool {
	return rule.Deprecated
}

func (rule *LinterRule[F]) IsExperimental() bool {
	return rule.Experimental
}

func LintFormatShort(rulename, msg string, line int) string {
	msg = fmt.Sprintf("%s: %s", rulename, msg)
	if line > 0 {
		msg = fmt.Sprintf("%s (line %d)", msg, line)
	}
	return msg
}

type LintWarnFunc func(rulename, description, url, fmtmsg string, location []parser.Range)

func ParseLintOptions(checkStr string) (*Config, error) {
	checkStr = strings.TrimSpace(checkStr)
	if checkStr == "" {
		return &Config{}, nil
	}

	parts := strings.SplitN(checkStr, ";", 3)
	var skipSet, experimentalSet []string
	var errorOnWarn, skipAll, experimentalAll bool
	for _, p := range parts {
		k, v, ok := strings.Cut(p, "=")
		if !ok {
			return nil, errors.Errorf("invalid check option %q", p)
		}
		k = strings.TrimSpace(k)
		switch k {
		case "skip":
			v = strings.TrimSpace(v)
			if v == "all" {
				skipAll = true
			} else {
				skipSet = strings.Split(v, ",")
				for i, rule := range skipSet {
					skipSet[i] = strings.TrimSpace(rule)
				}
			}
		case "experimental":
			v = strings.TrimSpace(v)
			if v == "all" {
				experimentalAll = true
			} else {
				experimentalSet = strings.Split(v, ",")
				for i, rule := range experimentalSet {
					experimentalSet[i] = strings.TrimSpace(rule)
				}
			}
		case "error":
			v, err := strconv.ParseBool(strings.TrimSpace(v))
			if err != nil {
				return nil, errors.Wrapf(err, "failed to parse check option %q", p)
			}
			errorOnWarn = v
		default:
			return nil, errors.Errorf("invalid check option %q", k)
		}
	}
	return &Config{
		ExperimentalAll:   experimentalAll,
		ExperimentalRules: experimentalSet,
		SkipRules:         skipSet,
		SkipAll:           skipAll,
		ReturnAsError:     errorOnWarn,
	}, nil
}
