package plan

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"sort"
)

const semanticDigestDomain = "loop.plan.semantic/v1\x00"

// Artifact is the versioned normalized compiler output. It contains no source
// Document, YAML, Plan aggregate metadata, visual layout, or authoring hints.
// Runtime consumers dispatch opcodes and evaluate the compiled expression AST;
// they never reinterpret Plan source.
type Artifact struct {
	ArtifactVersion string            `json:"artifactVersion"`
	CompilerVersion string            `json:"compilerVersion"`
	APIVersion      string            `json:"apiVersion"`
	Digest          string            `json:"digest"`
	Contracts       ArtifactContracts `json:"contracts"`
	Entry           ArtifactEntry     `json:"entry"`
	Nodes           []ArtifactNode    `json:"nodes"`
	Edges           []ArtifactEdge    `json:"edges"`
	Scopes          []ArtifactScope   `json:"scopes"`
}

// Compile performs strict parse, validation, and normalized IR construction.
// Author mistakes are diagnostics; only an impossible lowering or normalized
// encoding failure is returned as a Go error.
func Compile(raw []byte) (*Artifact, []Diagnostic, error) {
	document, diagnostics := Parse(raw)
	if HasErrors(diagnostics) {
		return nil, diagnostics, nil
	}
	source := indexSource(raw)
	validated, analysis := validate(document, source)
	diagnostics = append(diagnostics, validated...)
	sortDiagnostics(diagnostics)
	if HasErrors(diagnostics) {
		return nil, diagnostics, nil
	}
	artifact, err := buildArtifact(document, analysis)
	if err != nil {
		return nil, diagnostics, fmt.Errorf("build Plan artifact: %w", err)
	}
	budgetDiagnostics, err := artifactWireBudgetDiagnostics(artifact)
	if err != nil {
		return nil, diagnostics, fmt.Errorf("encode Plan artifact: %w", err)
	}
	if len(budgetDiagnostics) != 0 {
		diagnostics = append(diagnostics, budgetDiagnostics...)
		sortDiagnostics(diagnostics)
		return nil, diagnostics, nil
	}
	return artifact, diagnostics, nil
}

