package promptcompiler

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

// RenderRuntimeInput renders the complete runtime input (system prompt, conversation context, and tool definitions) for the given slot and event kind. The output is formatted for the target runtime (opencode, claude, or codex).
func RenderRuntimeInput(ctx context.Context, input RuntimeInputEnvelope, runtime string, opts RenderOptions) (RenderedPromptPayload, error) {
	compiler := New(DefaultPolicy())
	return compiler.RenderRuntimeInput(ctx, input, runtime, opts)
}

func (c *Compiler) RenderRuntimeInput(ctx context.Context, input RuntimeInputEnvelope, runtime string, _ RenderOptions) (RenderedPromptPayload, error) {
	if err := contextError(ctx); err != nil {
		return RenderedPromptPayload{}, err
	}
	if err := validateRuntimeInputEnvelope(input); err != nil {
		return RenderedPromptPayload{}, err
	}
	rendered := RenderedPromptPayload{
		Runtime: strings.TrimSpace(runtime),
		Slots:   make(map[string]any),
	}
	switch input.EventKind {
	case EventThreadContract:
		if input.Bundle == nil {
			return RenderedPromptPayload{}, fmt.Errorf("thread contract render requires bundle")
		}
		base := renderSlot(input.Bundle.Blocks, SlotBase)
		developer := renderSlot(input.Bundle.Blocks, SlotDeveloper)
		if runtimeUsesSplitInstructionSlots(runtime) {
			rendered.Slots["baseInstructions"] = base
			rendered.Slots["developerInstructions"] = developer
			rendered.PromptSlots.BaseInstructions = base
			rendered.PromptSlots.DeveloperInstructions = developer
			rendered.RenderOrder = []string{"baseInstructions", "developerInstructions"}
			return rendered, nil
		}
		if runtimeUsesNativeContractCarrier(runtime) {
			carrier := joinPromptSections(base, developer)
			rendered.Slots["runtimeCarrier"] = carrier
			rendered.PromptSlots.RuntimeCarrier = carrier
			rendered.RenderOrder = []string{"runtimeCarrier"}
			return rendered, nil
		}
		prompt := joinPromptSections(base, developer)
		rendered.Slots["prompt"] = prompt
		rendered.PromptSlots.Prompt = prompt
		rendered.RenderOrder = []string{"prompt"}
		return rendered, nil
	case EventTurnContext:
		if input.Bundle == nil {
			return RenderedPromptPayload{}, fmt.Errorf("turn context render requires bundle")
		}
		notices := renderBlockTypes(input.Bundle.Blocks, BlockNotice)
		turn := renderTurnContext(input.Bundle, runtime)
		reference := renderSlot(input.Bundle.Blocks, SlotReference)
		turnInput := joinPromptSections(notices, reference, turn)
		rendered.Slots["turnInput"] = turnInput
		rendered.PromptSlots.TurnInput = turnInput
		rendered.RenderOrder = []string{"turnInput"}
		rendered.AttributionBlocks = replyContextAttributionBlocks(input.Bundle, runtime)
		return rendered, nil
	case EventDeliveryEnvelope:
		if input.Delivery == nil {
			return RenderedPromptPayload{}, fmt.Errorf("delivery envelope render requires delivery payload")
		}
		text := renderDeliveryTextForRuntime(input.Delivery, runtime)
		rendered.Slots["deliveryText"] = text
		rendered.Slots["deliveryEnvelope"] = input.Delivery
		rendered.PromptSlots.DeliveryText = text
		rendered.PromptSlots.DeliveryEnvelope = input.Delivery
		rendered.RenderOrder = []string{"deliveryText", "deliveryEnvelope"}
		rendered.AttributionBlocks = deliveryAttributionBlocks(input.Delivery, text)
		return rendered, nil
	default:
		return RenderedPromptPayload{}, fmt.Errorf("unsupported event kind %q", input.EventKind)
	}
}

