package variables

import (
	"bytes"
	"fmt"
	"io"
	"slices"
	"strings"

	md "github.com/nao1215/markdown"
)

// titleKind upper-cases the first letter of a Kind's string form for use as a
// section heading. Kind strings are single-word lowercase ASCII (kind.go), so
// capitalizing the leading byte is sufficient.
func titleKind(s string) string {
	if s == "" {
		return s
	}
	return strings.ToUpper(s[:1]) + s[1:]
}

// GenerateMarkdown renders the catalog as Markdown. Sections:
// alphabetical index, by Kind, matrix by TemplateKind, by LifecyclePhase.
func GenerateMarkdown() string {
	var buf bytes.Buffer
	mdoc := md.NewMarkdown(io.Writer(&buf))
	all := All()

	mdoc.H1("Workflow variables catalog")
	mdoc.PlainText("")
	mdoc.PlainTextf("Auto-generated from `util/variables` via `GenerateMarkdown()`. %d variables registered.", len(all))
	mdoc.PlainText("")
	mdoc.PlainText("**Skipped and omitted nodes:** when a step or task is skipped (its `when` evaluates false) or omitted (its dependencies never ran), it produces no real outputs. Its `outputs.parameters.<name>`, `outputs.result` and `outputs.artifacts.<name>` variables are still populated with empty placeholder values, so downstream references resolve to empty rather than leaving the workflow stuck on an unresolvable variable.")
	mdoc.PlainText("")
	mdoc.PlainText("A runnable [variables-showcase.yaml](https://raw.githubusercontent.com/argoproj/argo-workflows/main/examples/variables-showcase.yaml) workflow that exercises these variables is embedded at the end of this page.")
	mdoc.PlainText("")

	mdoc.H2("1. Alphabetical index")
	mdoc.PlainText("")
	writeFullTable(mdoc, all)

	mdoc.H2("2. Grouped by Kind")
	mdoc.PlainText("")
	writeByKind(mdoc, all)

	mdoc.H2("3. Matrix by TemplateKind")
	mdoc.PlainText("")
	mdoc.PlainText("Which variables are in scope for each template type. `•` = in scope, blank = not in scope.")
	mdoc.PlainText("")
	writeMatrix(mdoc, all)

	mdoc.H2("4. Grouped by LifecyclePhase")
	mdoc.PlainText("")
	writePhaseLegend(mdoc)
	writeByPhase(mdoc, all)

	mdoc.H2("5. Showcase workflow")
	mdoc.PlainText("")
	mdoc.PlainText("The runnable example below exercises the variables catalogued above. It is embedded at docs-build time from `examples/variables-showcase.yaml` (the same file the example tests validate and run).")
	mdoc.PlainText("")
	// Emit a pymdownx snippets directive rather than inlining the file, so the
	// example stays the single source of truth and is pulled in at site-build
	// time (no second copy to keep in sync here).
	mdoc.PlainText("```yaml title=\"examples/variables-showcase.yaml\"")
	mdoc.PlainText("--8<-- \"examples/variables-showcase.yaml\"")
	mdoc.PlainText("```")
	mdoc.PlainText("")

	_ = mdoc.Build()
	return buf.String()
}

func writeFullTable(mdoc *md.Markdown, all []*Key) {
	rows := make([][]string, len(all))
	for i, k := range all {
		rows[i] = []string{md.Code(k.template), k.kind.String(), k.valueType, joinPhases(k.phases), k.description}
	}
	table(mdoc, []string{"Key", "Kind", "Type", "Availability", "Description"}, rows)
}

func writeByKind(mdoc *md.Markdown, all []*Key) {
	groups := map[Kind][]*Key{}
	var kinds []Kind
	for _, k := range all {
		if _, ok := groups[k.kind]; !ok {
			kinds = append(kinds, k.kind)
		}
		groups[k.kind] = append(groups[k.kind], k)
	}
	slices.Sort(kinds)
	for _, kd := range kinds {
		mdoc.H3(titleKind(kd.String()))
		mdoc.PlainText("")
		rows := make([][]string, len(groups[kd]))
		for i, k := range groups[kd] {
			rows[i] = []string{md.Code(k.template), k.valueType, joinPhases(k.phases), k.description}
		}
		table(mdoc, []string{"Key", "Type", "Availability", "Description"}, rows)
	}
}

