package reviewer

import (
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder/assembler"
	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder/schema"
)

const ArtifactSchema = "agentteam-insight.graph.review.v1"

type Finding struct {
	Severity string `json:"severity"`
	Category string `json:"category"`
	Message  string `json:"message"`
	NodeID   string `json:"nodeId,omitempty"`
	Edge     string `json:"edge,omitempty"`
	Path     string `json:"path,omitempty"`
}

type Stats struct {
	Nodes                 int `json:"nodes"`
	Edges                 int `json:"edges"`
	Layers                int `json:"layers"`
	TourSteps             int `json:"tourSteps"`
	Issues                int `json:"issues"`
	Warnings              int `json:"warnings"`
	DroppedDanglingEdges  int `json:"droppedDanglingEdges"`
	DroppedInvalidNodes   int `json:"droppedInvalidNodes"`
	DefaultedTags         int `json:"defaultedTags"`
	DefaultedSummaries    int `json:"defaultedSummaries"`
	SynthesizedTestedBy   int `json:"synthesizedTestedBy"`
	TestedProductionNodes int `json:"testedProductionNodes"`
}

type Artifact struct {
	Schema       string    `json:"schema"`
	Mode         string    `json:"mode"`
	Review       bool      `json:"review"`
	GeneratedAt  string    `json:"generatedAt"`
	Issues       []Finding `json:"issues"`
	Warnings     []Finding `json:"warnings"`
	Stats        Stats     `json:"stats"`
	StrictParity []string  `json:"strictParityGaps,omitempty"`
}

type Options struct {
	InputRoot string
	Mode      string
	Review    bool
	Now       time.Time
}

// ReviewAndFix performs the deterministic graph review and safe-fix pass.
// It records Layer-3-style findings and applies only safe fixes: drop invalid
// nodes/dangling edges, default missing tags/summaries, normalize graph shape,
// and let the assembler synthesize tested_by convention links.
func ReviewAndFix(graph schema.KnowledgeGraph, options Options) (schema.KnowledgeGraph, Artifact) {
	artifact := Artifact{
		Schema:      ArtifactSchema,
		Mode:        firstNonEmpty(strings.TrimSpace(options.Mode), "full"),
		Review:      options.Review,
		GeneratedAt: reviewTime(options.Now),
		StrictParity: []string{
			"deterministic review checks only; assemble-reviewer/graph-reviewer LLM quality remains a strict-parity gap",
		},
	}

	graph = schema.NormalizeAliases(schema.SanitizeGraph(graph))
	graph, fixStats := applySafeFixes(graph, &artifact)
	appendTypePrefixFindings(graph, &artifact)
	testedByBefore := countEdges(graph.Edges, "tested_by")
	graph = assembler.NormalizeGraph(graph)
	graph = validatedGraph(graph, &artifact)
	appendQualityFindings(graph, options, &artifact)
	artifact.Stats.Nodes = len(graph.Nodes)
	artifact.Stats.Edges = len(graph.Edges)
	artifact.Stats.Layers = len(graph.Layers)
	artifact.Stats.TourSteps = len(graph.Tour)
	artifact.Stats.DroppedDanglingEdges = fixStats.DroppedDanglingEdges
	artifact.Stats.DroppedInvalidNodes = fixStats.DroppedInvalidNodes
	artifact.Stats.DefaultedTags = fixStats.DefaultedTags
	artifact.Stats.DefaultedSummaries = fixStats.DefaultedSummaries
	artifact.Stats.SynthesizedTestedBy = max(0, countEdges(graph.Edges, "tested_by")-testedByBefore)
	artifact.Stats.TestedProductionNodes = countTestedProductionNodes(graph.Nodes)
	sortFindings(artifact.Issues)
	sortFindings(artifact.Warnings)
	artifact.Stats.Issues = len(artifact.Issues)
	artifact.Stats.Warnings = len(artifact.Warnings)
	return graph, artifact
}

