package schema

import (
	"encoding/json"
	"fmt"
	"math"
	"sort"
	"strings"
)

type IssueLevel string

const (
	IssueAutoCorrected IssueLevel = "auto-corrected"
	IssueDropped       IssueLevel = "dropped"
	IssueError         IssueLevel = "issue"
	IssueFatal         IssueLevel = "fatal"
	IssueWarning       IssueLevel = "warning"
)

type GraphIssue struct {
	Level    IssueLevel `json:"level"`
	Category string     `json:"category"`
	Message  string     `json:"message"`
	Path     string     `json:"path,omitempty"`
}

type ValidationResult struct {
	Success bool            `json:"success"`
	Data    *KnowledgeGraph `json:"data,omitempty"`
	Issues  []GraphIssue    `json:"issues"`
	Fatal   string          `json:"fatal,omitempty"`
}

func ValidateGraphJSON(data []byte) ValidationResult {
	var decoded any
	if err := json.Unmarshal(data, &decoded); err != nil {
		return fatalResult("Invalid input: not valid JSON")
	}
	if _, ok := decoded.(map[string]any); !ok {
		return fatalResult("Invalid input: not an object")
	}

	var raw map[string]json.RawMessage
	if err := json.Unmarshal(data, &raw); err != nil {
		return fatalResult("Invalid input: not an object")
	}
	graph, fatal := decodeRawKnowledgeGraph(raw)
	if fatal != nil {
		return *fatal
	}
	return ValidateGraph(graph)
}

func ValidateGraph(graph KnowledgeGraph) ValidationResult {
	issues := []GraphIssue{}
	graph = SanitizeGraph(graph)
	graph = NormalizeAliases(graph)
	graph = autoFixGraph(graph, &issues)
	graph = normalizeGraphNodeIDs(graph, &issues)

	if graph.Kind != "" && graph.Kind != KindCodebase && graph.Kind != KindKnowledge {
		return validationFatal(issues, "Invalid graph kind")
	}
	if issue, ok := invalidTopLevelCollectionIssue(graph); ok {
		issues = append(issues, issue)
		return ValidationResult{Success: false, Issues: issues, Fatal: issue.Message}
	}
	if err := validateProjectMeta(graph.Project); err != nil {
		return validationFatal(issues, "Missing or invalid project metadata")
	}

	validNodes := make([]GraphNode, 0, len(graph.Nodes))
	for i, node := range graph.Nodes {
		if err := validateNode(node); err != nil {
			name := firstNonEmpty(node.Name, node.ID, fmt.Sprintf("index %d", i))
			issues = append(issues, GraphIssue{
				Level:    IssueDropped,
				Category: "invalid-node",
				Message:  fmt.Sprintf("nodes[%d] (%q): %v — removed", i, name, err),
				Path:     fmt.Sprintf("nodes[%d]", i),
			})
			continue
		}
		validNodes = append(validNodes, node)
	}
	if len(validNodes) == 0 {
		return validationFatal(issues, "No valid nodes found in knowledge graph")
	}

	nodeIDs := make(map[string]struct{}, len(validNodes))
	nodesByID := make(map[string]GraphNode, len(validNodes))
	for _, node := range validNodes {
		nodeIDs[node.ID] = struct{}{}
		nodesByID[node.ID] = node
	}

	validEdges := make([]GraphEdge, 0, len(graph.Edges))
	for i, edge := range graph.Edges {
		if err := validateEdge(edge); err != nil {
			issues = append(issues, dropIssue("invalid-edge", fmt.Sprintf("edges[%d]: %v — removed", i, err), fmt.Sprintf("edges[%d]", i)))
			continue
		}
		if _, ok := nodeIDs[edge.Source]; !ok {
			issues = append(issues, dropIssue("invalid-reference", fmt.Sprintf("edges[%d]: source %q does not exist in nodes — removed", i, edge.Source), fmt.Sprintf("edges[%d].source", i)))
			continue
		}
		if _, ok := nodeIDs[edge.Target]; !ok {
			issues = append(issues, dropIssue("invalid-reference", fmt.Sprintf("edges[%d]: target %q does not exist in nodes — removed", i, edge.Target), fmt.Sprintf("edges[%d].target", i)))
			continue
		}
		validEdges = append(validEdges, edge)
	}

	validLayers := make([]Layer, 0, len(graph.Layers))
	for i, layer := range graph.Layers {
		if err := validateLayer(layer); err != nil {
			issues = append(issues, dropIssue("invalid-layer", fmt.Sprintf("layers[%d]: %v — removed", i, err), fmt.Sprintf("layers[%d]", i)))
			continue
		}
		layer.NodeIDs = filterNodeIDs(layer.NodeIDs, nodeIDs)
		validLayers = append(validLayers, layer)
	}

	validTour := make([]TourStep, 0, len(graph.Tour))
	for i, step := range graph.Tour {
		if err := validateTourStep(step); err != nil {
			issues = append(issues, dropIssue("invalid-tour-step", fmt.Sprintf("tour[%d]: %v — removed", i, err), fmt.Sprintf("tour[%d]", i)))
			continue
		}
		step.NodeIDs = filterNodeIDs(step.NodeIDs, nodeIDs)
		validTour = append(validTour, step)
	}
	issues = append(issues, layerSemanticIssues(validLayers, nodesByID, validEdges)...)
	issues = append(issues, orphanNodeWarnings(validNodes, validEdges)...)

	if graph.Version == "" {
		graph.Version = "1.0.0"
	}
	graph.Nodes = validNodes
	graph.Edges = validEdges
	graph.Layers = validLayers
	graph.Tour = validTour
	return ValidationResult{Success: true, Data: &graph, Issues: issues}
}

