package plan

import (
	"fmt"
	"sort"
)

func (v *validator) validateGraph() {
	entry := v.document.Graph.Entry
	if entry.NodeID == "" {
		v.error("graph.entry_required", "graph.entry.nodeId", "entry nodeId is required", "", "")
	} else if target, ok := v.nodes[entry.NodeID]; !ok {
		v.error("graph.entry_unknown_node", "graph.entry.nodeId", fmt.Sprintf("entry references unknown node %q", entry.NodeID), "", "")
	} else {
		if target.Type == NodeJoin {
			v.error("graph.entry_join", "graph.entry.nodeId", "a join cannot be a graph entry", target.ID, "")
		}
		if target.Type == NodeLoop && entry.Port != "enter" {
			v.error("loop.invalid_entry_port", "graph.entry.port", "a loop graph entry must target its enter port", target.ID, "")
		}
		v.validateTargetPort("graph.entry.port", target, entry.Port, "")
		entrySource := &Node{ID: "$entry"}
		targetSchema := v.targetSchema(target, entry.Port)
		bindings := entry.Input
		bindingsValid := true
		if bindings == nil {
			var compatible bool
			bindings, compatible = omittedBindings(v.document.Input, "plan.input", targetSchema)
			if !compatible {
				v.error("binding.passthrough_incompatible", "graph.entry.input", "omitted entry bindings cannot pass the Plan input contract to the target input", target.ID, "")
				bindingsValid = false
			}
		}
		if bindingsValid {
			v.validateBindings("graph.entry.input", bindings, entrySource, referenceContext{}, targetSchema, target.ID, "")
		}
	}

	outgoing := map[string]map[string][]*Edge{}
	incoming := map[string]map[string][]*Edge{}
	for i := range v.document.Graph.Edges {
		edge := &v.document.Graph.Edges[i]
		path := fmt.Sprintf("graph.edges[%d]", i)
		source, sourceOK := v.nodes[edge.Source.NodeID]
		target, targetOK := v.nodes[edge.Target.NodeID]
		if !sourceOK {
			v.error("graph.unknown_source", path+".source.nodeId", fmt.Sprintf("unknown source node %q", edge.Source.NodeID), "", edge.ID)
		}
		if !targetOK {
			v.error("graph.unknown_target", path+".target.nodeId", fmt.Sprintf("unknown target node %q", edge.Target.NodeID), "", edge.ID)
		}
		if sourceOK {
			v.validateSourcePort(path+".source.port", source, edge.Source.Port, edge.ID)
			if outgoing[source.ID] == nil {
				outgoing[source.ID] = map[string][]*Edge{}
			}
			outgoing[source.ID][edge.Source.Port] = append(outgoing[source.ID][edge.Source.Port], edge)
		}
		if targetOK {
			v.validateTargetPort(path+".target.port", target, edge.Target.Port, edge.ID)
			if incoming[target.ID] == nil {
				incoming[target.ID] = map[string][]*Edge{}
			}
			incoming[target.ID][edge.Target.Port] = append(incoming[target.ID][edge.Target.Port], edge)
		}
		if sourceOK && targetOK {
			var loop *AgentTeamConfig
			if source.Type == NodeLoop {
				loop = source.Loop
			}
			targetSchema := v.targetSchema(target, edge.Target.Port)
			bindings := edge.Bindings
			bindingsValid := true
			if bindings == nil {
				sourceSchema, root, exposesPayload := sourcePayloadContract(source, edge.Source.Port)
				var compatible bool
				if exposesPayload {
					bindings, compatible = omittedBindings(sourceSchema, root, targetSchema)
				} else if len(targetSchema) == 0 {
					bindings, compatible = map[string]ValueExpr{}, true
				}
				if !compatible {
					v.error("binding.passthrough_incompatible", path+".bindings", fmt.Sprintf("omitted bindings cannot pass node %q port %q payload to the target input", source.ID, edge.Source.Port), source.ID, edge.ID)
					bindingsValid = false
				}
			}
			if bindingsValid {
				v.validateBindings(path+".bindings", bindings, source, referenceContext{loop: loop, sourcePort: edge.Source.Port}, targetSchema, source.ID, edge.ID)
			}
		}
	}

	v.validatePortCoverage(outgoing)
	v.validateJoinContracts(incoming)
	v.validateFanOutWorkItemsSources(incoming, outgoing)
	v.validateParallelScopes(outgoing, incoming)
	v.validateFanOutScopes(outgoing, incoming)
	v.validateLoopContracts(outgoing, incoming)
	v.validateLoopScopes(outgoing, incoming)
	v.validateReachability()
	v.validateEnds()
	v.validateOwnedCycles()
}

