// Copyright 2026 The OPA Authors.  All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.

package ast

import (
	"fmt"
	"strconv"
	"strings"
)

// TODO: move/rename?

// mermaidFormatter is implemented by AST nodes that can render themselves as a
// Mermaid flowchart fragment. Each call writes node and edge declarations to b
// and returns the ID of the node it emitted.
type mermaidFormatter interface {
	mermaidFormat(b *mermaidBuilder) string
}

// mermaidBuilder accumulates Mermaid flowchart lines and issues unique node IDs.
type mermaidBuilder struct {
	buf     strings.Builder
	counter int
}

func (b *mermaidBuilder) newID() string {
	b.counter++
	return fmt.Sprintf("n%d", b.counter)
}

// node writes a Mermaid node declaration and returns its ID.
// Supported shapes: "rect" (default), "round", "hex", "cyl", "stadium", "trap".
func (b *mermaidBuilder) node(shape, label string) string {
	id := b.newID()
	label = mermaidEscapeLabel(label)
	b.buf.WriteString("  ")
	b.buf.WriteString(id)
	switch shape {
	case "round":
		b.buf.WriteString("(\"")
		b.buf.WriteString(label)
		b.buf.WriteString("\")\n")
	case "hex":
		b.buf.WriteString("{{\"")
		b.buf.WriteString(label)
		b.buf.WriteString("\"}}\n")
	case "cyl":
		b.buf.WriteString("[(\"")
		b.buf.WriteString(label)
		b.buf.WriteString("\")]\n")
	case "stadium":
		b.buf.WriteString("([\"")
		b.buf.WriteString(label)
		b.buf.WriteString("\"])\n")
	case "trap":
		b.buf.WriteString("[/\"")
		b.buf.WriteString(label)
		b.buf.WriteString("\"/]\n")
	default: // "rect"
		b.buf.WriteString("[\"")
		b.buf.WriteString(label)
		b.buf.WriteString("\"]\n")
	}
	return id
}

// edge writes a plain directed edge.
func (b *mermaidBuilder) edge(from, to string) {
	b.buf.WriteString("  ")
	b.buf.WriteString(from)
	b.buf.WriteString(" --> ")
	b.buf.WriteString(to)
	b.buf.WriteString("\n")
}

// edgeLabeled writes a directed edge with a text label.
func (b *mermaidBuilder) edgeLabeled(from, to, label string) {
	b.buf.WriteString("  ")
	b.buf.WriteString(from)
	b.buf.WriteString(" -->|")
	b.buf.WriteString(label)
	b.buf.WriteString("| ")
	b.buf.WriteString(to)
	b.buf.WriteString("\n")
}

// mermaidEscapeLabel replaces characters that break Mermaid quoted node labels.
func mermaidEscapeLabel(s string) string {
	return strings.ReplaceAll(s, `"`, "#quot;")
}

// mermaidGraph returns a Mermaid flowchart string representing the structure of
// the given module.
func mermaidGraph(module *Module) string {
	b := &mermaidBuilder{}
	module.mermaidFormat(b)
	var out strings.Builder
	out.WriteString("flowchart TD\n")
	out.WriteString(b.buf.String())
	return out.String()
}

// --- Module ---

func (mod *Module) mermaidFormat(b *mermaidBuilder) string {
	id := b.node("rect", "Module")
	pkgID := mod.Package.mermaidFormat(b)
	b.edgeLabeled(id, pkgID, "package")
	for _, imp := range mod.Imports {
		impID := imp.mermaidFormat(b)
		b.edgeLabeled(id, impID, "import")
	}
	for _, rule := range mod.Rules {
		ruleID := rule.mermaidFormat(b)
		b.edgeLabeled(id, ruleID, "rule")
	}
	return id
}

// --- Package ---

func (pkg *Package) mermaidFormat(b *mermaidBuilder) string {
	return b.node("rect", "Package: "+pkg.Path.String())
}

// --- Import ---

func (imp *Import) mermaidFormat(b *mermaidBuilder) string {
	label := "Import: " + imp.Path.String()
	if imp.Alias != "" {
		label += " as " + string(imp.Alias)
	}
	return b.node("rect", label)
}

// --- Rule ---

