package checks

import (
	"cmp"
	"context"
	"fmt"
	"slices"
	"strconv"
	"strings"

	"github.com/cloudflare/pint/internal/diags"
	"github.com/cloudflare/pint/internal/discovery"
	"github.com/cloudflare/pint/internal/parser/source"
	"github.com/cloudflare/pint/internal/parser/utils"

	"github.com/prometheus/prometheus/model/labels"
	promParser "github.com/prometheus/prometheus/promql/parser"
)

const (
	RuleDependencyCheckName = "rule/dependency"
)

func NewRuleDependencyCheck() RuleDependencyCheck {
	return RuleDependencyCheck{}
}

type RuleDependencyCheck struct{}

func (c RuleDependencyCheck) Meta() CheckMeta {
	return CheckMeta{
		States: []discovery.ChangeType{
			discovery.Noop,
			discovery.Added,
			discovery.Modified,
			discovery.Removed,
		},
		Online:        false,
		AlwaysEnabled: false,
	}
}

func (c RuleDependencyCheck) String() string {
	return RuleDependencyCheckName
}

func (c RuleDependencyCheck) Reporter() string {
	return RuleDependencyCheckName
}

func (c RuleDependencyCheck) Check(_ context.Context, entry *discovery.Entry, entries []*discovery.Entry) (problems []Problem) {
	problems = append(problems, c.checkRemovedDependency(entry, entries)...)
	problems = append(problems, c.checkCrossGroupDependency(entry, entries)...)
	return problems
}

func (c RuleDependencyCheck) checkRemovedDependency(entry *discovery.Entry, entries []*discovery.Entry) (problems []Problem) {
	if entry.Path.Name != entry.Path.SymlinkTarget {
		// Don't reported symlinks that are being removed.
		return problems
	}

	filtered := nonRemovedEntries(entries)

	for _, flt := range filtered {
		if flt.Rule.Type() == entry.Rule.Type() && flt.Rule.Name() == entry.Rule.Name() {
			// There's another rule with same type & name, do nothing.
			return problems
		}
	}

	var broken []*brokenDependency
	var dep *brokenDependency
	for _, flt := range filtered {
		if entry.Rule.RecordingRule != nil {
			dep = c.usesVector(flt, entry.Rule.RecordingRule.Record.Value)
		}
		if entry.Rule.AlertingRule != nil {
			dep = c.usesAlert(flt, entry.Rule.AlertingRule.Alert.Value)
		}
		if dep != nil {
			var found bool
			for _, b := range broken {
				if b.kind == dep.kind && b.path == dep.path && b.line == dep.line && b.name == dep.name {
					found = true
					break
				}
			}
			if !found {
				broken = append(broken, dep)
			}
		}
	}

	if len(broken) == 0 {
		return problems
	}

	slices.SortStableFunc(broken, func(a, b *brokenDependency) int {
		return cmp.Or(
			cmp.Compare(a.path, b.path),
			cmp.Compare(a.line, b.line),
			cmp.Compare(a.name, b.name),
		)
	})

	var details strings.Builder
	details.WriteString("If you remove the ")
	details.WriteString(broken[0].kind)
	details.WriteString(" rule generating `")
	details.WriteString(broken[0].metric)
	details.WriteString("`, and there is no other source of this metric, then any other rule depending on it will break.\n")
	details.WriteString("List of found rules that are using `")
	details.WriteString(broken[0].metric)
	details.WriteString("`:\n\n")
	for _, b := range broken {
		details.WriteString("- `")
		details.WriteString(b.name)
		details.WriteString("` at `")
		details.WriteString(b.path)
		details.WriteRune(':')
		details.WriteString(strconv.Itoa(b.line))
		details.WriteString("`\n")
	}

	name := entry.Rule.NameNode()
	problems = append(problems, Problem{
		Anchor:   AnchorBefore,
		Lines:    name.Pos.Lines(),
		Reporter: c.Reporter(),
		Summary:  "rule results used by another rule",
		Details:  details.String(),
		Severity: Warning,
		Diagnostics: []diags.Diagnostic{
			{
				Message:     fmt.Sprintf("Metric generated by this rule is used by %d other rule(s).", len(broken)),
				Pos:         name.Pos,
				FirstColumn: 1,
				LastColumn:  len(name.Value),
				Kind:        diags.Issue,
			},
		},
	})

	return problems
}