func buildArtifact(document *Document, analysis compilerAnalysis) (*Artifact, error) {
	if document == nil {
		return nil, fmt.Errorf("document is nil")
	}
	validatorView := &validator{
		document: document,
		nodes:    make(map[string]*Node, len(document.Graph.Nodes)),
	}
	for i := range document.Graph.Nodes {
		node := &document.Graph.Nodes[i]
		validatorView.nodes[node.ID] = node
	}

	contracts, err := lowerArtifactContracts(document)
	if err != nil {
		return nil, err
	}
	artifactVersion, compilerVersion := ArtifactVersionV1, CompilerVersionV1
	if documentRequiresArtifactV5(document) {
		artifactVersion, compilerVersion = ArtifactVersionV5, CompilerVersionV5
	} else if documentRequiresArtifactV4(document) {
		artifactVersion, compilerVersion = ArtifactVersionV4, CompilerVersionV4
	} else if documentRequiresArtifactV3(document) {
		artifactVersion, compilerVersion = ArtifactVersionV3, CompilerVersionV3
	} else if documentRequiresArtifactV2(document) {
		artifactVersion, compilerVersion = ArtifactVersionV2, CompilerVersionV2
	}
	scopes, nodePaths, transitions := lowerArtifactScopes(analysis)
	artifact := &Artifact{
		ArtifactVersion: artifactVersion,
		CompilerVersion: compilerVersion,
		APIVersion:      document.APIVersion,
		Contracts:       contracts,
		Nodes:           make([]ArtifactNode, 0, len(document.Graph.Nodes)),
		Edges:           make([]ArtifactEdge, 0, len(document.Graph.Edges)),
		Scopes:          scopes,
	}

	entryBindings := document.Graph.Entry.Input
	entryTarget := validatorView.nodes[document.Graph.Entry.NodeID]
	if entryBindings == nil && entryTarget != nil {
		entryBindings, _ = omittedBindings(document.Input, "plan.input", validatorView.targetSchema(entryTarget, document.Graph.Entry.Port))
	}
	entrySource := &Node{ID: "$entry"}
	compiledEntry, err := lowerArtifactBindings(validatorView, entryBindings, entrySource, referenceContext{}, validatorView.targetSchema(entryTarget, document.Graph.Entry.Port))
	if err != nil {
		return nil, fmt.Errorf("lower entry bindings: %w", err)
	}
	artifact.Entry = ArtifactEntry{
		NodeID:   document.Graph.Entry.NodeID,
		Port:     document.Graph.Entry.Port,
		Bindings: compiledEntry,
	}

	for i := range document.Graph.Nodes {
		node, err := lowerArtifactNode(validatorView, &document.Graph.Nodes[i], nodePaths[document.Graph.Nodes[i].ID])
		if err != nil {
			return nil, fmt.Errorf("lower node %q: %w", document.Graph.Nodes[i].ID, err)
		}
		artifact.Nodes = append(artifact.Nodes, node)
	}
	for i := range document.Graph.Edges {
		edge := &document.Graph.Edges[i]
		sourceNode := validatorView.nodes[edge.Source.NodeID]
		targetNode := validatorView.nodes[edge.Target.NodeID]
		bindings := edge.Bindings
		if bindings == nil && sourceNode != nil && targetNode != nil {
			sourceSchema, root, exposesPayload := sourcePayloadContract(sourceNode, edge.Source.Port)
			if exposesPayload {
				bindings, _ = omittedBindings(sourceSchema, root, validatorView.targetSchema(targetNode, edge.Target.Port))
			} else {
				bindings = map[string]ValueExpr{}
			}
		}
		context := referenceContext{sourcePort: edge.Source.Port}
		if sourceNode != nil && sourceNode.Type == NodeLoop {
			context.loop = sourceNode.Loop
		}
		compiledBindings, err := lowerArtifactBindings(validatorView, bindings, sourceNode, context, validatorView.targetSchema(targetNode, edge.Target.Port))
		if err != nil {
			return nil, fmt.Errorf("lower edge %q bindings: %w", edge.ID, err)
		}
		artifact.Edges = append(artifact.Edges, ArtifactEdge{
			ID:               edge.ID,
			Source:           ArtifactEndpoint{NodeID: edge.Source.NodeID, Port: edge.Source.Port},
			Target:           ArtifactEndpoint{NodeID: edge.Target.NodeID, Port: edge.Target.Port},
			Bindings:         compiledBindings,
			ScopeTransitions: append([]ArtifactScopeTransition{}, transitions[edge.ID]...),
		})
	}
	if err := lowerImplicitFailures(artifact, document); err != nil {
		return nil, fmt.Errorf("lower implicit failure routing: %w", err)
	}

	sort.Slice(artifact.Nodes, func(i, j int) bool { return artifact.Nodes[i].ID < artifact.Nodes[j].ID })
	sort.Slice(artifact.Edges, func(i, j int) bool { return artifact.Edges[i].ID < artifact.Edges[j].ID })
	applyArtifactStateEffects(artifact)
	digest, err := artifact.computeDigest()
	if err != nil {
		return nil, err
	}
	artifact.Digest = digest
	return artifact, nil
}

func documentRequiresArtifactV5(document *Document) bool {
	if document == nil {
		return false
	}
	for index := range document.Graph.Nodes {
		node := &document.Graph.Nodes[index]
		if node.RequestApproval != nil && node.RequestApproval.hasFeedbackScope() {
			return true
		}
	}
	return false
}