func (rule *Rule) mermaidFormat(b *mermaidBuilder) string {
	ref := rule.Head.Ref().String()
	label := "Rule: " + ref
	if rule.Default {
		label = "Rule: default " + ref
	}
	id := b.node("rect", label)

	headID := mermaidFormatHead(rule.Head, b)
	b.edge(id, headID)

	if len(rule.Body) > 0 {
		bodyID := b.node("rect", "Body")
		b.edge(id, bodyID)
		for i, expr := range rule.Body {
			exprID := mermaidFormatExpr(expr, b)
			b.edgeLabeled(bodyID, exprID, strconv.Itoa(i))
		}
	}

	if rule.Else != nil {
		elseID := rule.Else.mermaidFormat(b)
		b.edgeLabeled(id, elseID, "else")
	}
	return id
}

func mermaidFormatHead(head *Head, b *mermaidBuilder) string {
	id := b.node("rect", "Head")

	refId := head.Ref().mermaidFormat(b)
	b.edgeLabeled(id, refId, "ref")

	for i, arg := range head.Args {
		argID := arg.mermaidFormat(b)
		b.edgeLabeled(id, argID, fmt.Sprintf(`"arg[%d]"`, i))
	}

	if head.Key != nil {
		keyID := head.Key.mermaidFormat(b)
		b.edgeLabeled(id, keyID, "key")
	}

	if head.Value != nil {
		valID := head.Value.mermaidFormat(b)
		b.edgeLabeled(id, valID, "value")
	}

	return id
}

func mermaidFormatExpr(expr *Expr, b *mermaidBuilder) string {
	label := expr.String()

	id := b.node("hex", label)

	switch terms := expr.Terms.(type) {
	case *Term:
		termID := terms.mermaidFormat(b)
		b.edge(id, termID)
	case []*Term:
		// terms[0] is the operator; remaining are arguments.
		for i, t := range terms {
			tID := t.mermaidFormat(b)
			if i == 0 {
				b.edgeLabeled(id, tID, "op")
			} else {
				b.edgeLabeled(id, tID, fmt.Sprintf(`"arg[%d]"`, i-1))
			}
		}
	case *SomeDecl:
		for _, sym := range terms.Symbols {
			symID := sym.mermaidFormat(b)
			b.edgeLabeled(id, symID, "symbol")
		}
	case *Every:
		everyID := mermaidFormatEvery(terms, b)
		b.edge(id, everyID)
	case *Not:
		notID := terms.mermaidFormat(b)
		b.edge(id, notID)
	case *LogicalAnd:
		andID := mermaidFormatLogical("and", terms.Lhs, terms.Rhs, b)
		b.edge(id, andID)
	case *LogicalOr:
		orID := mermaidFormatLogical("or", terms.Lhs, terms.Rhs, b)
		b.edge(id, orID)
	}

	for _, w := range expr.With {
		withID := mermaidFormatWith(w, b)
		b.edgeLabeled(id, withID, "with")
	}
	return id
}

func mermaidFormatEvery(every *Every, b *mermaidBuilder) string {
	id := b.node("rect", "every")
	if every.Key != nil {
		keyID := every.Key.mermaidFormat(b)
		b.edgeLabeled(id, keyID, "key")
	}
	valID := every.Value.mermaidFormat(b)
	b.edgeLabeled(id, valID, "value")
	domainID := every.Domain.mermaidFormat(b)
	b.edgeLabeled(id, domainID, "domain")
	bodyID := b.node("rect", "Body")
	b.edge(id, bodyID)
	for _, expr := range every.Body {
		exprID := mermaidFormatExpr(expr, b)
		b.edge(bodyID, exprID)
	}
	return id
}

func mermaidFormatLogical(op string, lhs, rhs Body, b *mermaidBuilder) string {
	id := b.node("rect", op)
	lhsID := b.node("rect", "Lhs")
	b.edge(id, lhsID)
	for _, expr := range lhs {
		exprID := mermaidFormatExpr(expr, b)
		b.edge(lhsID, exprID)
	}
	rhsID := b.node("rect", "Rhs")
	b.edge(id, rhsID)
	for _, expr := range rhs {
		exprID := mermaidFormatExpr(expr, b)
		b.edge(rhsID, exprID)
	}
	return id
}

func mermaidFormatWith(w *With, b *mermaidBuilder) string {
	id := b.node("rect", "with")
	targetID := w.Target.mermaidFormat(b)
	b.edgeLabeled(id, targetID, "target")
	valID := w.Value.mermaidFormat(b)
	b.edgeLabeled(id, valID, "value")
	return id
}

// --- Not ---

func (n *Not) mermaidFormat(b *mermaidBuilder) string {
	id := b.node("stadium", "not")
	for i, expr := range n.Body {
		exprID := mermaidFormatExpr(expr, b)
		b.edgeLabeled(id, exprID, strconv.Itoa(i))
	}
	return id
}

