package common

import (
	contractevents "bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/contracts/events"
	"bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/contracts/graph"
)

// Builder accumulates graph nodes, edges, and code chunks for a single file.
type Builder struct {
	FilePath string
	Language string

	nodes   []graph.Node
	edges   []graph.Edge
	chunks  []contractevents.CodeChunk
	nodeSet map[string]struct{}
}

func NewBuilder(filePath, language string) *Builder {
	b := &Builder{
		FilePath: filePath,
		Language: language,
		nodeSet:  make(map[string]struct{}),
	}
	fileID := FileNodeID(filePath)
	b.AddNode(graph.Node{
		ID:       fileID,
		Kind:     graph.NodeKindFile,
		Name:     filePath,
		Language: language,
		Path:     filePath,
	})
	return b
}

func (b *Builder) FileID() string {
	return FileNodeID(b.FilePath)
}

func (b *Builder) AddNode(node graph.Node) {
	if node.Language == "" {
		node.Language = b.Language
	}
	if node.Path == "" && node.Kind == graph.NodeKindSymbol {
		node.Path = b.FilePath
	}
	if _, ok := b.nodeSet[node.ID]; ok {
		return
	}
	b.nodeSet[node.ID] = struct{}{}
	b.nodes = append(b.nodes, node)
}

func (b *Builder) AddEdge(edge graph.Edge) {
	b.edges = append(b.edges, edge)
}

func (b *Builder) AddSymbol(name string, symbolType contractevents.SymbolType, startLine, endLine int, metadata map[string]string) string {
	symbolID := SymbolNodeID(b.FilePath, name)
	meta := map[string]string{"symbol_type": string(symbolType)}
	for k, v := range metadata {
		meta[k] = v
	}
	b.AddNode(graph.Node{
		ID:       symbolID,
		Kind:     graph.NodeKindSymbol,
		Name:     name,
		Language: b.Language,
		Path:     b.FilePath,
		Metadata: meta,
	})
	b.AddEdge(graph.Edge{
		ID:       EdgeID(b.FilePath, "defines", b.FileID(), symbolID),
		Kind:     graph.EdgeKindDefines,
		SourceID: b.FileID(),
		TargetID: symbolID,
	})
	if startLine > 0 && endLine >= startLine {
		b.chunks = append(b.chunks, contractevents.CodeChunk{
			ChunkID:    ChunkID(symbolID),
			SymbolName: name,
			SymbolType: symbolType,
			StartLine:  startLine,
			EndLine:    endLine,
		})
	}
	return symbolID
}

func (b *Builder) AddImport(importPath string, line int) {
	targetID := ImportNodeID(importPath)
	b.AddNode(graph.Node{
		ID:       targetID,
		Kind:     graph.NodeKindPackage,
		Name:     importPath,
		Language: b.Language,
	})
	edgeMeta := map[string]string{}
	if line > 0 {
		edgeMeta["line"] = itoa(line)
	}
	b.AddEdge(graph.Edge{
		ID:       EdgeID(b.FilePath, "imports", b.FileID(), targetID),
		Kind:     graph.EdgeKindImports,
		SourceID: b.FileID(),
		TargetID: targetID,
		Metadata: edgeMeta,
	})
}

func (b *Builder) AddRoute(route, handler string, startLine, endLine int, metadata map[string]string) {
	routeID := RouteNodeID(b.FilePath, route)
	meta := map[string]string{"symbol_type": "route", "route": route}
	if handler != "" {
		meta["handler"] = handler
	}
	for k, v := range metadata {
		meta[k] = v
	}
	b.AddNode(graph.Node{
		ID:       routeID,
		Kind:     graph.NodeKindSymbol,
		Name:     route,
		Language: b.Language,
		Path:     b.FilePath,
		Metadata: meta,
	})
	b.AddEdge(graph.Edge{
		ID:       EdgeID(b.FilePath, "defines", b.FileID(), routeID),
		Kind:     graph.EdgeKindDefines,
		SourceID: b.FileID(),
		TargetID: routeID,
	})
	if startLine > 0 {
		b.chunks = append(b.chunks, contractevents.CodeChunk{
			ChunkID:    ChunkID(routeID),
			SymbolName: route,
			SymbolType: contractevents.SymbolTypeFunction,
			StartLine:  startLine,
			EndLine:    endLine,
		})
	}
}

func (b *Builder) AddComponent(name string, startLine, endLine int, metadata map[string]string) {
	componentID := ComponentNodeID(b.FilePath, name)
	meta := map[string]string{"symbol_type": "component", "component_type": name}
	for k, v := range metadata {
		meta[k] = v
	}
	b.AddNode(graph.Node{
		ID:       componentID,
		Kind:     graph.NodeKindSymbol,
		Name:     name,
		Language: b.Language,
		Path:     b.FilePath,
		Metadata: meta,
	})
	b.AddEdge(graph.Edge{
		ID:       EdgeID(b.FilePath, "defines", b.FileID(), componentID),
		Kind:     graph.EdgeKindDefines,
		SourceID: b.FileID(),
		TargetID: componentID,
	})
	if startLine > 0 {
		b.chunks = append(b.chunks, contractevents.CodeChunk{
			ChunkID:    ChunkID(componentID),
			SymbolName: name,
			SymbolType: contractevents.SymbolTypeType,
			StartLine:  startLine,
			EndLine:    endLine,
		})
	}
}

func (b *Builder) SetFallbackLevel(level string) {
	fileID := b.FileID()
	for i := range b.nodes {
		if b.nodes[i].ID == fileID {
			if b.nodes[i].Metadata == nil {
				b.nodes[i].Metadata = map[string]string{}
			}
			b.nodes[i].Metadata["fallback_level"] = level
			b.nodes[i].Kind = graph.NodeKindFile
			return
		}
	}
}

func (b *Builder) Artifact() *graph.Artifact {
	// Ensure file node uses NodeKindFile
	for i := range b.nodes {
		if b.nodes[i].ID == b.FileID() {
			b.nodes[i].Kind = graph.NodeKindFile
			break
		}
	}
	return &graph.Artifact{
		SchemaVersion: graph.SchemaVersionV1,
		Nodes:         b.nodes,
		Edges:         b.edges,
	}
}

func (b *Builder) Chunks() []contractevents.CodeChunk {
	return b.chunks
}

// EnsureLineWindowChunks adds overlapping line-window chunks when no symbol chunks exist.
func (b *Builder) EnsureLineWindowChunks(source []byte) {
	if len(b.chunks) > 0 {
		return
	}
	b.chunks = LineWindowChunks(source, b.FilePath, b.Language, 0, 0)
}

func (b *Builder) MergeFrameworkMetadata(framework, detectorID string) {
	for i := range b.nodes {
		if b.nodes[i].Metadata == nil {
			b.nodes[i].Metadata = map[string]string{}
		}
		if framework != "" && b.nodes[i].Metadata["framework"] == "" {
			b.nodes[i].Metadata["framework"] = framework
		}
		if detectorID != "" && b.nodes[i].Metadata["detector_id"] == "" {
			b.nodes[i].Metadata["detector_id"] = detectorID
		}
	}
}