// IsFileLevelNode records the graph builder file-level set: these node
// types represent top-level file artifacts that must each appear in exactly
// one architecture layer.
func IsFileLevelNode(node GraphNode) bool {
	switch node.Type {
	case "file", "config", "document", "service", "pipeline", "table", "schema", "resource", "endpoint":
		return true
	default:
		return false
	}
}

func layerSemanticIssues(layers []Layer, nodesByID map[string]GraphNode, edges []GraphEdge) []GraphIssue {
	issues := []GraphIssue{}
	contained := containedNodeIDs(edges)
	assigned := map[string][]string{}
	for i, layer := range layers {
		for j, id := range layer.NodeIDs {
			if _, ok := nodesByID[id]; !ok {
				continue
			}
			assigned[id] = append(assigned[id], layer.ID)
			if len(assigned[id]) == 2 {
				issues = append(issues, GraphIssue{
					Level:    IssueError,
					Category: "node-in-multiple-layers",
					Message:  fmt.Sprintf("node %q appears in multiple layers (%q, %q)", id, assigned[id][0], assigned[id][1]),
					Path:     fmt.Sprintf("layers[%d].nodeIds[%d]", i, j),
				})
			}
		}
	}
	for _, node := range nodesByID {
		if !IsFileLevelNode(node) {
			continue
		}
		if _, ok := contained[node.ID]; ok {
			continue
		}
		if len(assigned[node.ID]) == 0 {
			issues = append(issues, GraphIssue{
				Level:    IssueError,
				Category: "file-node-not-in-any-layer",
				Message:  fmt.Sprintf("file-level node %q is not in any layer", node.ID),
				Path:     "layers",
			})
		}
	}
	sortGraphIssues(issues)
	return issues
}

func containedNodeIDs(edges []GraphEdge) map[string]struct{} {
	contained := map[string]struct{}{}
	for _, edge := range edges {
		if edge.Type == "contains" && edge.Target != "" {
			contained[edge.Target] = struct{}{}
		}
	}
	return contained
}

func orphanNodeWarnings(nodes []GraphNode, edges []GraphEdge) []GraphIssue {
	connected := map[string]struct{}{}
	for _, edge := range edges {
		connected[edge.Source] = struct{}{}
		connected[edge.Target] = struct{}{}
	}
	issues := []GraphIssue{}
	for i, node := range nodes {
		if _, ok := connected[node.ID]; ok {
			continue
		}
		issues = append(issues, GraphIssue{
			Level:    IssueWarning,
			Category: "orphan-node",
			Message:  fmt.Sprintf("node %q has no edges (orphan)", node.ID),
			Path:     fmt.Sprintf("nodes[%d]", i),
		})
	}
	return issues
}

func sortGraphIssues(issues []GraphIssue) {
	sort.Slice(issues, func(i, j int) bool {
		if issues[i].Category != issues[j].Category {
			return issues[i].Category < issues[j].Category
		}
		if issues[i].Path != issues[j].Path {
			return issues[i].Path < issues[j].Path
		}
		return issues[i].Message < issues[j].Message
	})
}