// --- Term ---

// mermaidFormat delegates to the underlying Value. Term itself does not emit a
// node; the Value determines the node shape and label.
func (term *Term) mermaidFormat(b *mermaidBuilder) string {
	switch v := term.Value.(type) {
	case mermaidFormatter:
		return v.mermaidFormat(b)
	case Set:
		return mermaidFormatSet(v, b)
	case Object:
		return mermaidFormatObject(v, b)
	default:
		return b.node("round", term.String())
	}
}

// --- Scalar Values ---

func (Null) mermaidFormat(b *mermaidBuilder) string {
	return b.node("round", "null")
}

func (bol Boolean) mermaidFormat(b *mermaidBuilder) string {
	return b.node("round", bol.String())
}

func (num Number) mermaidFormat(b *mermaidBuilder) string {
	return b.node("round", num.String())
}

func (str String) mermaidFormat(b *mermaidBuilder) string {
	return b.node("round", str.String())
}

func (v Var) mermaidFormat(b *mermaidBuilder) string {
	return b.node("round", string(v))
}

// --- Ref ---

func (ref Ref) mermaidFormat(b *mermaidBuilder) string {
	return b.node("round", "ref: "+ref.String())
}

// --- Array ---

func (arr *Array) mermaidFormat(b *mermaidBuilder) string {
	id := b.node("cyl", fmt.Sprintf("array[%d]", arr.Len()))
	arr.Foreach(func(t *Term) {
		elemID := t.mermaidFormat(b)
		b.edge(id, elemID)
	})
	return id
}

// --- Set ---

func mermaidFormatSet(s Set, b *mermaidBuilder) string {
	id := b.node("cyl", fmt.Sprintf("set{%d}", s.Len()))
	s.Foreach(func(t *Term) {
		elemID := t.mermaidFormat(b)
		b.edge(id, elemID)
	})
	return id
}

// --- Object ---

func mermaidFormatObject(o Object, b *mermaidBuilder) string {
	id := b.node("cyl", fmt.Sprintf("object{%d}", o.Len()))
	_ = o.Iter(func(k, v *Term) error {
		kvID := b.node("rect", "kv")
		b.edge(id, kvID)
		kID := k.mermaidFormat(b)
		b.edgeLabeled(kvID, kID, "key")
		vID := v.mermaidFormat(b)
		b.edgeLabeled(kvID, vID, "value")
		return nil
	})
	return id
}

// --- Comprehensions ---

func (ac *ArrayComprehension) mermaidFormat(b *mermaidBuilder) string {
	id := b.node("stadium", "Array Comprehension")
	termID := ac.Term.mermaidFormat(b)
	b.edgeLabeled(id, termID, "term")
	bodyID := b.node("rect", "Body")
	b.edge(id, bodyID)
	for i, expr := range ac.Body {
		exprID := mermaidFormatExpr(expr, b)
		b.edgeLabeled(bodyID, exprID, strconv.Itoa(i))
	}
	return id
}

func (oc *ObjectComprehension) mermaidFormat(b *mermaidBuilder) string {
	id := b.node("stadium", "Object Comprehension")
	kID := oc.Key.mermaidFormat(b)
	b.edgeLabeled(id, kID, "key")
	vID := oc.Value.mermaidFormat(b)
	b.edgeLabeled(id, vID, "value")
	bodyID := b.node("rect", "Body")
	b.edge(id, bodyID)
	for i, expr := range oc.Body {
		exprID := mermaidFormatExpr(expr, b)
		b.edgeLabeled(bodyID, exprID, strconv.Itoa(i))
	}
	return id
}

func (sc *SetComprehension) mermaidFormat(b *mermaidBuilder) string {
	id := b.node("stadium", "Set Comprehension")
	termID := sc.Term.mermaidFormat(b)
	b.edgeLabeled(id, termID, "term")
	bodyID := b.node("rect", "Body")
	b.edge(id, bodyID)
	for i, expr := range sc.Body {
		exprID := mermaidFormatExpr(expr, b)
		b.edgeLabeled(bodyID, exprID, strconv.Itoa(i))
	}
	return id
}

// --- Call ---

func (c Call) mermaidFormat(b *mermaidBuilder) string {
	opLabel := "call"
	if len(c) > 0 {
		opLabel = "call: " + c[0].String()
	}
	id := b.node("trap", opLabel)
	for i, arg := range c[1:] {
		argID := arg.mermaidFormat(b)
		b.edgeLabeled(id, argID, fmt.Sprintf(`"arg[%d]"`, i))
	}
	return id
}