func appendTypePrefixFindings(graph schema.KnowledgeGraph, artifact *Artifact) {
	for _, node := range graph.Nodes {
		if nodeIDPrefix(node.ID) != "" && nodeIDPrefix(node.ID) != node.Type {
			artifact.addWarning("node-type-id-prefix", "node ID prefix does not match node type", node.ID, "", node.FilePath)
		}
	}
}

type fixStats struct {
	DroppedDanglingEdges int
	DroppedInvalidNodes  int
	DefaultedTags        int
	DefaultedSummaries   int
}

func applySafeFixes(graph schema.KnowledgeGraph, artifact *Artifact) (schema.KnowledgeGraph, fixStats) {
	stats := fixStats{}
	validTypes := make(map[string]struct{}, len(schema.NodeTypes))
	for _, typ := range schema.NodeTypes {
		validTypes[typ] = struct{}{}
	}
	nodes := make([]schema.GraphNode, 0, len(graph.Nodes))
	for _, node := range graph.Nodes {
		if _, ok := validTypes[strings.ToLower(strings.TrimSpace(node.Type))]; !ok {
			stats.DroppedInvalidNodes++
			artifact.addIssue("invalid-node-type", "dropped invalid node type "+strconvQuote(node.Type), node.ID, "", node.FilePath)
			continue
		}
		if len(node.Tags) == 0 {
			node.Tags = []string{"untagged"}
			stats.DefaultedTags++
			artifact.addWarning("missing-tags", "defaulted missing tags to [untagged]", node.ID, "", node.FilePath)
		}
		if strings.TrimSpace(node.Summary) == "" {
			node.Summary = "No summary available"
			stats.DefaultedSummaries++
			artifact.addWarning("missing-summary", "defaulted missing summary", node.ID, "", node.FilePath)
		}
		nodes = append(nodes, node)
	}
	graph.Nodes = nodes
	valid := map[string]struct{}{}
	for _, node := range graph.Nodes {
		if node.ID != "" {
			valid[node.ID] = struct{}{}
		}
	}
	edges := make([]schema.GraphEdge, 0, len(graph.Edges))
	for _, edge := range graph.Edges {
		_, sourceOK := valid[edge.Source]
		_, targetOK := valid[edge.Target]
		if !sourceOK || !targetOK {
			stats.DroppedDanglingEdges++
			artifact.addIssue("dangling-edge", "dropped dangling edge", "", edge.Source+" -> "+edge.Target+" ("+edge.Type+")", "edges")
			continue
		}
		edges = append(edges, edge)
	}
	graph.Edges = edges
	return graph, stats
}

func validatedGraph(graph schema.KnowledgeGraph, artifact *Artifact) schema.KnowledgeGraph {
	result := schema.ValidateGraph(graph)
	for _, issue := range result.Issues {
		finding := Finding{Severity: string(issue.Level), Category: issue.Category, Message: issue.Message, Path: issue.Path}
		switch issue.Level {
		case schema.IssueWarning:
			artifact.Warnings = append(artifact.Warnings, finding)
		default:
			artifact.Issues = append(artifact.Issues, finding)
		}
	}
	if result.Success && result.Data != nil {
		return *result.Data
	}
	artifact.addIssue("invalid-graph", firstNonEmpty(result.Fatal, "invalid graph"), "", "", "graph")
	return graph
}