func deliveryAttributionBlocks(delivery *DeliveryEnvelope, renderedText string) []PromptBlock {
	if delivery == nil || strings.TrimSpace(renderedText) == "" {
		return nil
	}
	payloadEstimate := EstimateTokens(strings.TrimSpace(delivery.PayloadText))
	totalEstimate := EstimateTokens(renderedText)
	framingEstimate := max(totalEstimate-payloadEstimate, 0)
	blocks := make([]PromptBlock, 0, 2)
	if framingEstimate > 0 {
		blocks = append(blocks, renderedAttributionBlock("event.wake_context", framingEstimate))
	}
	if payloadEstimate > 0 {
		blocks = append(blocks, renderedAttributionBlock("event.current_message", payloadEstimate))
	}
	attachmentGuidance := RenderAttachmentResolution(AttachmentResolution{Prepared: delivery.ResolvedAttachments, Failure: cloneAttachmentFailure(delivery.AttachmentFailure)})
	if strings.TrimSpace(attachmentGuidance) != "" {
		id := "runtime_attachment_manifest"
		if delivery.AttachmentFailure != nil {
			id = "runtime_attachment_preparation_failure"
		}
		blocks = append(blocks, renderedAttributionBlock(id, EstimateTokens(attachmentGuidance)))
	}
	return blocks
}

func renderedAttributionBlock(id string, tokens int) PromptBlock {
	block, err := DefaultPromptBlockRegistry().ConstructAttribution(id, AttributionTemplateData{TokenEstimate: tokens})
	if err != nil {
		panic(err)
	}
	return block
}
func replyContextAttributionBlocks(bundle *PromptBundle, runtime string) []PromptBlock {
	if bundle == nil {
		return nil
	}
	attributed := append([]PromptBlock(nil), bundle.Blocks...)
	context := bundle.ReplyContext
	if context == nil {
		return attributed
	}
	text := replyTargetContext(context.Target, runtime)
	if strings.TrimSpace(text) == "" {
		return attributed
	}
	block := renderedAttributionBlock("renderer.reply_target_context", EstimateTokens(text))
	return append(attributed, block)
}

func renderTurnContext(bundle *PromptBundle, runtime string) string {
	if bundle == nil {
		return ""
	}
	text := renderBlockTypes(bundle.Blocks, BlockTurnContext)
	context := bundle.ReplyContext
	if context == nil {
		return text
	}
	target := strings.TrimSpace(context.Target)
	if target == "" {
		return text
	}
	return joinPromptSections(text, replyTargetContext(target, runtime))
}

func runtimeUsesSplitInstructionSlots(runtime string) bool {
	switch strings.TrimSpace(runtime) {
	case "codex", "claude", "kimi", "pi":
		return true
	default:
		return false
	}
}

func runtimeUsesNativeContractCarrier(runtime string) bool {
	switch strings.TrimSpace(runtime) {
	case "opencode":
		return true
	default:
		return false
	}
}

func renderSlot(blocks []PromptBlock, slot OutputSlot) string {
	if len(blocks) == 0 {
		return ""
	}
	parts := make([]string, 0, len(blocks))
	for _, block := range blocks {
		if block.Slot != slot {
			continue
		}
		if text := strings.TrimSpace(block.text); text != "" {
			parts = append(parts, text)
		}
	}
	return joinPromptSections(parts...)
}

func renderBlockTypes(blocks []PromptBlock, kinds ...BlockType) string {
	if len(blocks) == 0 || len(kinds) == 0 {
		return ""
	}
	allowed := make(map[BlockType]struct{}, len(kinds))
	for _, kind := range kinds {
		allowed[kind] = struct{}{}
	}
	parts := make([]string, 0, len(blocks))
	for _, block := range blocks {
		if _, ok := allowed[block.Type]; !ok {
			continue
		}
		if text := strings.TrimSpace(block.text); text != "" {
			parts = append(parts, text)
		}
	}
	return joinPromptSections(parts...)
}

func renderDeliveryText(delivery *DeliveryEnvelope) string {
	runtime := ""
	return renderDeliveryTextForRuntime(delivery, runtime)
}