func writeMatrix(mdoc *md.Markdown, all []*Key) {
	cols := append([]TemplateKind{TmplAll}, AllTemplateKinds...)
	header := append([]string{"Key"}, kindStrings(cols)...)
	rows := make([][]string, len(all))
	for i, k := range all {
		row := make([]string, len(cols)+1)
		row[0] = md.Code(k.template)
		hasAll := slices.Contains(k.appliesTo, TmplAll)
		for j, c := range cols {
			explicit := slices.Contains(k.appliesTo, c)
			// The "any" column reports raw eligibility, not reachability.
			if c == TmplAll {
				if explicit {
					row[j+1] = "•"
				}
				continue
			}
			// appliesTo gate: either the kind is listed explicitly, or the
			// variable opts in to every kind via TmplAll.
			if !explicit && !hasAll {
				continue
			}
			// Phase-reachability gate: at least one of the variable's
			// declared phases must be reachable from inside this kind.
			// Without this check, narrow phases like PhInsideLoop or
			// PhExitHandler bleed into columns that never enter them.
			if !phasesIntersect(k.phases, ReachablePhases(c)) {
				continue
			}
			row[j+1] = "•"
		}
		rows[i] = row
	}
	table(mdoc, header, rows)
}

func phasesIntersect(a, b []LifecyclePhase) bool {
	for _, x := range a {
		if slices.Contains(b, x) {
			return true
		}
	}
	return false
}

func writePhaseLegend(mdoc *md.Markdown) {
	table(mdoc, []string{"Phase", "Meaning"}, [][]string{
		{string(PhWorkflowStart), "Globals populated once, up front, before any template runs."},
		{string(PhPreDispatch), "Immediately before a template's pod is created; pod.name / node.name / steps.name / tasks.name are set."},
		{string(PhDuringExecute), "Inside a template body; inputs.* are bound."},
		{string(PhInsideLoop), "Inside a withItems/withParam expansion; `item`, `item.<key>` are bound."},
		{string(PhInsideRetry), "Inside a retryStrategy template; retries.* are bound."},
		{string(PhAfterNodeInit), "A referenced node has been initialized by the controller (id, status, startedAt are populated — startedAt is set at node-init time, before any pod is created, for all node types including non-pod ones like Suspend / HTTP / Plugin / Steps / DAG)."},
		{string(PhAfterPodStart), "The referenced node's pod has started; ip, hostNodeName are populated (k8s-supplied; meaningless for non-pod node types)."},
		{string(PhAfterNodeComplete), "The referenced node has finished (any terminal phase); finishedAt, exitCode are populated."},
		{string(PhAfterNodeSucceeded), "The referenced node has finished with Succeeded; outputs.result, outputs.parameters.*, outputs.artifacts.* are populated."},
		{string(PhAfterLoop), "Every child of a withItems/withParam group has completed; aggregated outputs appear."},
		{string(PhExitHandler), "The onExit template runs. workflow.{status,failures,duration} are final. Any earlier-phase variable is also visible here (scope accumulates)."},
		{string(PhMetricEmission), "Inside a Prometheus metric expression. Adds duration, status, exitCode, `resourcesDuration.<resource>`, and the current node's bare outputs.result / `outputs.parameters.<name>`."},
		{string(PhCronEval), "Evaluating a CronWorkflow `spec.when` or `spec.stopStrategy.expression`. Adds cronworkflow.* variables describing the cron object's identity, labels/annotations, and run counts."},
	})
}

func writeByPhase(mdoc *md.Markdown, all []*Key) {
	phases := []LifecyclePhase{
		PhWorkflowStart, PhPreDispatch, PhDuringExecute,
		PhInsideLoop, PhInsideRetry,
		PhAfterNodeInit, PhAfterPodStart, PhAfterNodeComplete, PhAfterNodeSucceeded,
		PhAfterLoop, PhExitHandler, PhMetricEmission, PhCronEval,
	}
	for _, p := range phases {
		var keys []*Key
		for _, k := range all {
			if slices.Contains(k.phases, p) {
				keys = append(keys, k)
				continue
			}
			// Exit-handler scope accumulates: every node-ref variable whose
			// appliesTo includes TmplExitHandler is reachable from onExit,
			// even though its primary phase is the one when the underlying
			// value first becomes available (after-node-init, after-loop, …).
			if p == PhExitHandler && slices.Contains(k.appliesTo, TmplExitHandler) {
				keys = append(keys, k)
			}
		}
		if len(keys) == 0 {
			continue
		}
		mdoc.H3(fmt.Sprintf("%s (%d variables)", p, len(keys)))
		mdoc.PlainText("")
		rows := make([][]string, len(keys))
		for i, k := range keys {
			rows[i] = []string{md.Code(k.template), k.kind.String(), k.valueType}
		}
		table(mdoc, []string{"Key", "Kind", "Type"}, rows)
	}
}

func table(mdoc *md.Markdown, header []string, rows [][]string) {
	mdoc.CustomTable(md.TableSet{Header: header, Rows: rows}, md.TableOptions{AutoWrapText: false})
}

func joinPhases(ps []LifecyclePhase) string {
	s := make([]string, len(ps))
	for i, p := range ps {
		s[i] = string(p)
	}
	return strings.Join(s, ", ")
}

func kindStrings(ks []TemplateKind) []string {
	out := make([]string, len(ks))
	for i, k := range ks {
		out[i] = string(k)
	}
	return out
}
