package promptcompiler

import (
	"context"
	"fmt"
	"strings"
)

// CompileLaunchPrompt compiles every semantic launch surface in one atomic
// compiler operation. Dispatcher code supplies only typed facts; it never
// joins a thread contract to a turn prompt or selects fresh versus resume
// strings itself.
func CompileLaunchPrompt(ctx context.Context, desc LaunchCompileDescriptor, in LaunchCompileInput) (CompiledPrompt, error) {
	compiler := New(DefaultPolicy())
	return compiler.CompileLaunchPrompt(ctx, desc, in)
}

// CompileLaunchPrompt compiles one complete launch carrier under this
// compiler's registry, budget, and runtime surface policy.
func (c *Compiler) CompileLaunchPrompt(ctx context.Context, desc LaunchCompileDescriptor, in LaunchCompileInput) (CompiledPrompt, error) {
	if err := contextError(ctx); err != nil {
		return CompiledPrompt{}, err
	}
	runtime := strings.TrimSpace(desc.Runtime)
	if runtime == "" {
		return CompiledPrompt{}, fmt.Errorf("launch compilation requires runtime")
	}
	if desc.ContractMode == "" {
		desc.ContractMode = LaunchContractModeForRuntime(runtime)
	}
	if desc.ContractCarrierFile == "" {
		desc.ContractCarrierFile = LaunchContractFileForRuntime(runtime)
	}
	contractType := EventThreadStart
	if strings.TrimSpace(desc.SessionID) != "" {
		contractType = EventThreadResume
	}
	turnType := desc.TurnEventType
	if turnType == "" {
		if strings.TrimSpace(desc.SessionID) != "" {
			turnType = EventResume
		} else {
			turnType = EventFreshStart
		}
	}
	baseInput := in.Input
	baseInput.Runtime = runtime
	contractDesc := CompileDescriptor{EventID: NewEventID(contractType), EventKind: EventThreadContract, EventType: contractType}
	contractEnv, err := c.CompileRuntimeInput(ctx, contractDesc, baseInput)
	if err != nil {
		return CompiledPrompt{}, err
	}
	contractRendered, err := c.RenderRuntimeInput(ctx, contractEnv, runtime, RenderOptions{})
	if err != nil {
		return CompiledPrompt{}, err
	}

	turnInput := baseInput
	if strings.TrimSpace(desc.SessionID) != "" {
		turnInput.ExternalSkillReferencePayload = ""
		turnInput.SkillPlan.Reference = ""
	}
	turnDesc := CompileDescriptor{EventID: firstNonEmptyLaunchEventID(desc.EventID, turnType), EventKind: EventTurnContext, EventType: turnType}
	turnEnv, err := c.CompileRuntimeInput(ctx, turnDesc, turnInput)
	if err != nil {
		return CompiledPrompt{}, err
	}
	turnRendered, err := c.RenderRuntimeInput(ctx, turnEnv, runtime, RenderOptions{})
	if err != nil {
		return CompiledPrompt{}, err
	}
	freshFallbackRendered := RenderedPromptPayload{}
	freshFallbackEnv := RuntimeInputEnvelope{}
	if strings.TrimSpace(desc.SessionID) != "" && desc.FreshFallbackOnResume {
		freshFallbackEnv, err = c.CompileRuntimeInput(ctx, CompileDescriptor{EventID: NewEventID(turnType), EventKind: EventTurnContext, EventType: turnType}, baseInput)
		if err != nil {
			return CompiledPrompt{}, err
		}
		freshFallbackRendered, err = c.RenderRuntimeInput(ctx, freshFallbackEnv, runtime, RenderOptions{})
		if err != nil {
			return CompiledPrompt{}, err
		}
	}

	carrier := launchPromptCarrier{
		ContractCarrierFile: desc.ContractCarrierFile,
	}
	carrier.BaseInstructions = contractRendered.SlotText("baseInstructions")
	carrier.DeveloperInstructions = contractRendered.SlotText("developerInstructions")
	carrier.SystemPrompt = joinPromptSections(carrier.BaseInstructions, carrier.DeveloperInstructions)
	carrier.RuntimeCarrier = contractRendered.SlotText("runtimeCarrier")
	freshTurn := turnRendered.SlotText("turnInput")
	resumeTurn := freshTurn
	if strings.TrimSpace(desc.SessionID) != "" {
		resumeTurn = turnRendered.SlotText("turnInput")
	}
	carrier.KimiRestrictedSystemPrompt = renderRestrictedSystemPrompt(baseInput.RestrictedRuntime)
	carrier.KimiInitialPrompt = joinPromptSections(carrier.BaseInstructions, carrier.DeveloperInstructions, selectedLaunchTurn(desc.SessionID, freshTurn, resumeTurn))

	switch desc.ContractMode {
	case LaunchContractNative:
		carrier.Prompt = selectedLaunchTurn(desc.SessionID, freshTurn, resumeTurn)
	case LaunchContractSplit:
		carrier.Prompt = ""
		if strings.TrimSpace(desc.SessionID) == "" {
			carrier.FreshPrompt = freshTurn
		} else if freshFallbackRendered.Runtime != "" {
			carrier.FreshPrompt = freshFallbackRendered.SlotText("turnInput")
		}
		if strings.TrimSpace(desc.SessionID) != "" {
			carrier.ResumePrompt = resumeTurn
		}
	case LaunchContractCombined:
		carrier.FreshPrompt = joinPromptSections(contractRendered.SlotText("prompt"), freshTurn)
		if strings.TrimSpace(desc.SessionID) != "" {
			carrier.ResumePrompt = resumeTurn
		}
		carrier.Prompt = selectedLaunchTurn(desc.SessionID, carrier.FreshPrompt, carrier.ResumePrompt)
	default:
		return CompiledPrompt{}, fmt.Errorf("unsupported launch contract mode %q", desc.ContractMode)
	}

	// The selected surface projection is retained for generic attribution and
	// transport serialization. The complete launch carrier remains private.
	selected := turnRendered
	selected.PromptSlots.Prompt = carrier.Prompt
	selected.PromptSlots.FreshPrompt = carrier.FreshPrompt
	selected.PromptSlots.ResumePrompt = carrier.ResumePrompt
	selected.PromptSlots.BaseInstructions = carrier.BaseInstructions
	selected.PromptSlots.DeveloperInstructions = carrier.DeveloperInstructions
	selected.PromptSlots.SystemPrompt = carrier.SystemPrompt
	selected.PromptSlots.RuntimeCarrier = carrier.RuntimeCarrier
	selected.PromptSlots.ContractCarrierFile = carrier.ContractCarrierFile
	selected.PromptSlots.KimiRestrictedSystemPrompt = carrier.KimiRestrictedSystemPrompt
	selected.PromptSlots.KimiInitialPrompt = carrier.KimiInitialPrompt
	selected.Slots = map[string]any{
		"prompt":                     carrier.Prompt,
		"freshPrompt":                carrier.FreshPrompt,
		"resumePrompt":               carrier.ResumePrompt,
		"baseInstructions":           carrier.BaseInstructions,
		"developerInstructions":      carrier.DeveloperInstructions,
		"systemPrompt":               carrier.SystemPrompt,
		"runtimeCarrier":             carrier.RuntimeCarrier,
		"contractCarrierFile":        carrier.ContractCarrierFile,
		"kimiRestrictedSystemPrompt": carrier.KimiRestrictedSystemPrompt,
		"kimiInitialPrompt":          carrier.KimiInitialPrompt,
	}
	selected.RenderOrder = []string{"prompt", "freshPrompt", "resumePrompt", "baseInstructions", "developerInstructions", "runtimeCarrier"}
	selected.AttributionBlocks = append(append([]PromptBlock(nil), contractRendered.AttributionBlocks...), turnRendered.AttributionBlocks...)
	if freshFallbackRendered.Runtime != "" {
		selected.AttributionBlocks = append(selected.AttributionBlocks, freshFallbackRendered.AttributionBlocks...)
	}

	components := launchPromptComponents(contractEnv, contractRendered, turnEnv, turnRendered, freshFallbackEnv, freshFallbackRendered, desc.SessionID)
	if desc.ContractMode == LaunchContractNative && strings.TrimSpace(desc.SessionID) != "" {
		components = append(components, CompiledPromptComponent{Stage: "turn_context", Freshness: "fresh", Envelope: turnEnv, Rendered: turnRendered})
	}
	return CompiledPrompt{
		envelope:   turnEnv,
		rendered:   selected,
		launch:     &carrier,
		components: components,
	}, nil
}