func renderDeliveryTextForRuntime(delivery *DeliveryEnvelope, runtime string) string {
	if delivery == nil {
		return ""
	}
	author := strings.TrimSpace(delivery.AuthorMentionName)
	if author == "" {
		author = strings.TrimSpace(delivery.AuthorDisplayName)
	}
	if author != "" && !strings.HasPrefix(author, "@") {
		author = "@" + author
	}
	header := renderRegisteredPromptText("event.delivery_header", newDeliveryHeaderPromptInput(DeliveryHeaderTemplateData{
		Target: strings.TrimSpace(delivery.Binding.Target), MessageID: strings.TrimSpace(delivery.Correlation.MessageID),
		DeliveryID: deliveryHeaderDeliveryID(delivery), Source: strings.TrimSpace(delivery.Correlation.SourceMessageID),
		Timestamp: strings.TrimSpace(delivery.Timestamp), Type: strings.TrimSpace(delivery.MessageType), Author: author, Payload: strings.TrimSpace(delivery.PayloadText),
	}))
	lines := []string{header}
	threadContext := threadContextTemplateData(delivery.Binding.Target, deliveryReadHistoryTool(runtime))
	if replay := deliveryReplayGuidance(delivery); replay != "" {
		lines = append(lines, replay)
	}

	if details := formatAttachmentDetails(false, messagePromptAttachments(delivery.PromptFacts.Attachments)); details != "" {
		lines = append(lines, details)
	}
	if guidance := RenderAttachmentResolution(AttachmentResolution{
		Prepared: delivery.ResolvedAttachments,
		Failure:  cloneAttachmentFailure(delivery.AttachmentFailure),
	}); guidance != "" {
		lines = append(lines, guidance)
	}
	if details := formatWorkflowTeamContextPromptDetails(delivery.TeamContexts); details != "" {
		lines = append(lines, details)
	} else if details := formatTeamContextPromptDetails(delivery.TeamContexts); details != "" {
		lines = append(lines, details)
	}
	if details := formatWorkflowContextAvailablePrompt(delivery.WorkflowContext); details != "" {
		lines = append(lines, details)
	}
	if delivery.PromptFacts.WorkflowEdit != nil {
		if details := formatWorkflowAgentEditPrompt(*delivery.PromptFacts.WorkflowEdit); details != "" {
			lines = append(lines, details)
		}
	}

	if delivery.PromptFacts.HasTask {
		if details := formatTaskDispatchPromptMetadata(delivery.PromptFacts.Task, deliverySendMessageTool(runtime)); details != "" {
			lines = append(lines, details)
		}
	}
	if details := formatTaskScopeDiscussionPromptDetails(delivery.Binding.Target, types.InboxMessage{
		MessageType: delivery.MessageType,
	}, deliverySendMessageTool(runtime)); details != "" {
		lines = append(lines, details)
	}
	if delivery.PromptFacts.ThreadRoot != nil {
		lines = append(lines, formatReplyThreadRootPromptBlock(*delivery.PromptFacts.ThreadRoot, delivery.Correlation.TargetSeq))
	}

	if delivery.ActiveTaskTarget != nil {
		activeTaskTarget := SelectPromptActiveTaskTarget(types.ActiveTaskTarget{}, promptFactsInboxMessage(delivery.PromptFacts))
		if activeTarget := formatActiveTaskTargetPrompt(activeTaskTarget, deliverySendMessageTool(runtime)); activeTarget != "" {
			lines = append(lines, activeTarget)
		}
		if activeTaskTarget.Valid() {
			if directRouting := FormatActiveTaskDirectMessageRoutingPrompt(delivery.Binding.Target, delivery.MessageType, deliverySendMessageTool(runtime), deliveryReadHistoryTool(runtime)); directRouting != "" {
				lines = append(lines, directRouting)
			}
		}
	}

	if delivery.PromptFacts.Reaction != nil {
		if details := formatReactionPromptData(*delivery.PromptFacts.Reaction); details != "" {
			lines = append(lines, details)
		}
	}
	body := strings.Join(lines, "\n")
	text := renderRegisteredPromptText("event.delivery_message", newDeliveryMessagePromptInput(DeliveryMessageTemplateData{Body: body, ReplyInstruction: renderDeliveryReplyGuidance(delivery, runtime), DisplayName: deliveryPromptAuthorDisplayName(delivery.AuthorMentionName, delivery.AuthorDisplayName), ThreadContext: threadContext}))
	if delivery.IdentityNoticeData.CurrentName != "" {
		notice := renderRegisteredPromptText("notice.identity", newIdentityNoticePromptInput(delivery.IdentityNoticeData))
		return notice + "\n\n" + text
	}
	return text
}
func deliverySendMessageTool(runtime string) string {
	if strings.TrimSpace(runtime) == "" {
		return "message_send"
	}
	return toolNamePrefix(runtime, "agentteam")("message_send")
}

