package sql

import (
	"sort"

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

// RebuildStats summarises a RebuildTablesFromStringRegistry pass. All
// counts are net (created — already-present). EmittersLinked counts the
// unique (caller, table) pairs that produced an EdgeQueries edge.
type RebuildStats struct {
	StringsVisited int `json:"strings_visited"`
	TablesCreated  int `json:"tables_created"`
	ColumnsCreated int `json:"columns_created"`
	QueryEdges     int `json:"query_edges_created"`
	ReadColEdges   int `json:"reads_col_edges_created"`
	WriteColEdges  int `json:"writes_col_edges_created"`
	EmittersLinked int `json:"emitters_linked"`
	Skipped        int `json:"skipped"`
}

// RebuildTablesFromStringRegistry is the short-circuit: rederive
// the KindTable / KindColumn / EdgeQueries / EdgeReadsCol /
// EdgeWritesCol layer from the KindString context="sql" registry
// already present in g, without re-parsing any source file. Idempotent
// — nodes and edges are deduped via graph.AddNode / AddEdge semantics,
// so running it twice produces the same graph.
//
// For each KindString context="sql" node:
//
//   - Re-extract tables and columns from node.Name (the verbatim
//     query) using the canonical ExtractTables / ExtractColumns
//     parsers — the same shape the source-time extractor uses.
//   - For every emitter (EdgeEmits.From → KindString), wire
//     EdgeQueries to each derived KindTable and EdgeReadsCol /
//     EdgeWritesCol to each derived KindColumn.
//
// Dialect is taken from Meta["dialect"] when present (set by the Go
// SQL extractor) and falls back to "generic". Origin on rebuilt edges
// is text_matched (same tier the source-time extractor uses for
// regex-derived SQL).
//
// Returns counts for telemetry; rebuilt edges idempotently replace
// any existing edges with the same edgeKey, so a second call after
// the first reports tablesCreated=0, emittersLinked=0.
func RebuildTablesFromStringRegistry(g *graph.Graph) RebuildStats {
	if g == nil {
		return RebuildStats{}
	}
	var stats RebuildStats
	// Snapshot the node set to avoid mutation-during-iteration; we
	// append KindTable / KindColumn nodes below.
	nodes := g.AllNodes()
	// Track per-call counts of new node/edge insertions; we infer
	// "created" by checking presence before AddNode / AddEdge.
	preExistingTables := make(map[string]struct{})
	preExistingCols := make(map[string]struct{})
	for _, n := range nodes {
		if n == nil {
			continue
		}
		switch n.Kind {
		case graph.KindTable:
			preExistingTables[n.ID] = struct{}{}
		case graph.KindColumn:
			preExistingCols[n.ID] = struct{}{}
		}
	}
	// EdgeQueries / EdgeReadsCol / EdgeWritesCol dedup tracker.
	// Seeded from the live graph so a re-run on an already-rebuilt
	// graph reports zero new edges (graph.AddEdge is idempotent by
	// edgeKey, but the stats counters live in this function so they
	// need to know what was already there).
	type edgeKey struct {
		from, to string
		kind     graph.EdgeKind
	}
	seenEdges := make(map[edgeKey]struct{})
	for _, n := range nodes {
		if n == nil {
			continue
		}
		if n.Kind != graph.KindTable && n.Kind != graph.KindColumn {
			continue
		}
		for _, e := range g.GetInEdges(n.ID) {
			if e == nil {
				continue
			}
			switch e.Kind {
			case graph.EdgeQueries, graph.EdgeReadsCol, graph.EdgeWritesCol:
				seenEdges[edgeKey{from: e.From, to: e.To, kind: e.Kind}] = struct{}{}
			}
		}
	}
	for _, n := range nodes {
		if n == nil || n.Kind != graph.KindString {
			continue
		}
		if ctx, _ := n.Meta["context"].(string); ctx != "sql" {
			continue
		}
		query := n.Name
		if query == "" {
			if v, ok := n.Meta["value"].(string); ok {
				query = v
			}
		}
		if query == "" {
			stats.Skipped++
			continue
		}
		stats.StringsVisited++
		dialect, _ := n.Meta["dialect"].(string)
		if dialect == "" {
			dialect = "generic"
		}
		tables := ExtractTables(query)
		columns := ExtractColumns(query)
		if len(tables) == 0 {
			stats.Skipped++
			continue
		}
		// Collect emitters from EdgeEmits to this KindString.
		emitters := make([]string, 0, 4)
		seenEmitter := make(map[string]struct{}, 4)
		for _, e := range g.GetInEdges(n.ID) {
			if e == nil || e.Kind != graph.EdgeEmits {
				continue
			}
			if _, dup := seenEmitter[e.From]; dup {
				continue
			}
			seenEmitter[e.From] = struct{}{}
			emitters = append(emitters, e.From)
		}
		// Stable ordering — emitter snapshots are otherwise shard-
		// order-dependent, which makes test assertions racy.
		sort.Strings(emitters)
		// Ensure KindTable nodes.
		tableNodes := make([]struct {
			id    string
			ref   TableRef
			added bool
		}, 0, len(tables))
		for _, ref := range tables {
			id := TableNodeID(dialect, ref.Schema, ref.Table)
			if _, exists := preExistingTables[id]; !exists {
				meta := map[string]any{
					"table":   ref.Table,
					"dialect": dialect,
				}
				if ref.Schema != "" {
					meta["schema"] = ref.Schema
				}
				g.AddNode(&graph.Node{
					ID:       id,
					Kind:     graph.KindTable,
					Name:     ref.Table,
					FilePath: n.FilePath,
					Language: "sql",
					Meta:     meta,
				})
				preExistingTables[id] = struct{}{}
				stats.TablesCreated++
				tableNodes = append(tableNodes, struct {
					id    string
					ref   TableRef
					added bool
				}{id, ref, true})
			} else {
				tableNodes = append(tableNodes, struct {
					id    string
					ref   TableRef
					added bool
				}{id, ref, false})
			}
		}
		// Ensure KindColumn nodes.
		colNodes := make([]struct {
			id  string
			ref ColumnRef
		}, 0, len(columns))
		for _, col := range columns {
			id := ColumnNodeID(dialect, col.Schema, col.Table, col.Column)
			if _, exists := preExistingCols[id]; !exists {
				meta := map[string]any{
					"table":   col.Table,
					"column":  col.Column,
					"dialect": dialect,
				}
				if col.Schema != "" {
					meta["schema"] = col.Schema
				}
				g.AddNode(&graph.Node{
					ID:       id,
					Kind:     graph.KindColumn,
					Name:     col.Column,
					FilePath: n.FilePath,
					Language: "sql",
					Meta:     meta,
				})
				preExistingCols[id] = struct{}{}
				stats.ColumnsCreated++
			}
			colNodes = append(colNodes, struct {
				id  string
				ref ColumnRef
			}{id, col})
		}
		// Wire emitters → tables / columns.
		for _, emitter := range emitters {
			for _, t := range tableNodes {
				k := edgeKey{from: emitter, to: t.id, kind: graph.EdgeQueries}
				if _, dup := seenEdges[k]; dup {
					continue
				}
				seenEdges[k] = struct{}{}
				g.AddEdge(&graph.Edge{
					From:     emitter,
					To:       t.id,
					Kind:     graph.EdgeQueries,
					FilePath: n.FilePath,
					Origin:   graph.OriginTextMatched,
					Meta: map[string]any{
						"op":     t.ref.Op,
						"source": "string_registry",
					},
				})
				stats.QueryEdges++
				stats.EmittersLinked++
			}
			for _, c := range colNodes {
				kind := graph.EdgeReadsCol
				if c.ref.Op == "write" {
					kind = graph.EdgeWritesCol
				}
				k := edgeKey{from: emitter, to: c.id, kind: kind}
				if _, dup := seenEdges[k]; dup {
					continue
				}
				seenEdges[k] = struct{}{}
				g.AddEdge(&graph.Edge{
					From:     emitter,
					To:       c.id,
					Kind:     kind,
					FilePath: n.FilePath,
					Origin:   graph.OriginTextMatched,
					Meta: map[string]any{
						"source": "string_registry",
					},
				})
				if kind == graph.EdgeReadsCol {
					stats.ReadColEdges++
				} else {
					stats.WriteColEdges++
				}
			}
		}
	}
	return stats
}
