package goparser

import (
	"context"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"regexp"
	"strconv"
	"strings"

	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"
	"bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/service/parser/analyzererrors"
)

var parseErrorLinePattern = regexp.MustCompile(`:(\d+):`)

// Analyzer parses Go source files using go/parser and go/ast.
type Analyzer struct{}

func NewAnalyzer() *Analyzer {
	return &Analyzer{}
}

func (a *Analyzer) SupportedLanguage() string {
	return "go"
}

func (a *Analyzer) ParseFile(ctx context.Context, source []byte, filePath string) (*graph.Artifact, []contractevents.CodeChunk, error) {
	_ = ctx

	fset := token.NewFileSet()
	file, err := parser.ParseFile(fset, filePath, source, parser.ParseComments)
	if err != nil {
		return nil, nil, syntaxParseError(filePath, err)
	}

	builder := newGraphBuilder(fset, filePath, file)
	return builder.build(), builder.chunks, nil
}

type graphBuilder struct {
	fset     *token.FileSet
	filePath string
	file     *ast.File

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

func newGraphBuilder(fset *token.FileSet, filePath string, file *ast.File) *graphBuilder {
	b := &graphBuilder{
		fset:     fset,
		filePath: filePath,
		file:     file,
		nodeSet:  make(map[string]struct{}),
		symbols:  make(map[string]string),
	}

	fileID := fileNodeID(filePath)
	b.addNode(graph.Node{
		ID:       fileID,
		Kind:     graph.NodeKindFile,
		Name:     filePath,
		Language: "go",
		Path:     filePath,
	})

	pkgName := file.Name.Name
	pkgID := pkgNodeID(pkgName)
	b.addNode(graph.Node{
		ID:       pkgID,
		Kind:     graph.NodeKindPackage,
		Name:     pkgName,
		Language: "go",
	})
	b.addEdge(graph.Edge{
		ID:       edgeID(filePath, "contains", fileID, pkgID),
		Kind:     graph.EdgeKindContains,
		SourceID: fileID,
		TargetID: pkgID,
	})

	b.collectImports()
	b.collectDeclarations()
	b.collectCalls()

	return b
}

func (b *graphBuilder) build() *graph.Artifact {
	return &graph.Artifact{
		SchemaVersion: graph.SchemaVersionV1,
		Nodes:         b.nodes,
		Edges:         b.edges,
	}
}

func (b *graphBuilder) collectImports() {
	fileID := fileNodeID(b.filePath)
	for _, imp := range b.file.Imports {
		if imp == nil || imp.Path == nil {
			continue
		}
		importPath := strings.Trim(imp.Path.Value, `"`)
		if importPath == "" {
			continue
		}

		targetID := importNodeID(importPath)
		b.addNode(graph.Node{
			ID:       targetID,
			Kind:     graph.NodeKindPackage,
			Name:     importPath,
			Language: "go",
		})
		b.addEdge(graph.Edge{
			ID:       edgeID(b.filePath, "imports", fileID, targetID),
			Kind:     graph.EdgeKindImports,
			SourceID: fileID,
			TargetID: targetID,
		})
	}
}

func (b *graphBuilder) collectDeclarations() {
	fileID := fileNodeID(b.filePath)

	for _, decl := range b.file.Decls {
		switch typed := decl.(type) {
		case *ast.FuncDecl:
			if typed.Name == nil {
				continue
			}
			b.addFunction(typed, fileID)
		case *ast.GenDecl:
			if typed.Tok != token.TYPE {
				continue
			}
			for _, spec := range typed.Specs {
				typeSpec, ok := spec.(*ast.TypeSpec)
				if !ok || typeSpec.Name == nil {
					continue
				}
				b.addType(typeSpec, fileID)
			}
		}
	}
}

func (b *graphBuilder) collectCalls() {
	for _, decl := range b.file.Decls {
		fn, ok := decl.(*ast.FuncDecl)
		if !ok || fn.Name == nil || fn.Body == nil {
			continue
		}

		callerID := symbolNodeID(b.filePath, fn.Name.Name)
		if _, ok := b.symbols[fn.Name.Name]; !ok {
			continue
		}

		ast.Inspect(fn.Body, func(n ast.Node) bool {
			call, ok := n.(*ast.CallExpr)
			if !ok {
				return true
			}

			targetID, ok := b.callTargetID(call)
			if !ok || targetID == callerID {
				return true
			}

			line := b.fset.Position(call.Pos()).Line
			b.addEdge(graph.Edge{
				ID:       edgeID(b.filePath, fmt.Sprintf("calls@%d", line), callerID, targetID),
				Kind:     graph.EdgeKindCalls,
				SourceID: callerID,
				TargetID: targetID,
			})
			return true
		})
	}
}

func (b *graphBuilder) addFunction(fn *ast.FuncDecl, fileID string) {
	start := b.fset.Position(fn.Pos()).Line
	end := b.fset.Position(fn.End()).Line
	symbolType := contractevents.SymbolTypeFunction
	if fn.Recv != nil {
		symbolType = contractevents.SymbolTypeMethod
	}

	name := fn.Name.Name
	symbolID := symbolNodeID(b.filePath, name)
	b.symbols[name] = symbolID
	b.addNode(graph.Node{
		ID:       symbolID,
		Kind:     graph.NodeKindSymbol,
		Name:     name,
		Language: "go",
		Path:     b.filePath,
		Metadata: map[string]string{"symbol_type": string(symbolType)},
	})
	b.addEdge(graph.Edge{
		ID:       edgeID(b.filePath, "defines", fileID, symbolID),
		Kind:     graph.EdgeKindDefines,
		SourceID: fileID,
		TargetID: symbolID,
	})
	b.chunks = append(b.chunks, contractevents.CodeChunk{
		ChunkID:    chunkID(symbolID),
		SymbolName: name,
		SymbolType: symbolType,
		StartLine:  start,
		EndLine:    end,
	})
}

func (b *graphBuilder) addType(typeSpec *ast.TypeSpec, fileID string) {
	start := b.fset.Position(typeSpec.Pos()).Line
	end := b.fset.Position(typeSpec.End()).Line
	name := typeSpec.Name.Name
	symbolID := symbolNodeID(b.filePath, name)
	b.symbols[name] = symbolID

	typeKind := "type"
	switch typeSpec.Type.(type) {
	case *ast.StructType:
		typeKind = "struct"
	case *ast.InterfaceType:
		typeKind = "interface"
	}

	b.addNode(graph.Node{
		ID:       symbolID,
		Kind:     graph.NodeKindSymbol,
		Name:     name,
		Language: "go",
		Path:     b.filePath,
		Metadata: map[string]string{
			"symbol_type": string(contractevents.SymbolTypeType),
			"type_kind":   typeKind,
		},
	})
	b.addEdge(graph.Edge{
		ID:       edgeID(b.filePath, "defines", fileID, symbolID),
		Kind:     graph.EdgeKindDefines,
		SourceID: fileID,
		TargetID: symbolID,
	})
	b.chunks = append(b.chunks, contractevents.CodeChunk{
		ChunkID:    chunkID(symbolID),
		SymbolName: name,
		SymbolType: contractevents.SymbolTypeType,
		StartLine:  start,
		EndLine:    end,
	})
}

func (b *graphBuilder) callTargetID(call *ast.CallExpr) (string, bool) {
	switch fun := call.Fun.(type) {
	case *ast.Ident:
		if fun.Name == "" {
			return "", false
		}
		symbolID, ok := b.symbols[fun.Name]
		if !ok {
			return "", false
		}
		return symbolID, true
	case *ast.SelectorExpr:
		if fun.Sel == nil || fun.Sel.Name == "" {
			return "", false
		}
		if ident, ok := fun.X.(*ast.Ident); ok && ident.Name != "" {
			return externalSymbolNodeID(ident.Name, fun.Sel.Name), true
		}
	}
	return "", false
}

func (b *graphBuilder) addNode(node graph.Node) {
	if _, ok := b.nodeSet[node.ID]; ok {
		return
	}
	b.nodeSet[node.ID] = struct{}{}
	b.nodes = append(b.nodes, node)
}

func (b *graphBuilder) addEdge(edge graph.Edge) {
	b.edges = append(b.edges, edge)
}

func fileNodeID(path string) string {
	return "file:" + path
}

func pkgNodeID(name string) string {
	return "pkg:" + name
}

func importNodeID(importPath string) string {
	return "pkg:" + importPath
}

func symbolNodeID(filePath, name string) string {
	return "symbol:" + filePath + "#" + name
}

func externalSymbolNodeID(pkgName, symbolName string) string {
	return "symbol:" + pkgName + "#" + symbolName
}

func chunkID(symbolID string) string {
	return strings.ReplaceAll(symbolID, ":", "_")
}

func edgeID(filePath, kind, sourceID, targetID string) string {
	return fmt.Sprintf("edge:%s#%s#%s#%s", filePath, kind, sourceID, targetID)
}

func syntaxParseError(filePath string, err error) error {
	diag := contractevents.ParserDiagnostic{
		Code:     "syntax_error",
		Message:  err.Error(),
		Severity: contractevents.DiagnosticSeverityError,
		FilePath: filePath,
	}
	if line := parseErrorLine(err.Error()); line > 0 {
		diag.Line = line
	}
	return &analyzererrors.ParseError{
		Diagnostics: []contractevents.ParserDiagnostic{diag},
		Cause:       err,
	}
}

func parseErrorLine(message string) int {
	match := parseErrorLinePattern.FindStringSubmatch(message)
	if len(match) < 2 {
		return 0
	}
	line, err := strconv.Atoi(match[1])
	if err != nil {
		return 0
	}
	return line
}
