package exporter

import (
	"fmt"
	"io"
	"sort"
	"strings"

	"github.com/zzet/gortex/internal/analysis"
	"github.com/zzet/gortex/internal/graph"
)

// MermaidOpts narrows what the Mermaid exporter renders.
type MermaidOpts struct {
	// Scope picks the diagram flavour: "architecture" (default),
	// "communities", "processes", or "all" (writes all three when
	// the caller is feeding WriteMermaidScoped via the multi-file
	// directory mode).
	Scope string
	// MaxCommunities caps how many communities to include in the
	// architecture and communities diagrams. Zero means 20.
	MaxCommunities int
	// MinCommunity drops communities below this size. Zero means 3.
	MinCommunity int
	// Kinds and Languages mirror the standard exporter Options for
	// filtering the underlying graph before community detection.
	Kinds     []graph.NodeKind
	Languages []string
}

func (o MermaidOpts) withDefaults() MermaidOpts {
	if o.Scope == "" {
		o.Scope = "architecture"
	}
	if o.MaxCommunities == 0 {
		o.MaxCommunities = 20
	}
	if o.MinCommunity == 0 {
		o.MinCommunity = 3
	}
	return o
}

// WriteMermaid emits a single Mermaid diagram for the chosen scope.
// Use this when the caller asks for one file. For multi-file output
// the CLI calls WriteMermaid once per scope into separate files.
func WriteMermaid(w io.Writer, g *graph.Graph, opts MermaidOpts) (Stats, error) {
	opts = opts.withDefaults()
	cw := &countingWriter{w: w}

	if g == nil {
		_, _ = io.WriteString(cw, "graph LR\n  empty[\"no graph\"]\n")
		return Stats{BytesWritten: cw.n}, nil
	}

	body, nodes, edges, err := renderForScope(g, opts)
	if err != nil {
		return Stats{}, err
	}
	if _, err := io.WriteString(cw, body); err != nil {
		return Stats{}, err
	}
	return Stats{NodesWritten: nodes, EdgesWritten: edges, BytesWritten: cw.n}, nil
}

// renderForScope dispatches the Scope to the right diagram builder and
// returns the rendered Mermaid plus a (nodes, edges) count that the
// caller surfaces in Stats.
func renderForScope(g *graph.Graph, opts MermaidOpts) (body string, nodes, edges int, err error) {
	switch strings.ToLower(opts.Scope) {
	case "architecture":
		body, nodes, edges = renderArchitecture(g, opts)
	case "communities":
		body, nodes, edges = renderCommunities(g, opts)
	case "processes":
		body, nodes, edges = renderProcesses(g, opts)
	case "all":
		// Emit a single document containing all three diagrams in
		// sequence, separated by `%% ---` markers. Useful for
		// quick preview without enabling --out-dir.
		var sb strings.Builder
		arch, an, ae := renderArchitecture(g, opts)
		comm, cn, ce := renderCommunities(g, opts)
		proc, pn, pe := renderProcesses(g, opts)
		sb.WriteString("%% gortex architecture diagram\n")
		sb.WriteString(arch)
		sb.WriteString("\n%% ---\n")
		sb.WriteString("%% gortex communities diagram\n")
		sb.WriteString(comm)
		sb.WriteString("\n%% ---\n")
		sb.WriteString("%% gortex processes diagram\n")
		sb.WriteString(proc)
		body = sb.String()
		nodes = an + cn + pn
		edges = ae + ce + pe
	default:
		return "", 0, 0, fmt.Errorf("unknown scope %q (expected architecture | communities | processes | all)", opts.Scope)
	}
	return body, nodes, edges, nil
}

// renderArchitecture builds a top-level community map with hub
// annotations. Mirrors the layout used by the wiki page.
func renderArchitecture(g *graph.Graph, opts MermaidOpts) (string, int, int) {
	comms := analysis.DetectCommunities(g)
	var sb strings.Builder
	sb.WriteString("graph TB\n")
	if comms == nil || len(comms.Communities) == 0 {
		sb.WriteString("  empty[\"no communities\"]\n")
		return sb.String(), 1, 0
	}
	type item struct {
		id    string
		label string
		size  int
		hub   string
	}
	var kept []item
	for _, c := range comms.Communities {
		if c.Size < opts.MinCommunity {
			continue
		}
		label := c.Label
		if label == "" {
			label = c.ID
		}
		kept = append(kept, item{id: c.ID, label: label, size: c.Size, hub: c.Hub})
	}
	sort.Slice(kept, func(i, j int) bool { return kept[i].size > kept[j].size })
	if opts.MaxCommunities > 0 && len(kept) > opts.MaxCommunities {
		kept = kept[:opts.MaxCommunities]
	}
	keepSet := make(map[string]bool, len(kept))
	for _, k := range kept {
		keepSet[k.id] = true
		hub := k.hub
		if hub == "" {
			hub = "—"
		}
		label := fmt.Sprintf("%s\\n%d symbols\\nhub: %s", k.label, k.size, hub)
		fmt.Fprintf(&sb, "  %s[\"%s\"]\n", mermaidSafeID(k.id), mermaidEscape(label))
	}
	sb.WriteString("\n")
	edges := emitCrossCommEdges(&sb, g, comms, keepSet)
	return sb.String(), len(kept), edges
}