func autoFixGraph(graph KnowledgeGraph, issues *[]GraphIssue) KnowledgeGraph {
	for i := range graph.Nodes {
		node := &graph.Nodes[i]
		name := firstNonEmpty(node.Name, node.ID, fmt.Sprintf("index %d", i))
		if strings.TrimSpace(node.Type) == "" {
			node.Type = "file"
			*issues = append(*issues, autoIssue("missing-field", fmt.Sprintf("nodes[%d] (%q): missing type — defaulted to file", i, name), fmt.Sprintf("nodes[%d].type", i)))
		}
		if strings.TrimSpace(node.ID) == "" {
			node.ID = NormalizeNodeID(*node)
			*issues = append(*issues, autoIssue("missing-field", fmt.Sprintf("nodes[%d] (%q): missing id — defaulted to %q", i, name, node.ID), fmt.Sprintf("nodes[%d].id", i)))
		} else {
			node.ID = strings.TrimSpace(node.ID)
		}
		if strings.TrimSpace(node.Complexity) == "" {
			node.Complexity = "moderate"
			*issues = append(*issues, autoIssue("missing-field", fmt.Sprintf("nodes[%d] (%q): missing complexity — defaulted to moderate", i, name), fmt.Sprintf("nodes[%d].complexity", i)))
		}
		if node.Tags == nil {
			node.Tags = []string{}
			*issues = append(*issues, autoIssue("missing-field", fmt.Sprintf("nodes[%d] (%q): missing tags — defaulted to []", i, name), fmt.Sprintf("nodes[%d].tags", i)))
		}
		if strings.TrimSpace(node.Summary) == "" {
			node.Summary = firstNonEmpty(node.Name, "No summary")
			*issues = append(*issues, autoIssue("missing-field", fmt.Sprintf("nodes[%d] (%q): missing summary — defaulted to name", i, name), fmt.Sprintf("nodes[%d].summary", i)))
		}
	}
	for i := range graph.Edges {
		edge := &graph.Edges[i]
		if strings.TrimSpace(edge.Type) == "" {
			edge.Type = "depends_on"
			*issues = append(*issues, autoIssue("missing-field", fmt.Sprintf("edges[%d]: missing type — defaulted to depends_on", i), fmt.Sprintf("edges[%d].type", i)))
		}
		if strings.TrimSpace(edge.Direction) == "" {
			edge.Direction = "forward"
			*issues = append(*issues, autoIssue("missing-field", fmt.Sprintf("edges[%d]: missing direction — defaulted to forward", i), fmt.Sprintf("edges[%d].direction", i)))
		}
		if math.IsNaN(edge.Weight) && !edge.weightInvalid {
			edge.Weight = 0.5
			*issues = append(*issues, autoIssue("missing-field", fmt.Sprintf("edges[%d]: missing weight — defaulted to 0.5", i), fmt.Sprintf("edges[%d].weight", i)))
		}
		if !edge.weightInvalid && (math.IsNaN(edge.Weight) || edge.Weight < 0 || edge.Weight > 1) {
			original := edge.Weight
			edge.Weight = math.Max(0, math.Min(1, edge.Weight))
			*issues = append(*issues, autoIssue("out-of-range", fmt.Sprintf("edges[%d]: weight %v clamped to %v", i, original, edge.Weight), fmt.Sprintf("edges[%d].weight", i)))
		}
	}
	return graph
}

func normalizeGraphNodeIDs(graph KnowledgeGraph, issues *[]GraphIssue) KnowledgeGraph {
	remap := map[string]string{}
	for i := range graph.Nodes {
		node := &graph.Nodes[i]
		oldID := strings.TrimSpace(node.ID)
		canonical := NormalizeNodeID(*node)
		if canonical == "" || oldID == canonical {
			continue
		}
		node.ID = canonical
		remap[oldID] = canonical
		*issues = append(*issues, autoIssue("node-id", fmt.Sprintf("nodes[%d]: id %q normalized to %q", i, oldID, canonical), fmt.Sprintf("nodes[%d].id", i)))
	}
	if len(remap) == 0 {
		return graph
	}
	for i := range graph.Edges {
		if target, ok := remap[graph.Edges[i].Source]; ok {
			graph.Edges[i].Source = target
		}
		if target, ok := remap[graph.Edges[i].Target]; ok {
			graph.Edges[i].Target = target
		}
	}
	for i := range graph.Layers {
		for j, id := range graph.Layers[i].NodeIDs {
			if target, ok := remap[id]; ok {
				graph.Layers[i].NodeIDs[j] = target
			}
		}
	}
	for i := range graph.Tour {
		for j, id := range graph.Tour[i].NodeIDs {
			if target, ok := remap[id]; ok {
				graph.Tour[i].NodeIDs[j] = target
			}
		}
	}
	return graph
}

func validateProjectMeta(project ProjectMeta) error {
	if project.Languages == nil || project.Frameworks == nil {
		return fmt.Errorf("languages and frameworks are required arrays")
	}
	return nil
}