func documentRequiresArtifactV4(document *Document) bool {
	if document == nil {
		return false
	}
	schemaMapUsesWorkItems := func(schemas map[string]Schema) bool {
		for name, schema := range schemas {
			if isWorkItemCamelContractField(name) {
				return true
			}
			for current := &schema; current != nil; current = current.Items {
				if current.Type == TypeWorkItems {
					return true
				}
			}
		}
		return false
	}
	if schemaMapUsesWorkItems(document.Input) || schemaMapUsesWorkItems(document.Output) || schemaMapUsesWorkItems(document.State) {
		return true
	}
	for index := range document.Graph.Nodes {
		node := &document.Graph.Nodes[index]
		if node.Type == NodeFanOut || schemaMapUsesWorkItems(node.Input) {
			return true
		}
		if node.Join != nil && node.Join.FanOutID != "" {
			return true
		}
		if config := node.SendMessage; config != nil &&
			(config.Responder != nil && !config.Responder.IsZero() || config.WorkItems != nil || config.RelatedScopes.Array != nil) {
			return true
		}
	}
	return false
}

func documentRequiresArtifactV3(document *Document) bool {
	if document == nil {
		return false
	}
	for index := range document.Graph.Nodes {
		config := document.Graph.Nodes[index].SendMessage
		if config != nil && (config.hasWorkScope() || config.hasRelatedScopes()) {
			return true
		}
	}
	return false
}

func documentRequiresArtifactV2(document *Document) bool {
	if document == nil {
		return false
	}
	schemaMapUsesScope := func(schemas map[string]Schema) bool {
		for _, schema := range schemas {
			for current := &schema; current != nil; current = current.Items {
				if current.Type == TypeScope {
					return true
				}
			}
		}
		return false
	}
	if schemaMapUsesScope(document.Input) || schemaMapUsesScope(document.Output) || schemaMapUsesScope(document.State) {
		return true
	}
	for index := range document.Graph.Nodes {
		node := &document.Graph.Nodes[index]
		if schemaMapUsesScope(node.Input) {
			return true
		}
		if node.SendMessage != nil && (node.SendMessage.Scope != nil || node.SendMessage.StartThread || schemaMapUsesScope(node.SendMessage.CallbackOutput)) {
			return true
		}
		if node.Join != nil && schemaMapUsesScope(node.Join.Result) {
			return true
		}
		if node.Loop != nil && schemaMapUsesScope(node.Loop.State) {
			return true
		}
	}
	return false
}

// semanticArtifact is the domain-separated digest projection. Envelope format
// and compiler versions are deliberately excluded; a compiler upgrade that
// emits the same runtime semantics retains the same semantic digest.
type semanticArtifact struct {
	Contracts ArtifactContracts `json:"contracts"`
	Entry     ArtifactEntry     `json:"entry"`
	Nodes     []ArtifactNode    `json:"nodes"`
	Edges     []ArtifactEdge    `json:"edges"`
	Scopes    []ArtifactScope   `json:"scopes"`
}

func (artifact *Artifact) digestPayload() ([]byte, error) {
	if artifact == nil {
		return nil, fmt.Errorf("artifact is nil")
	}
	return json.Marshal(semanticArtifact{
		Contracts: artifact.Contracts,
		Entry:     artifact.Entry,
		Nodes:     artifact.Nodes,
		Edges:     artifact.Edges,
		Scopes:    artifact.Scopes,
	})
}

func (artifact *Artifact) computeDigest() (string, error) {
	payload, err := artifact.digestPayload()
	if err != nil {
		return "", err
	}
	hash := sha256.New()
	_, _ = hash.Write([]byte(semanticDigestDomain))
	_, _ = hash.Write(payload)
	return "sha256:" + hex.EncodeToString(hash.Sum(nil)), nil
}

// DeterministicJSON returns the deterministic normalized wire representation. All
// map-like source constructs have already been lowered to sorted slices.
func (artifact *Artifact) DeterministicJSON() ([]byte, error) {
	if artifact == nil {
		return nil, fmt.Errorf("artifact is nil")
	}
	return json.Marshal(artifact)
}

// VerifyDigest verifies runtime semantics, not envelope/compiler identity.
// ValidateEnvelope additionally enforces exact supported envelope versions.
func (artifact *Artifact) VerifyDigest() bool {
	if artifact == nil || artifact.Digest == "" {
		return false
	}
	digest, err := artifact.computeDigest()
	return err == nil && artifact.Digest == digest
}