func (c RuleDependencyCheck) checkCrossGroupDependency(entry *discovery.Entry, entries []*discovery.Entry) (problems []Problem) {
	expr := entry.Rule.Expr()
	if expr.SyntaxError() != nil {
		return problems
	}

	filtered := nonRemovedEntries(entries)

	var deps []*crossGroupDep
	for _, src := range expr.Source() {
		src.WalkSources(func(s source.Source, _ *source.Join, _ *source.Unless) {
			vs, ok := source.MostOuterOperation[*promParser.VectorSelector](s)
			if !ok {
				return
			}
			for _, other := range filtered {
				if other.Rule.RecordingRule == nil {
					continue
				}
				if other.Rule.RecordingRule.Record.Value != vs.Name {
					continue
				}
				if entry.Group != nil && other.Group != nil && entry.Group.Name.Value == other.Group.Name.Value {
					continue
				}
				deps = append(deps, &crossGroupDep{
					metric: vs.Name,
					group:  other.Group.Name.Value,
					path:   other.Path.SymlinkTarget,
					line:   other.Rule.RecordingRule.Record.Pos.Lines().First,
				})
			}
		})
	}

	if len(deps) == 0 {
		return problems
	}

	slices.SortStableFunc(deps, func(a, b *crossGroupDep) int {
		return cmp.Or(
			cmp.Compare(a.path, b.path),
			cmp.Compare(a.line, b.line),
			cmp.Compare(a.metric, b.metric),
		)
	})

	for _, dep := range deps {
		var details strings.Builder
		details.WriteString("This rule uses `")
		details.WriteString(dep.metric)
		details.WriteString("` which is generated by a recording rule in the `")
		details.WriteString(dep.group)
		details.WriteString("` group at `")
		details.WriteString(dep.path)
		details.WriteRune(':')
		details.WriteString(strconv.Itoa(dep.line))
		details.WriteString("`.\n")
		details.WriteString("Rules in different groups are evaluated independently. ")
		details.WriteString("This rule may use data from the previous evaluation cycle, introducing up to one evaluation interval of lag. ")
		details.WriteString("Moving both rules to the same group ensures they are evaluated together with consistent data.")

		problems = append(problems, Problem{
			Anchor:   AnchorAfter,
			Lines:    expr.Value.Pos.Lines(),
			Reporter: c.Reporter(),
			Summary:  "rule uses metric produced by a recording rule from a different group",
			Details:  details.String(),
			Severity: Warning,
			Diagnostics: []diags.Diagnostic{
				{
					Message:     fmt.Sprintf("Rule uses `%s` which is generated by a recording rule in a different group `%s`.", dep.metric, dep.group),
					Pos:         expr.Value.Pos,
					FirstColumn: 1,
					LastColumn:  len(expr.Value.Value),
					Kind:        diags.Issue,
				},
			},
		})
	}

	return problems
}

func (c RuleDependencyCheck) usesVector(entry *discovery.Entry, name string) *brokenDependency {
	expr := entry.Rule.Expr()
	if expr.SyntaxError() != nil {
		return nil
	}

	for _, vs := range utils.HasVectorSelector(expr.Query()) {
		if vs.Name == name {
			return &brokenDependency{
				kind:   "recording",
				metric: name,
				path:   entry.Path.SymlinkTarget,
				line:   expr.Value.Pos.Lines().First,
				name:   entry.Rule.Name(),
			}
		}
	}

	return nil
}

func (c RuleDependencyCheck) usesAlert(entry *discovery.Entry, name string) *brokenDependency {
	expr := entry.Rule.Expr()
	if expr.SyntaxError() != nil {
		return nil
	}

	for _, vs := range utils.HasVectorSelector(expr.Query()) {
		if vs.Name != "ALERTS" && vs.Name != "ALERTS_FOR_STATE" {
			continue
		}
		for _, lm := range vs.LabelMatchers {
			if lm.Name == "alertname" && lm.Type == labels.MatchEqual && lm.Value == name {
				return &brokenDependency{
					kind:   "alerting",
					metric: vs.String(),
					path:   entry.Path.SymlinkTarget,
					line:   expr.Value.Pos.Lines().First,
					name:   entry.Rule.Name(),
				}
			}
		}
	}

	return nil
}

type brokenDependency struct {
	kind   string
	metric string
	path   string
	name   string
	line   int
}

type crossGroupDep struct {
	metric string
	group  string
	path   string
	line   int
}

func nonRemovedEntries(src []*discovery.Entry) (dst []*discovery.Entry) {
	for _, entry := range src {
		if entry.State == discovery.Removed {
			continue
		}
		if entry.PathError != nil {
			continue
		}
		if entry.Rule.Error.Err != nil {
			continue
		}
		dst = append(dst, entry)
	}
	return dst
}
