// Package run executes supported immutable loop.plan.artifact graphs. It is a
// pure kernel: this package owns no database handle, clock, queue, or network
// client. Callers persist State and deliver the returned Intents.
package run

import (
	"fmt"
	"sort"

	"github.com/tidbcloud/AgentTeam/internal/workflow/plan"
)

// Graph is a validated artifact plus deterministic runtime indexes.  The
// artifact is treated as immutable after construction.
type Graph struct {
	Artifact *plan.Artifact

	nodes          map[string]*plan.ArtifactNode
	edges          map[string]*plan.ArtifactEdge
	outgoing       map[endpoint][]*plan.ArtifactEdge
	scopes         map[scopeKey]*plan.ArtifactScope
	parallelByJoin map[string]*plan.ArtifactScope
	fanOutByJoin   map[string]*plan.ArtifactScope
}

type endpoint struct {
	node string
	port string
}

type scopeKey struct {
	kind  plan.ArtifactScopeKind
	owner string
}

// DecodeArtifact strictly decodes, verifies, and indexes one immutable Plan
// artifact.  Source/YAML is deliberately not accepted by the runtime.
func DecodeArtifact(raw []byte) (*Graph, error) {
	artifact, err := plan.DecodeArtifact(raw)
	if err != nil {
		return nil, err
	}
	return IndexArtifact(artifact)
}

// IndexArtifact builds runtime indexes for an already decoded artifact.
func IndexArtifact(artifact *plan.Artifact) (*Graph, error) {
	if err := plan.ValidateEnvelope(artifact); err != nil {
		return nil, fmt.Errorf("validate Run artifact: %w", err)
	}
	graph := &Graph{
		Artifact:       artifact,
		nodes:          make(map[string]*plan.ArtifactNode, len(artifact.Nodes)),
		edges:          make(map[string]*plan.ArtifactEdge, len(artifact.Edges)),
		outgoing:       make(map[endpoint][]*plan.ArtifactEdge),
		scopes:         make(map[scopeKey]*plan.ArtifactScope, len(artifact.Scopes)),
		parallelByJoin: make(map[string]*plan.ArtifactScope),
		fanOutByJoin:   make(map[string]*plan.ArtifactScope),
	}
	for index := range artifact.Nodes {
		node := &artifact.Nodes[index]
		graph.nodes[node.ID] = node
	}
	for index := range artifact.Edges {
		edge := &artifact.Edges[index]
		graph.edges[edge.ID] = edge
		key := endpoint{node: edge.Source.NodeID, port: edge.Source.Port}
		graph.outgoing[key] = append(graph.outgoing[key], edge)
	}
	for key := range graph.outgoing {
		sort.Slice(graph.outgoing[key], func(left, right int) bool {
			return graph.outgoing[key][left].ID < graph.outgoing[key][right].ID
		})
	}
	for index := range artifact.Scopes {
		scope := &artifact.Scopes[index]
		key := scopeKey{kind: scope.Kind, owner: scope.OwnerID}
		if _, exists := graph.scopes[key]; exists {
			return nil, fmt.Errorf("duplicate %s scope owner %q", scope.Kind, scope.OwnerID)
		}
		graph.scopes[key] = scope
		if scope.Parallel != nil {
			if _, exists := graph.parallelByJoin[scope.Parallel.JoinID]; exists {
				return nil, fmt.Errorf("parallel scopes share join %q", scope.Parallel.JoinID)
			}
			graph.parallelByJoin[scope.Parallel.JoinID] = scope
		}
		if scope.FanOut != nil {
			if _, exists := graph.fanOutByJoin[scope.FanOut.JoinID]; exists {
				return nil, fmt.Errorf("fan-out scopes share join %q", scope.FanOut.JoinID)
			}
			graph.fanOutByJoin[scope.FanOut.JoinID] = scope
		}
	}
	return graph, nil
}

func (graph *Graph) Node(id string) (*plan.ArtifactNode, bool) {
	if graph == nil {
		return nil, false
	}
	node, ok := graph.nodes[id]
	return node, ok
}

func (graph *Graph) Edge(id string) (*plan.ArtifactEdge, bool) {
	if graph == nil {
		return nil, false
	}
	edge, ok := graph.edges[id]
	return edge, ok
}

// Outgoing returns edges in stable edge-ID order.
func (graph *Graph) Outgoing(nodeID, port string) []*plan.ArtifactEdge {
	if graph == nil {
		return nil
	}
	return append([]*plan.ArtifactEdge(nil), graph.outgoing[endpoint{node: nodeID, port: port}]...)
}

func (graph *Graph) Scope(kind plan.ArtifactScopeKind, ownerID string) (*plan.ArtifactScope, bool) {
	if graph == nil {
		return nil, false
	}
	scope, ok := graph.scopes[scopeKey{kind: kind, owner: ownerID}]
	return scope, ok
}

func (graph *Graph) ParallelScopeForJoin(joinID string) (*plan.ArtifactScope, bool) {
	if graph == nil {
		return nil, false
	}
	scope, ok := graph.parallelByJoin[joinID]
	return scope, ok
}

func (graph *Graph) FanOutScopeForJoin(joinID string) (*plan.ArtifactScope, bool) {
	if graph == nil {
		return nil, false
	}
	scope, ok := graph.fanOutByJoin[joinID]
	return scope, ok
}

// WorkItemsDependencyValueContract returns the frozen business-value schema
// shared by every dependency envelope produced by the one fan_out directly
// consuming this callback's WorkItems batch. IndexArtifact has already
// validated that the callback-to-fan_out relation is unique.
func (graph *Graph) WorkItemsDependencyValueContract(callbackNodeID string) (plan.ArtifactContract, error) {
	if graph == nil || graph.Artifact == nil {
		return nil, fmt.Errorf("Run graph is nil")
	}
	return plan.WorkItemsDependencyValueContract(graph.Artifact, callbackNodeID)
}

func inputPort(node *plan.ArtifactNode, name string) (plan.ArtifactContract, bool) {
	if node == nil {
		return nil, false
	}
	for _, port := range node.InputPorts {
		if port.Name == name {
			return port.Contract, true
		}
	}
	return nil, false
}

func outputPort(node *plan.ArtifactNode, name string) (plan.ArtifactContract, bool) {
	if node == nil {
		return nil, false
	}
	for _, port := range node.OutputPorts {
		if port.Name == name {
			return port.Contract, true
		}
	}
	return nil, false
}