func deliveryReadHistoryTool(runtime string) string {
	if strings.TrimSpace(runtime) == "" {
		return "message_history_read"
	}
	return toolNamePrefix(runtime, "agentteam")("message_history_read")
}

func replyTargetContext(target, runtime string) string {
	target = strings.TrimSpace(target)
	if target == "" {
		return ""
	}
	return renderRegisteredPromptText("event.reply_target_context", newReplyTargetPromptInput(ReplyTargetTemplateData{Target: target, SendCall: formatMessageSendCall(deliverySendMessageTool(runtime), target)}))
}

func renderDeliveryReplyGuidance(delivery *DeliveryEnvelope, runtime string) string {
	if delivery == nil {
		return ""
	}
	kind := ""
	if strings.TrimSpace(delivery.MessageType) == "reaction" {
		kind = "reaction"
	} else if delivery.ReplyUnavailable {
		kind = "workflow"
	}
	target := strings.TrimSpace(delivery.Binding.Target)
	send := formatMessageSendCall(deliverySendMessageTool(runtime), target)
	if kind == "" && send == "" {
		kind = "no_target"
	}
	return renderRegisteredPromptText("event.delivery_reply", newDeliveryReplyPromptInput(DeliveryReplyTemplateData{Kind: kind, Target: target, SendCall: send}))
}

func deliveryReplayGuidance(delivery *DeliveryEnvelope) string {
	if delivery == nil {
		return ""
	}
	state := strings.TrimSpace(delivery.Replay.ReplayState)
	reason := strings.TrimSpace(delivery.Replay.DeferredReason)
	if state == "" && reason == "" {
		return ""
	}
	return renderRegisteredPromptText("event.delivery_replay", newDeliveryReplayPromptInput(DeliveryReplayTemplateData{State: state, Reason: reason}))
}

func deliveryHeaderDeliveryID(delivery *DeliveryEnvelope) string {
	if delivery == nil {
		return ""
	}
	id := strings.TrimSpace(delivery.Correlation.DeliveryID)
	if id == strings.TrimSpace(delivery.Correlation.MessageID) {
		return ""
	}
	return id
}

func deliveryPromptAuthorDisplayName(mentionName string, displayName string) string {
	mentionName = strings.TrimSpace(mentionName)
	displayName = strings.TrimSpace(displayName)
	if mentionName == "" || displayName == "" || displayName == mentionName {
		return ""
	}
	return displayName
}

// SlotText returns a named rendered surface without map parsing.
func (rendered RenderedPromptPayload) SlotText(key string) string {
	slots := rendered.PromptSlots
	return slots.slotText(key)
}

// ExtractStringSlot returns a rendered slot as text. New callers should use
// SlotText; this compatibility helper only falls back to the legacy map for
// old golden fixtures.
func ExtractStringSlot(rendered RenderedPromptPayload, key string) string {
	if text := rendered.PromptSlots.slotText(key); text != "" {
		return text
	}
	raw, ok := rendered.Slots[key]
	if !ok {
		return ""
	}
	text, ok := raw.(string)
	if ok {
		return text
	}
	data, err := json.Marshal(raw)
	if err != nil {
		return ""
	}
	return string(data)
}

func (s PromptSlots) slotText(key string) string {
	switch strings.TrimSpace(key) {
	case "baseInstructions":
		return s.BaseInstructions
	case "developerInstructions":
		return s.DeveloperInstructions
	case "prompt":
		return s.Prompt
	case "freshPrompt":
		return s.FreshPrompt
	case "resumePrompt":
		return s.ResumePrompt
	case "runtimeCarrier":
		return s.RuntimeCarrier
	case "contractCarrierFile":
		return s.ContractCarrierFile
	case "kimiRestrictedSystemPrompt":
		return s.KimiRestrictedSystemPrompt
	case "kimiInitialPrompt":
		return s.KimiInitialPrompt
	case "turnInput":
		return s.TurnInput
	case "deliveryText":
		return s.DeliveryText
	default:
		return ""
	}
}