func (v *validator) validateBindings(path string, bindings map[string]ValueExpr, source *Node, references referenceContext, target map[string]Schema, nodeID, edgeID string) {
	for name, expression := range bindings {
		targetSchema, exists := target[name]
		if !exists {
			v.error("binding.unknown_target", path+"."+name, fmt.Sprintf("target input %q is not declared", name), nodeID, edgeID)
		}
		expressionSchema := v.validateValueExpr(path+"."+name, expression, source, references, edgeID)
		if exists && expression.Value != nil && !valueMatchesSchema(*expression.Value, targetSchema) {
			v.error("binding.literal_type", path+"."+name, "literal does not satisfy the target schema", nodeID, edgeID)
		} else if exists && !valueExprAssignable(expression, expressionSchema, targetSchema) {
			v.error("binding.type_mismatch", path+"."+name, fmt.Sprintf("%s cannot bind to %s", describeSchema(expressionSchema), describeSchema(targetSchema)), nodeID, edgeID)
		}
	}
	for name, schema := range target {
		required := schema.Required != nil && *schema.Required
		if required && schema.Default == nil {
			if _, exists := bindings[name]; !exists {
				v.error("binding.required_target", path+"."+name, fmt.Sprintf("required target input %q is not bound", name), nodeID, edgeID)
			}
		}
	}
}

func (v *validator) validateSourcePort(path string, node *Node, port, edgeID string) {
	ports := sourcePorts(node)
	if !contains(ports, port) {
		v.error("graph.invalid_source_port", path, fmt.Sprintf("node %q does not expose source port %q", node.ID, port), node.ID, edgeID)
	}
}

func (v *validator) validateTargetPort(path string, node *Node, port, edgeID string) {
	ports := v.targetPorts(node)
	if !contains(ports, port) {
		v.error("graph.invalid_target_port", path, fmt.Sprintf("node %q does not accept target port %q", node.ID, port), node.ID, edgeID)
	}
}

func sourcePorts(node *Node) []string {
	if node == nil {
		return nil
	}
	capability, ok := compilerAuthoringCapabilities.nodeCapability(node.Type)
	if !ok {
		return nil
	}
	if !capability.DynamicSourcePorts {
		return append([]string(nil), capability.SourcePorts...)
	}
	switch node.Type {
	case NodeSwitch:
		if node.Switch == nil {
			return nil
		}
		ports := make([]string, 0, len(node.Switch.Cases))
		for _, switchCase := range node.Switch.Cases {
			ports = append(ports, switchCase.Port)
		}
		return ports
	case NodeFork:
		if node.Fork != nil {
			return append([]string(nil), node.Fork.Branches...)
		}
	}
	return nil
}

func (v *validator) targetPorts(node *Node) []string {
	if node == nil {
		return nil
	}
	capability, ok := compilerAuthoringCapabilities.nodeCapability(node.Type)
	if !ok {
		return nil
	}
	if !capability.DynamicTargetPorts {
		return append([]string(nil), capability.TargetPorts...)
	}
	switch node.Type {
	case NodeJoin:
		if node.Join == nil {
			return nil
		}
		if node.Join.FanOutID != "" {
			return []string{"item"}
		}
		if branches, ok := v.joinOwnerBranches(node.Join.ForkID); ok {
			return branches
		}
		return nil
	}
	return nil
}

func (v *validator) targetSchema(node *Node, port string) map[string]Schema {
	if node == nil {
		return nil
	}
	switch node.Type {
	case NodeLoop:
		if node.Loop == nil {
			return nil
		}
		if port == "break" {
			return node.Loop.outputContract()
		}
		return node.Loop.State
	case NodeEnd:
		if node.End != nil && node.End.Outcome == EndSucceeded {
			return v.document.Output
		}
		return nil
	default:
		return node.Input
	}
}

