package plan

import (
	"fmt"
	"sort"
	"strings"
	"time"
)

func lowerArtifactContracts(document *Document) (ArtifactContracts, error) {
	input, err := lowerArtifactContract(document.Input)
	if err != nil {
		return ArtifactContracts{}, fmt.Errorf("lower Plan input: %w", err)
	}
	output, err := lowerArtifactContract(document.Output)
	if err != nil {
		return ArtifactContracts{}, fmt.Errorf("lower Plan output: %w", err)
	}
	state, err := lowerArtifactContract(document.State)
	if err != nil {
		return ArtifactContracts{}, fmt.Errorf("lower Run state: %w", err)
	}
	return ArtifactContracts{Input: input, Output: output, State: state}, nil
}

func lowerArtifactContract(source map[string]Schema) (ArtifactContract, error) {
	contract := make(ArtifactContract, 0, len(source))
	for _, name := range sortedSchemaKeys(source) {
		schema, err := lowerArtifactSchema(source[name])
		if err != nil {
			return nil, fmt.Errorf("field %q: %w", name, err)
		}
		contract = append(contract, ArtifactField{Name: name, Schema: schema})
	}
	return contract, nil
}

func lowerArtifactSchema(source Schema) (ArtifactSchema, error) {
	values := append([]string{}, source.Values...)
	sort.Strings(values)
	values = uniqueStrings(values)
	compiled := ArtifactSchema{
		Type:     source.Type,
		Required: schemaRequired(source),
		Values:   values,
	}
	if compiled.Values == nil {
		compiled.Values = []string{}
	}
	if source.Default != nil {
		literal, err := compileArtifactLiteral(source.Default, &source)
		if err != nil {
			return ArtifactSchema{}, fmt.Errorf("default: %w", err)
		}
		compiled.Default = &literal
	}
	if source.Items != nil {
		items, err := lowerArtifactSchema(*source.Items)
		if err != nil {
			return ArtifactSchema{}, fmt.Errorf("items: %w", err)
		}
		compiled.Items = &items
	}
	return compiled, nil
}

func uniqueStrings(values []string) []string {
	if len(values) == 0 {
		return []string{}
	}
	result := make([]string, 0, len(values))
	for _, value := range values {
		if len(result) == 0 || result[len(result)-1] != value {
			result = append(result, value)
		}
	}
	return result
}

