package languages

import (
	"strings"

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

// JSON has no symbol semantics, but we still want CLAUDE.md /
// package.json / tsconfig keys to show up in the graph so they can
// be referenced and searched. Extract top-level keys of the outermost
// object as variable nodes. Nested keys are intentionally skipped.
type JSONExtractor struct{}

func NewJSONExtractor() *JSONExtractor { return &JSONExtractor{} }

func (e *JSONExtractor) Language() string     { return "json" }
func (e *JSONExtractor) Extensions() []string { return []string{".json", ".json5", ".jsonc"} }

func (e *JSONExtractor) Extract(filePath string, src []byte) (*parser.ExtractionResult, error) {
	lines := strings.Split(string(src), "\n")
	result := &parser.ExtractionResult{}

	fileNode := &graph.Node{
		ID: filePath, Kind: graph.KindFile, Name: filePath,
		FilePath: filePath, StartLine: 1, EndLine: len(lines),
		Language: "json",
	}
	result.Nodes = append(result.Nodes, fileNode)

	seen := make(map[string]bool)
	add := func(name string, start int) {
		if name == "" {
			return
		}
		id := filePath + "::" + name
		if seen[id] {
			return
		}
		seen[id] = true
		result.Nodes = append(result.Nodes, &graph.Node{
			ID: id, Kind: graph.KindVariable, Name: name,
			FilePath: filePath, StartLine: start, EndLine: start,
			Language: "json",
		})
		result.Edges = append(result.Edges, &graph.Edge{
			From: fileNode.ID, To: id, Kind: graph.EdgeDefines,
			FilePath: filePath, Line: start,
		})
	}

	// Manual scan: track brace/bracket depth while skipping strings and
	// comments (JSON5/JSONC). Emit each string key that sits at depth == 1
	// inside an object (not an array) and is followed by a `:`.
	//
	// Depth convention: incremented on `{` / `[`, decremented on `}` / `]`.
	// A key is "top-level" when the innermost open container is the
	// outermost `{` — i.e. depth transitions 0 -> 1 on `{`, and we accept
	// keys at depth == 1 while the innermost container is an object.
	var stack []byte // tracks nested container chars: '{' or '['
	line := 1
	i := 0
	n := len(src)
	for i < n {
		c := src[i]
		switch {
		case c == '\n':
			line++
			i++
		case c == '/' && i+1 < n && src[i+1] == '/':
			// Line comment (JSON5/JSONC).
			for i < n && src[i] != '\n' {
				i++
			}
		case c == '/' && i+1 < n && src[i+1] == '*':
			i += 2
			for i+1 < n && (src[i] != '*' || src[i+1] != '/') {
				if src[i] == '\n' {
					line++
				}
				i++
			}
			if i+1 < n {
				i += 2
			}
		case c == '{' || c == '[':
			stack = append(stack, c)
			i++
		case c == '}' || c == ']':
			if len(stack) > 0 {
				stack = stack[:len(stack)-1]
			}
			i++
		case c == '"':
			// String literal — capture the contents.
			keyStart := i + 1
			j := i + 1
			for j < n {
				if src[j] == '\\' && j+1 < n {
					if src[j+1] == '\n' {
						line++
					}
					j += 2
					continue
				}
				if src[j] == '"' {
					break
				}
				if src[j] == '\n' {
					line++
				}
				j++
			}
			if j >= n {
				i = n
				break
			}
			key := string(src[keyStart:j])
			// Move past closing quote.
			i = j + 1
			// Look ahead past whitespace/comments for `:` — if present at
			// depth 1 inside an object, it's a top-level key.
			k := i
			for k < n {
				if src[k] == ' ' || src[k] == '\t' || src[k] == '\r' || src[k] == '\n' {
					k++
					continue
				}
				break
			}
			if k < n && src[k] == ':' {
				// At depth 1 means stack length is exactly 1 and top is '{'.
				if len(stack) == 1 && stack[0] == '{' {
					add(key, line)
				}
			}
		default:
			i++
		}
	}

	return result, nil
}

var _ parser.Extractor = (*JSONExtractor)(nil)