func (v *validator) validatePortCoverage(outgoing map[string]map[string][]*Edge) {
	for _, node := range v.nodes {
		for _, port := range sourcePorts(node) {
			edges := outgoing[node.ID][port]
			path := fmt.Sprintf("graph.nodes[%d]", v.nodeIndexes[node.ID])
			if len(edges) == 0 {
				if usesImplicitFailureAuthoring(v.document) && isImplicitFailurePort(port) {
					continue
				}
				v.error("graph.unhandled_port", path, fmt.Sprintf("source port %q has no edge", port), node.ID, "")
			} else if len(edges) > 1 {
				v.error("graph.ambiguous_port", path, fmt.Sprintf("source port %q has %d edges; a port must have exactly one", port, len(edges)), node.ID, "")
			}
		}
		if node.Type == NodeEnd && len(outgoing[node.ID]) != 0 {
			v.error("end.has_outgoing_edge", fmt.Sprintf("graph.nodes[%d]", v.nodeIndexes[node.ID]), "end node cannot have outgoing edges", node.ID, "")
		}
	}
}

func (v *validator) validateJoinContracts(incoming map[string]map[string][]*Edge) {
	for _, node := range v.nodes {
		if node.Type != NodeJoin || node.Join == nil {
			continue
		}
		if node.Join.FanOutID != "" {
			owner := v.nodes[node.Join.FanOutID]
			path := fmt.Sprintf("graph.nodes[%d].join.fanOutId", v.nodeIndexes[node.ID])
			if owner == nil {
				v.error("join.unknown_fan_out", path, fmt.Sprintf("unknown fan_out %q", node.Join.FanOutID), node.ID, "")
				continue
			}
			if owner.Type != NodeFanOut || owner.FanOut == nil {
				v.error("join.invalid_fan_out_owner", path, fmt.Sprintf("node %q must be a fan_out", node.Join.FanOutID), node.ID, "")
				continue
			}
			if len(incoming[node.ID]["item"]) == 0 {
				v.error("join.missing_item", fmt.Sprintf("graph.nodes[%d].join", v.nodeIndexes[node.ID]), "fan_out join has no incoming item edge", node.ID, "")
			}
			continue
		}
		owner := v.nodes[node.Join.ForkID]
		path := fmt.Sprintf("graph.nodes[%d].join.forkId", v.nodeIndexes[node.ID])
		if owner == nil {
			v.error("join.unknown_fork", path, fmt.Sprintf("unknown fork %q", node.Join.ForkID), node.ID, "")
			continue
		}
		branches, ok := v.joinOwnerBranches(node.Join.ForkID)
		if !ok {
			v.error("join.invalid_fork_owner", path, fmt.Sprintf("node %q must be a fork or an all_match switch", node.Join.ForkID), node.ID, "")
			continue
		}
		for _, branch := range branches {
			if len(incoming[node.ID][branch]) == 0 {
				v.error("join.missing_branch", fmt.Sprintf("graph.nodes[%d].join", v.nodeIndexes[node.ID]), fmt.Sprintf("join has no incoming edge for fork branch %q", branch), node.ID, "")
			}
		}
	}
}

// joinOwnerBranches returns every possible lineage branch. An all_match
// switch is a dynamic fork owner: a run records only the case ports selected
// for that evaluation, and an all join waits for that activated subset.
func (v *validator) joinOwnerBranches(ownerID string) ([]string, bool) {
	owner := v.nodes[ownerID]
	if owner == nil {
		return nil, false
	}
	switch owner.Type {
	case NodeFork:
		if owner.Fork == nil {
			return nil, false
		}
		return append([]string(nil), owner.Fork.Branches...), true
	case NodeSwitch:
		if owner.Switch == nil || owner.Switch.Mode != SwitchAllMatch {
			return nil, false
		}
		branches := make([]string, 0, len(owner.Switch.Cases))
		for _, switchCase := range owner.Switch.Cases {
			branches = append(branches, switchCase.Port)
		}
		return branches, true
	default:
		return nil, false
	}
}