func launchPromptComponents(contractEnv RuntimeInputEnvelope, contractRendered RenderedPromptPayload, turnEnv RuntimeInputEnvelope, turnRendered RenderedPromptPayload, freshEnv RuntimeInputEnvelope, freshRendered RenderedPromptPayload, sessionID string) []CompiledPromptComponent {
	components := []CompiledPromptComponent{
		{Stage: "thread_contract", Freshness: launchFreshness(sessionID), Envelope: contractEnv, Rendered: contractRendered},
		{Stage: "turn_context", Freshness: launchFreshness(sessionID), Envelope: turnEnv, Rendered: turnRendered},
	}
	if freshRendered.Runtime != "" {
		components = append(components, CompiledPromptComponent{Stage: "turn_context", Freshness: "fresh", Envelope: freshEnv, Rendered: freshRendered})
	}
	return components
}

func firstNonEmptyLaunchEventID(eventID string, eventType CompileEventType) string {
	if strings.TrimSpace(eventID) != "" {
		return strings.TrimSpace(eventID)
	}
	return NewEventID(eventType)
}

func selectedLaunchTurn(sessionID, fresh, resume string) string {
	if strings.TrimSpace(sessionID) != "" {
		return resume
	}
	return fresh
}

func launchFreshness(sessionID string) string {
	if strings.TrimSpace(sessionID) != "" {
		return "resume"
	}
	return "fresh"
}

// LaunchContractModeForRuntime is the single runtime-surface policy table.
func LaunchContractModeForRuntime(runtime string) LaunchContractMode {
	switch strings.TrimSpace(runtime) {
	case "opencode":
		return LaunchContractNative
	case "codex", "claude", "kimi", "pi":
		return LaunchContractSplit
	default:
		return LaunchContractCombined
	}
}

// LaunchNeedsFreshFallbackOnResume reports runtimes that need a compiler
// rendered fresh fallback alongside the resume surface.
func LaunchNeedsFreshFallbackOnResume(runtime string) bool {
	switch strings.TrimSpace(runtime) {
	case "codex", "kimi":
		return true
	default:
		return false
	}
}

// LaunchContractFileForRuntime is the compiler-owned native carrier filename.
func LaunchContractFileForRuntime(runtime string) string {
	if strings.TrimSpace(runtime) == "opencode" {
		return "AGENTS.md"
	}
	return ""
}

func renderRestrictedSystemPrompt(restricted bool) string {
	if !restricted {
		return ""
	}
	return renderRegisteredPromptText("runtime.kimi_restricted_system", newRuntimePolicyPromptInput(KimiRestrictedSystemTemplateData{}))
}