// renderCommunities is identical to architecture today but exposes
// `graph LR` for a wider canvas. Caller picks via Scope.
func renderCommunities(g *graph.Graph, opts MermaidOpts) (string, int, int) {
	comms := analysis.DetectCommunities(g)
	var sb strings.Builder
	sb.WriteString("graph LR\n")
	if comms == nil || len(comms.Communities) == 0 {
		sb.WriteString("  empty[\"no communities\"]\n")
		return sb.String(), 1, 0
	}
	type item struct {
		id    string
		label string
		size  int
	}
	var kept []item
	for _, c := range comms.Communities {
		if c.Size < opts.MinCommunity {
			continue
		}
		label := c.Label
		if label == "" {
			label = c.ID
		}
		kept = append(kept, item{id: c.ID, label: label, size: c.Size})
	}
	sort.Slice(kept, func(i, j int) bool { return kept[i].size > kept[j].size })
	if opts.MaxCommunities > 0 && len(kept) > opts.MaxCommunities {
		kept = kept[:opts.MaxCommunities]
	}
	keepSet := make(map[string]bool, len(kept))
	for _, k := range kept {
		keepSet[k.id] = true
		fmt.Fprintf(&sb, "  %s([\"%s\\n%d\"])\n", mermaidSafeID(k.id), mermaidEscape(k.label), k.size)
	}
	sb.WriteString("\n")
	edges := emitCrossCommEdges(&sb, g, comms, keepSet)
	return sb.String(), len(kept), edges
}

// renderProcesses lists every process as a small flowchart of
// caller→callee pairs, capped to keep the rendering responsive.
func renderProcesses(g *graph.Graph, _ MermaidOpts) (string, int, int) {
	procs := analysis.DiscoverProcesses(g)
	var sb strings.Builder
	sb.WriteString("graph LR\n")
	if procs == nil || len(procs.Processes) == 0 {
		sb.WriteString("  empty[\"no processes\"]\n")
		return sb.String(), 1, 0
	}
	maxProcesses := 12
	if len(procs.Processes) < maxProcesses {
		maxProcesses = len(procs.Processes)
	}
	nodeCount, edgeCount := 0, 0
	nodeByID := make(map[string]*graph.Node)
	for _, n := range g.AllNodes() {
		nodeByID[n.ID] = n
	}
	for i := 0; i < maxProcesses; i++ {
		p := procs.Processes[i]
		fmt.Fprintf(&sb, "  subgraph %s [%s]\n", mermaidSafeID("p_"+p.ID), mermaidEscape(p.Name))
		// Walk steps in order and emit caller→callee using the
		// nearest preceding step with smaller depth.
		parentStack := make([]int, 0, len(p.Steps))
		for j, step := range p.Steps {
			for len(parentStack) > 0 {
				top := parentStack[len(parentStack)-1]
				if p.Steps[top].Depth < step.Depth {
					break
				}
				parentStack = parentStack[:len(parentStack)-1]
			}
			parent := -1
			if len(parentStack) > 0 {
				parent = parentStack[len(parentStack)-1]
			}
			parentStack = append(parentStack, j)

			name := step.ID
			if n := nodeByID[step.ID]; n != nil && n.Name != "" {
				name = n.Name
			}
			id := mermaidSafeID(step.ID + "_" + p.ID)
			fmt.Fprintf(&sb, "    %s[\"%s\"]\n", id, mermaidEscape(name))
			nodeCount++
			if parent >= 0 {
				parentID := mermaidSafeID(p.Steps[parent].ID + "_" + p.ID)
				fmt.Fprintf(&sb, "    %s --> %s\n", parentID, id)
				edgeCount++
			}
		}
		sb.WriteString("  end\n")
	}
	return sb.String(), nodeCount, edgeCount
}

// emitCrossCommEdges writes EdgeCalls between communities (filtered
// to the kept set) and returns the edge count.
func emitCrossCommEdges(sb *strings.Builder, g *graph.Graph, comms *analysis.CommunityResult, keep map[string]bool) int {
	type edge struct {
		from, to string
		count    int
	}
	edges := make(map[string]*edge)
	for _, e := range g.AllEdges() {
		if e.Kind != graph.EdgeCalls {
			continue
		}
		from := comms.NodeToComm[e.From]
		to := comms.NodeToComm[e.To]
		if from == "" || to == "" || from == to {
			continue
		}
		if !keep[from] || !keep[to] {
			continue
		}
		k := from + "→" + to
		if x, ok := edges[k]; ok {
			x.count++
		} else {
			edges[k] = &edge{from: from, to: to, count: 1}
		}
	}
	keys := make([]string, 0, len(edges))
	for k := range edges {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		ed := edges[k]
		fmt.Fprintf(sb, "  %s -->|%d| %s\n", mermaidSafeID(ed.from), ed.count, mermaidSafeID(ed.to))
	}
	return len(edges)
}

// mermaidSafeID — mirror of the one in internal/wiki/mermaid.go. Kept
// duplicated so the exporter package has no cross-coupling to wiki.
func mermaidSafeID(id string) string {
	r := strings.NewReplacer(
		"::", "_",
		"/", "_",
		".", "_",
		"-", "_",
		" ", "_",
		"<", "_",
		">", "_",
		"(", "_",
		")", "_",
		"*", "_",
		"#", "_",
		"@", "_",
		":", "_",
	)
	return r.Replace(id)
}

func mermaidEscape(s string) string {
	return strings.ReplaceAll(s, `"`, `#quot;`)
}