func (v *validator) validateLoopContracts(outgoing, incoming map[string]map[string][]*Edge) {
	for _, node := range v.nodes {
		if node.Type != NodeLoop || node.Loop == nil {
			continue
		}
		path := fmt.Sprintf("graph.nodes[%d].loop", v.nodeIndexes[node.ID])
		if len(incoming[node.ID]["enter"]) == 0 && v.document.Graph.Entry.NodeID != node.ID {
			v.error("loop.enter_required", path, "loop requires an enter edge or graph entry", node.ID, "")
		}
		if len(incoming[node.ID]["continue"]) == 0 {
			v.error("loop.continue_required", path, "loop body requires a continue edge", node.ID, "")
		}
		if len(outgoing[node.ID]["body"]) != 1 || len(outgoing[node.ID]["done"]) != 1 || len(outgoing[node.ID]["failed"]) != 1 {
			// The generic port coverage diagnostics carry the individual detail.
			continue
		}
	}
}

func (v *validator) validateReachability() {
	entry := v.document.Graph.Entry.NodeID
	if _, ok := v.nodes[entry]; !ok {
		return
	}
	adjacency := v.adjacency(false)
	reachable, queue := map[string]bool{entry: true}, []string{entry}
	for len(queue) != 0 {
		current := queue[0]
		queue = queue[1:]
		for _, next := range adjacency[current] {
			if !reachable[next] {
				reachable[next] = true
				queue = append(queue, next)
			}
		}
	}
	for id := range v.nodes {
		if !reachable[id] {
			v.error("graph.unreachable_node", fmt.Sprintf("graph.nodes[%d]", v.nodeIndexes[id]), fmt.Sprintf("node %q is unreachable from entry", id), id, "")
		}
	}
}

func (v *validator) validateEnds() {
	succeeded, failed := 0, 0
	for _, node := range v.nodes {
		if node.Type != NodeEnd || node.End == nil {
			continue
		}
		switch node.End.Outcome {
		case EndSucceeded:
			succeeded++
		case EndFailed:
			failed++
		}
	}
	if succeeded == 0 {
		v.error("graph.success_end_required", "graph.nodes", "graph requires an explicit succeeded end", "", "")
	}
	if failed == 0 && !usesImplicitFailureAuthoring(v.document) {
		v.error("graph.failure_end_required", "graph.nodes", "graph requires an explicit failed end", "", "")
	}
}

func (v *validator) validateOwnedCycles() {
	// Removing continue edges must make the graph acyclic. Therefore every
	// cycle crosses a bounded loop's continue contract and is owned by it.
	adjacency := v.adjacency(true)
	state := map[string]uint8{}
	var visit func(string) bool
	visit = func(id string) bool {
		state[id] = 1
		for _, next := range adjacency[id] {
			if state[next] == 1 {
				return true
			}
			if state[next] == 0 && visit(next) {
				return true
			}
		}
		state[id] = 2
		return false
	}
	ids := make([]string, 0, len(v.nodes))
	for id := range v.nodes {
		ids = append(ids, id)
	}
	sort.Strings(ids)
	for _, id := range ids {
		if state[id] == 0 && visit(id) {
			v.error("graph.unowned_cycle", "graph.edges", "every cycle must return through a bounded loop continue port", "", "")
			return
		}
	}
}

func (v *validator) adjacency(skipLoopContinue bool) map[string][]string {
	adjacency := map[string][]string{}
	for i := range v.document.Graph.Edges {
		edge := &v.document.Graph.Edges[i]
		if _, ok := v.nodes[edge.Source.NodeID]; !ok {
			continue
		}
		target, ok := v.nodes[edge.Target.NodeID]
		if !ok {
			continue
		}
		if skipLoopContinue && target.Type == NodeLoop && (edge.Target.Port == "continue" || edge.Target.Port == "break") {
			continue
		}
		adjacency[edge.Source.NodeID] = append(adjacency[edge.Source.NodeID], edge.Target.NodeID)
	}
	return adjacency
}

func contains(values []string, target string) bool {
	for _, value := range values {
		if value == target {
			return true
		}
	}
	return false
}