func appendQualityFindings(graph schema.KnowledgeGraph, options Options, artifact *Artifact) {
	edgesByNode := map[string][]schema.GraphEdge{}
	for _, edge := range graph.Edges {
		if edge.Source == edge.Target && edge.Source != "" {
			artifact.addWarning("self-referencing-edge", "edge references the same node as source and target", edge.Source, edge.Source+" -> "+edge.Target+" ("+edge.Type+")", "edges")
		}
		edgesByNode[edge.Source] = append(edgesByNode[edge.Source], edge)
		edgesByNode[edge.Target] = append(edgesByNode[edge.Target], edge)
	}
	for i, layer := range graph.Layers {
		if len(layer.NodeIDs) == 0 {
			artifact.addIssue("empty-layer", "layer has zero nodeIds", layer.ID, "", layerPath(i))
		}
	}
	if len(graph.Tour) < 5 || len(graph.Tour) > 15 {
		artifact.addWarning("tour-step-count", "tour should contain between 5 and 15 steps", "", "", "tour")
	}
	for _, node := range graph.Nodes {
		if len(edgesByNode[node.ID]) == 0 {
			artifact.addWarning("orphan-node", "node has no incoming or outgoing edges", node.ID, "", node.FilePath)
		}
		if summaryDegraded(node.Summary) {
			artifact.addWarning("summary-degradation", "node summary is empty or placeholder-like", node.ID, "", node.FilePath)
		}
		if isNonCodeSemanticNode(node) && !hasNonCodeSemanticEdge(edgesByNode[node.ID]) {
			artifact.addWarning("non-code-node-missing-edge", "non-code node has no contains/defines_schema/serves/configures/deploys edge", node.ID, "", node.FilePath)
		}
		if options.InputRoot != "" && node.FilePath != "" && !fileExists(options.InputRoot, node.FilePath) {
			artifact.addWarning("missing-file", "node filePath does not exist under input root", node.ID, "", node.FilePath)
		}
	}
}

func (artifact *Artifact) addIssue(category, message, nodeID, edge, path string) {
	artifact.Issues = append(artifact.Issues, Finding{Severity: "critical", Category: category, Message: message, NodeID: nodeID, Edge: edge, Path: path})
}

func (artifact *Artifact) addWarning(category, message, nodeID, edge, path string) {
	artifact.Warnings = append(artifact.Warnings, Finding{Severity: "warning", Category: category, Message: message, NodeID: nodeID, Edge: edge, Path: path})
}

func summaryDegraded(summary string) bool {
	value := strings.ToLower(strings.TrimSpace(summary))
	return value == "" || value == "todo" || value == "tbd" || value == "no summary" || value == "no summary available"
}

func isNonCodeSemanticNode(node schema.GraphNode) bool {
	switch node.Type {
	case "service", "endpoint", "table", "schema", "resource", "step":
		return true
	default:
		return false
	}
}

func hasNonCodeSemanticEdge(edges []schema.GraphEdge) bool {
	for _, edge := range edges {
		switch edge.Type {
		case "contains", "defines_schema", "serves", "configures", "deploys":
			return true
		}
	}
	return false
}

func fileExists(root, rel string) bool {
	path := rel
	if !filepath.IsAbs(path) {
		path = filepath.Join(root, filepath.FromSlash(rel))
	}
	_, err := os.Stat(path)
	return err == nil
}

func nodeIDPrefix(id string) string {
	prefix, _, ok := strings.Cut(id, ":")
	if !ok {
		return ""
	}
	return strings.TrimSpace(prefix)
}

func layerPath(index int) string {
	return "layers[" + strconv.Itoa(index) + "]"
}

func countEdges(edges []schema.GraphEdge, typ string) int {
	count := 0
	for _, edge := range edges {
		if edge.Type == typ {
			count++
		}
	}
	return count
}

func countTestedProductionNodes(nodes []schema.GraphNode) int {
	count := 0
	for _, node := range nodes {
		for _, tag := range node.Tags {
			if tag == "tested" {
				count++
				break
			}
		}
	}
	return count
}

func max(left, right int) int {
	if left > right {
		return left
	}
	return right
}

func sortFindings(findings []Finding) {
	sort.Slice(findings, func(i, j int) bool {
		return findings[i].Category+"\x00"+findings[i].Path+"\x00"+findings[i].NodeID+"\x00"+findings[i].Message < findings[j].Category+"\x00"+findings[j].Path+"\x00"+findings[j].NodeID+"\x00"+findings[j].Message
	})
}

func reviewTime(now time.Time) string {
	if now.IsZero() {
		now = time.Now().UTC()
	}
	return now.UTC().Format(time.RFC3339)
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return strings.TrimSpace(value)
		}
	}
	return ""
}

func strconvQuote(value string) string {
	return strconv.Quote(value)
}