func validateNode(node GraphNode) error {
	if node.shapeInvalid != "" {
		return fmt.Errorf("%s", node.shapeInvalid)
	}
	if _, ok := validNodeTypes[node.Type]; !ok {
		return fmt.Errorf("invalid node type %q", node.Type)
	}
	if _, ok := validComplexities[node.Complexity]; !ok {
		return fmt.Errorf("invalid complexity %q", node.Complexity)
	}
	if len(node.LineRange) != 0 && len(node.LineRange) != 2 {
		return fmt.Errorf("lineRange must contain exactly two numbers")
	}
	if node.DomainMeta != nil && node.DomainMeta.EntryType != "" {
		if _, ok := validEntryTypes[node.DomainMeta.EntryType]; !ok {
			return fmt.Errorf("invalid domainMeta.entryType %q", node.DomainMeta.EntryType)
		}
	}
	return nil
}

func validateEdge(edge GraphEdge) error {
	if edge.shapeInvalid != "" {
		return fmt.Errorf("%s", edge.shapeInvalid)
	}
	if edge.weightInvalid {
		return fmt.Errorf("weight must be a number")
	}
	if _, ok := validEdgeTypes[edge.Type]; !ok {
		return fmt.Errorf("invalid edge type %q", edge.Type)
	}
	if _, ok := validDirections[edge.Direction]; !ok {
		return fmt.Errorf("invalid direction %q", edge.Direction)
	}
	if math.IsNaN(edge.Weight) || edge.Weight < 0 || edge.Weight > 1 {
		return fmt.Errorf("weight must be in [0,1]")
	}
	return nil
}

func validateLayer(layer Layer) error {
	if layer.shapeInvalid != "" {
		return fmt.Errorf("%s", layer.shapeInvalid)
	}
	if layer.NodeIDs == nil {
		return fmt.Errorf("nodeIds must be an array")
	}
	return nil
}

func validateTourStep(step TourStep) error {
	if step.shapeInvalid != "" {
		return fmt.Errorf("%s", step.shapeInvalid)
	}
	if step.NodeIDs == nil {
		return fmt.Errorf("nodeIds must be an array")
	}
	return nil
}

func ValidateDiffOverlayJSON(data []byte) error {
	// Strict case-sensitive key check: graph consumers read
	// changedNodeIds/affectedNodeIds with exact casing, so a payload like
	// `affectedNodeIDs` (capital D) must be rejected even though Go's
	// encoding/json would silently match it to the struct field via
	// case-insensitive lookup. Decode against a known-key allowlist so any
	// deviation — wrong case, typo, or unknown field — is surfaced.
	var raw map[string]json.RawMessage
	if err := json.Unmarshal(data, &raw); err != nil {
		return fmt.Errorf("invalid diff-overlay JSON: %w", err)
	}
	for key := range raw {
		if key != "changedNodeIds" && key != "affectedNodeIds" {
			return fmt.Errorf("unknown or case-mismatched field %q in diff-overlay JSON (expected changedNodeIds/affectedNodeIds)", key)
		}
	}
	var overlay DiffOverlay
	if err := json.Unmarshal(data, &overlay); err != nil {
		return fmt.Errorf("invalid diff-overlay JSON: %w", err)
	}
	return ValidateDiffOverlay(overlay)
}

func ValidateDiffOverlay(overlay DiffOverlay) error {
	if overlay.ChangedNodeIDs == nil {
		return fmt.Errorf("changedNodeIds must be an array")
	}
	if overlay.AffectedNodeIDs == nil {
		return fmt.Errorf("affectedNodeIds must be an array")
	}
	return nil
}

func filterNodeIDs(ids []string, valid map[string]struct{}) []string {
	filtered := make([]string, 0, len(ids))
	for _, id := range ids {
		if _, ok := valid[id]; ok {
			filtered = append(filtered, id)
		}
	}
	return filtered
}

func fatalResult(message string) ValidationResult {
	return ValidationResult{Success: false, Issues: nil, Fatal: message}
}

func validationFatal(issues []GraphIssue, message string) ValidationResult {
	issues = append(issues, GraphIssue{Level: IssueFatal, Category: "fatal", Message: message})
	return ValidationResult{Success: false, Issues: issues, Fatal: message}
}

func autoIssue(category, message, path string) GraphIssue {
	return GraphIssue{Level: IssueAutoCorrected, Category: category, Message: message, Path: path}
}

func dropIssue(category, message, path string) GraphIssue {
	return GraphIssue{Level: IssueDropped, Category: category, Message: message, Path: path}
}

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