func lowerArtifactBindings(v *validator, source map[string]ValueExpr, node *Node, references referenceContext, target map[string]Schema) ([]ArtifactBinding, error) {
	if node == nil {
		node = &Node{ID: "$unknown"}
	}
	bindings := make([]ArtifactBinding, 0, len(source))
	keys := make([]string, 0, len(source))
	for key := range source {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	for _, key := range keys {
		expected, hasExpected := target[key]
		var expectedPointer *Schema
		if hasExpected {
			expectedCopy := expected
			expectedPointer = &expectedCopy
		}
		expression, err := compileArtifactValueExpr(v, source[key], node, references, expectedPointer)
		if err != nil {
			return nil, fmt.Errorf("binding %q: %w", key, err)
		}
		bindings = append(bindings, ArtifactBinding{Target: key, Value: expression})
	}
	return bindings, nil
}

func lowerArtifactNode(v *validator, source *Node, scopePath []ArtifactScopeFrame) (ArtifactNode, error) {
	opcode, err := artifactOpcode(source)
	if err != nil {
		return ArtifactNode{}, err
	}
	inputPorts, err := lowerArtifactInputPorts(v, source)
	if err != nil {
		return ArtifactNode{}, err
	}
	outputPorts, err := lowerArtifactOutputPorts(source)
	if err != nil {
		return ArtifactNode{}, err
	}
	compiled := ArtifactNode{
		ID:          source.ID,
		Opcode:      opcode,
		InputPorts:  inputPorts,
		OutputPorts: outputPorts,
		StateEffects: ArtifactStateEffects{
			Reads: []string{}, Writes: []string{},
		},
		ScopePath: append([]ArtifactScopeFrame{}, scopePath...),
	}
	config, err := lowerArtifactNodeConfig(v, source)
	if err != nil {
		return ArtifactNode{}, err
	}
	compiled.Config = config
	return compiled, nil
}

func artifactOpcode(node *Node) (ArtifactOpcode, error) {
	return compilerAuthoringCapabilities.opcode(node)
}

func lowerArtifactInputPorts(v *validator, node *Node) ([]ArtifactPort, error) {
	ports := append([]string{}, v.targetPorts(node)...)
	sort.Strings(ports)
	compiled := make([]ArtifactPort, 0, len(ports))
	for _, port := range ports {
		contract, err := lowerArtifactContract(v.targetSchema(node, port))
		if err != nil {
			return nil, fmt.Errorf("input port %q: %w", port, err)
		}
		compiled = append(compiled, ArtifactPort{Name: port, Contract: contract})
	}
	return compiled, nil
}

func lowerArtifactOutputPorts(node *Node) ([]ArtifactPort, error) {
	ports := append([]string{}, sourcePorts(node)...)
	sort.Strings(ports)
	compiled := make([]ArtifactPort, 0, len(ports))
	for _, port := range ports {
		if compiledIRPortDropped(node, port) {
			continue
		}
		contractSource, _, exposes := sourcePayloadContract(node, port)
		if !exposes {
			contractSource = map[string]Schema{}
		}
		contract, err := lowerArtifactContract(contractSource)
		if err != nil {
			return nil, fmt.Errorf("output port %q: %w", port, err)
		}
		compiled = append(compiled, ArtifactPort{Name: port, Contract: contract})
	}
	return compiled, nil
}

// compiledIRPortDropped reports whether a source-port no longer exists on the
// compiled IR for a given node. send_message, request_approval, and join no
// longer carry a non-wait timeout/deadline, so their timed_out ports are not
// part of the compiled Artifact; wait preserves its timeout and timed_out port.
func compiledIRPortDropped(node *Node, port string) bool {
	if node == nil {
		return false
	}
	return compiledIRTypePortDropped(node.Type, port)
}

// compiledIRTypePortDropped is the node-type level form of compiledIRPortDropped
// used by authoring coverage so the official reference is not required to
// demonstrate source ports that no longer exist on the compiled IR.
func compiledIRTypePortDropped(nodeType NodeType, port string) bool {
	if port != "timed_out" {
		return false
	}
	switch nodeType {
	case NodeSendMessage, NodeRequestApproval, NodeJoin:
		return true
	}
	return false
}

func lowerArtifactNodeConfig(v *validator, node *Node) (ArtifactNodeConfig, error) {
	var config ArtifactNodeConfig
	switch node.Type {
	case NodeSendMessage:
		var target *ArtifactTemplate
		if strings.TrimSpace(node.SendMessage.Target) != "" {
			compiled, err := compileArtifactTemplate(v, node.SendMessage.Target, node, referenceContext{})
			if err != nil {
				return config, fmt.Errorf("target template: %w", err)
			}
			target = &compiled
		}
		var scope *ArtifactExpression
		if node.SendMessage.Scope != nil {
			expected := guaranteedSchema(Schema{Type: TypeScope})
			compiled, err := compileArtifactValueExpr(
				v,
				*node.SendMessage.Scope,
				node,
				referenceContext{activationOutputUnavailable: true},
				&expected,
			)
			if err != nil {
				return config, fmt.Errorf("scope expression: %w", err)
			}
			scope = &compiled
		}
		var responder *ArtifactExpression
		var selectOne *ArtifactExpression
		if fixed := node.SendMessage.Responder.fixed(); fixed != nil {
			expected := guaranteedSchema(Schema{Type: TypeAgent})
			compiled, err := compileArtifactValueExpr(
				v,
				*fixed,
				node,
				referenceContext{activationOutputUnavailable: true},
				&expected,
			)
			if err != nil {
				return config, fmt.Errorf("responder expression: %w", err)
			}
			responder = &compiled
		} else if selected := node.SendMessage.Responder.selectOne(); selected != nil {
			expected := guaranteedSchema(Schema{Type: TypeArray, Items: &Schema{Type: TypeAgent}})
			compiled, err := compileArtifactValueExpr(
				v,
				*selected,
				node,
				referenceContext{activationOutputUnavailable: true},
				&expected,
			)
			if err != nil {
				return config, fmt.Errorf("responder selectOne expression: %w", err)
			}
			selectOne = &compiled
		}
		var selectOneStrategy ArtifactSelectOneStrategy
		if node.SendMessage.Responder.selectOne() != nil {
			selectOneStrategy = ArtifactSelectOneStrategy(node.SendMessage.Responder.Strategy.Normalized())
		}
		var workScopeMode ArtifactWorkScopeMode
		var workScope *ArtifactExpression
		if node.SendMessage.hasWorkScope() {
			workScopeMode = ArtifactWorkScopeAgent
			if node.SendMessage.WorkScope != nil {
				expected := guaranteedSchema(Schema{Type: TypeScope})
				compiled, err := compileArtifactValueExpr(
					v,
					*node.SendMessage.WorkScope,
					node,
					referenceContext{activationOutputUnavailable: true},
					&expected,
				)
				if err != nil {
					return config, fmt.Errorf("workScope expression: %w", err)
				}
				workScopeMode = ArtifactWorkScopeFixed
				workScope = &compiled
			}
		}
		var relatedScopes []ArtifactExpression
		if node.SendMessage.RelatedScopes.Items != nil {
			relatedScopes = make([]ArtifactExpression, 0, len(node.SendMessage.RelatedScopes.Items))
			expected := guaranteedSchema(Schema{Type: TypeScope})
			for index, source := range node.SendMessage.RelatedScopes.Items {
				compiled, err := compileArtifactValueExpr(
					v,
					source,
					node,
					referenceContext{activationOutputUnavailable: true},
					&expected,
				)
				if err != nil {
					return config, fmt.Errorf("relatedScopes[%d] expression: %w", index, err)
				}
				relatedScopes = append(relatedScopes, compiled)
			}
		}
		var relatedScopesArray *ArtifactExpression
		if node.SendMessage.RelatedScopes.Array != nil {
			expected := guaranteedSchema(Schema{Type: TypeArray, Items: &Schema{Type: TypeScope}})
			compiled, err := compileArtifactValueExpr(
				v,
				*node.SendMessage.RelatedScopes.Array,
				node,
				referenceContext{activationOutputUnavailable: true},
				&expected,
			)
			if err != nil {
				return config, fmt.Errorf("relatedScopes array expression: %w", err)
			}
			relatedScopesArray = &compiled
		}
		var workItems *ArtifactWorkItemsConfig
		if node.SendMessage.WorkItems != nil {
			expected := guaranteedSchema(Schema{Type: TypeArray, Items: &Schema{Type: TypeAgent}})
			allowedResponders, err := compileArtifactValueExpr(
				v,
				node.SendMessage.WorkItems.AllowedResponders,
				node,
				referenceContext{activationOutputUnavailable: true},
				&expected,
			)
			if err != nil {
				return config, fmt.Errorf("workItems.allowedResponders expression: %w", err)
			}
			workItems = &ArtifactWorkItemsConfig{AllowedResponders: allowedResponders}
		}
		text, err := compileArtifactTemplate(v, node.SendMessage.Text, node, referenceContext{})
		if err != nil {
			return config, fmt.Errorf("text template: %w", err)
		}
		callback, err := lowerArtifactContract(node.SendMessage.CallbackOutput)
		if err != nil {
			return config, fmt.Errorf("callback output: %w", err)
		}
		retry, err := lowerArtifactRetry(effectiveActionRetry(v.document, node.SendMessage.Retry))
		if err != nil {
			return config, fmt.Errorf("retry: %w", err)
		}
		config.SendMessage = &ArtifactSendMessageConfig{
			Target: target, Scope: scope, Responder: responder, SelectOne: selectOne, SelectOneStrategy: selectOneStrategy, WorkScopeMode: workScopeMode, WorkScope: workScope,
			RelatedScopes: relatedScopes, RelatedScopesArray: relatedScopesArray, WorkItems: workItems,
			Text: text, StartThread: node.SendMessage.StartThread,
			CallbackOutput: callback, Retry: retry,
		}
	case NodeSetVariable:
		assignments := make([]ArtifactStateAssignment, 0, len(node.SetVariable.Assignments))
		for _, source := range node.SetVariable.Assignments {
			expected := v.document.State[source.State]
			expression, err := compileArtifactValueExpr(v, source.Value, node, referenceContext{}, &expected)
			if err != nil {
				return config, fmt.Errorf("assignment %q: %w", source.State, err)
			}
			assignments = append(assignments, ArtifactStateAssignment{State: source.State, Value: expression})
		}
		config.SetVariable = &ArtifactSetVariableConfig{Assignments: assignments}
	case NodeWait:
		expected := waitArtifactExpected(v, node)
		until, err := compileArtifactValueExpr(v, node.Wait.Until, node, referenceContext{}, expected)
		if err != nil {
			return config, fmt.Errorf("until: %w", err)
		}
		timeout, err := normalizedDurationNanos(node.Wait.Timeout)
		if err != nil {
			return config, fmt.Errorf("timeout: %w", err)
		}
		retry, err := lowerArtifactRetry(effectiveActionRetry(v.document, node.Wait.Retry))
		if err != nil {
			return config, fmt.Errorf("retry: %w", err)
		}
		config.Wait = &ArtifactWaitConfig{Until: until, TimeoutNanos: timeout, Retry: retry}
	case NodeRequestApproval:
		assignee, err := compileArtifactTemplate(v, node.RequestApproval.Assignee, node, referenceContext{})
		if err != nil {
			return config, fmt.Errorf("assignee template: %w", err)
		}
		summary, err := compileArtifactTemplate(v, node.RequestApproval.Summary, node, referenceContext{})
		if err != nil {
			return config, fmt.Errorf("summary template: %w", err)
		}
		var feedbackScope *ArtifactExpression
		if node.RequestApproval.hasFeedbackScope() {
			if node.RequestApproval.FeedbackScope == nil {
				return config, fmt.Errorf("feedbackScope expression cannot be null")
			}
			expected := guaranteedSchema(Schema{Type: TypeScope})
			compiled, compileErr := compileArtifactValueExpr(
				v,
				*node.RequestApproval.FeedbackScope,
				node,
				referenceContext{activationOutputUnavailable: true},
				&expected,
			)
			if compileErr != nil {
				return config, fmt.Errorf("feedbackScope expression: %w", compileErr)
			}
			feedbackScope = &compiled
		}
		retry, err := lowerArtifactRetry(effectiveActionRetry(v.document, node.RequestApproval.Retry))
		if err != nil {
			return config, fmt.Errorf("retry: %w", err)
		}
		config.RequestApproval = &ArtifactRequestApprovalConfig{Assignee: assignee, Summary: summary, FeedbackScope: feedbackScope, Retry: retry}
	case NodeSwitch:
		cases := make([]ArtifactSwitchCase, 0, len(node.Switch.Cases))
		for _, source := range node.Switch.Cases {
			compiled := ArtifactSwitchCase{Port: source.Port, Default: source.IsDefault}
			if source.When != nil {
				condition, err := compileArtifactPredicate(v, *source.When, node, referenceContext{})
				if err != nil {
					return config, fmt.Errorf("switch case %q: %w", source.Port, err)
				}
				compiled.Condition = &condition
			}
			cases = append(cases, compiled)
		}
		// all_match evaluates a set of pure predicates and activates a set of
		// branch ports, so authoring order cannot affect execution. Normalize
		// that set by port. first_match intentionally retains source order
		// because the first true predicate wins.
		if node.Switch.Mode == SwitchAllMatch {
			sort.Slice(cases, func(i, j int) bool { return cases[i].Port < cases[j].Port })
		}
		config.Switch = &ArtifactSwitchConfig{Cases: cases}
	case NodeFork:
		branches := append([]string{}, node.Fork.Branches...)
		sort.Strings(branches)
		config.Fork = &ArtifactForkConfig{Branches: branches}
	case NodeFanOut:
		expected := guaranteedSchema(Schema{Type: TypeWorkItems})
		items, err := compileArtifactValueExpr(v, node.FanOut.Items, node, referenceContext{}, &expected)
		if err != nil {
			return config, fmt.Errorf("items: %w", err)
		}
		maxConcurrency := DefaultFanOutMaxConcurrency
		if node.FanOut.MaxConcurrency != nil {
			maxConcurrency = *node.FanOut.MaxConcurrency
		}
		config.FanOut = &ArtifactFanOutConfig{
			Items:          items,
			MaxConcurrency: maxConcurrency,
			FailureMode:    node.FanOut.FailureMode,
		}
	case NodeJoin:
		result, err := lowerArtifactContract(node.Join.Result)
		if err != nil {
			return config, fmt.Errorf("result: %w", err)
		}
		merge, err := lowerArtifactBindings(v, node.Join.Merge, node, referenceContext{joinMerge: true}, node.Join.Result)
		if err != nil {
			return config, fmt.Errorf("merge: %w", err)
		}
		ownerID := node.Join.ForkID
		if node.Join.FanOutID != "" {
			ownerID = node.Join.FanOutID
		}
		config.Join = &ArtifactJoinConfig{OwnerID: ownerID, Result: result, Merge: merge}
	case NodeLoop:
		state, err := lowerArtifactContract(node.Loop.State)
		if err != nil {
			return config, fmt.Errorf("state: %w", err)
		}
		config.Loop = &ArtifactAgentTeamConfig{Mode: node.Loop.Mode, Iterations: node.Loop.Iterations, State: state}
	case NodeEnd:
		config.End = &ArtifactEndConfig{Code: node.End.Code}
	default:
		return config, fmt.Errorf("unsupported node type %q", node.Type)
	}
	return config, nil
}

func lowerArtifactRetry(source RetryPolicy) (ArtifactRetryPolicy, error) {
	backoff, err := normalizedDurationNanos(source.Backoff)
	if err != nil {
		return ArtifactRetryPolicy{}, err
	}
	return ArtifactRetryPolicy{MaxAttempts: source.MaxAttempts, BackoffNanos: backoff}, nil
}

func normalizedDurationNanos(source string) (int64, error) {
	duration, err := parsePlanDuration(source)
	if err != nil {
		return 0, err
	}
	return int64(duration), nil
}

func waitArtifactExpected(v *validator, node *Node) *Schema {
	value := node.Wait.Until
	if value.Value != nil {
		if text, ok := (*value.Value).(string); ok {
			return temporalArtifactSchema(text)
		}
	}
	if reference := strings.TrimSpace(value.Ref); reference != "" {
		if schema, ok := v.referenceSchema(reference, node, referenceContext{}); ok {
			return &schema
		}
	}
	if expression := strings.TrimSpace(value.Expr); expression != "" {
		if schema, ok := v.referenceSchema(expression, node, referenceContext{}); ok {
			return &schema
		}
		if schema, _, ok := v.inferTypedExpression(expression, node, referenceContext{}); ok && schema.Type == TypeEnum && len(schema.Values) == 1 {
			return temporalArtifactSchema(schema.Values[0])
		}
	}
	return nil
}

func temporalArtifactSchema(value string) *Schema {
	if duration, err := parsePlanDuration(value); err == nil && duration > 0 {
		schema := Schema{Type: TypeDuration}
		return &schema
	}
	if _, err := time.Parse(time.RFC3339, value); err == nil {
		schema := Schema{Type: TypeDatetime}
		return &schema
	}
	return nil
}
