package promptcompiler

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"
	"unicode/utf8"

	"github.com/google/uuid"

	"github.com/tidbcloud/AgentTeam/desktop/internal/identitydoc"
	"github.com/tidbcloud/AgentTeam/desktop/internal/identitynotice"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
	"github.com/tidbcloud/AgentTeam/desktop/prompts"
	"github.com/tidbcloud/AgentTeam/internal/core/domain"
)

const (
	SchemaVersion                    = "prompt-compiler/v1"
	DefaultMaxWorkspaceDocBytes      = 64 * 1024
	DefaultMaxIdentityDocCharacters  = 4096
	DefaultMaxBlockTokens            = 16384
	identityNoticeVersionMetadataKey = "identityNoticeSourceVersion"

	workspaceDocOversizeNoticeTemplate = "notices/workspace_doc_oversize_notice.md"
)

// Compiler builds and validates runtime input envelopes under a Policy.
type Compiler struct {
	policy Policy
}

// New creates a Compiler with normalized policy defaults.
func New(policy Policy) *Compiler {
	return &Compiler{policy: normalizePolicy(policy)}
}

// DefaultPolicy returns the normalized default compiler policy.
func DefaultPolicy() Policy {
	return normalizePolicy(Policy{})
}

func normalizePolicy(policy Policy) Policy {
	if policy.MaxWorkspaceDocBytes <= 0 {
		policy.MaxWorkspaceDocBytes = DefaultMaxWorkspaceDocBytes
	}
	if policy.MaxIdentityDocCharacters <= 0 {
		policy.MaxIdentityDocCharacters = DefaultMaxIdentityDocCharacters
	}
	if policy.MaxBlockTokens <= 0 {
		policy.MaxBlockTokens = DefaultMaxBlockTokens
	}
	return policy
}

// NewEventID returns a unique event ID using the event type as a prefix.
func NewEventID(eventType CompileEventType) string {
	prefix := strings.ReplaceAll(string(eventType), "_", "-")
	return prefix + "-" + strings.ToLower(uuid.NewString())
}

// CompileRuntimeInput compiles an envelope with a default-policy compiler.
func CompileRuntimeInput(ctx context.Context, desc CompileDescriptor, in CompileInput) (RuntimeInputEnvelope, error) {
	return New(DefaultPolicy()).CompileRuntimeInput(ctx, desc, in)
}

// CompileRuntimeInput dispatches compilation based on the descriptor event kind.
func (c *Compiler) CompileRuntimeInput(ctx context.Context, desc CompileDescriptor, in CompileInput) (RuntimeInputEnvelope, error) {
	if err := contextError(ctx); err != nil {
		return RuntimeInputEnvelope{}, err
	}
	switch desc.EventKind {
	case EventThreadContract:
		return c.compileThreadContract(desc, in)
	case EventTurnContext:
		return c.compileTurnContext(desc, in)
	case EventDeliveryEnvelope:
		return c.compileDeliveryEnvelope(desc, in)
	default:
		return RuntimeInputEnvelope{}, fmt.Errorf("unsupported compile event kind %q", desc.EventKind)
	}
}

// CompileThreadContract compiles persistent thread instructions with default policy.
func CompileThreadContract(ctx context.Context, eventType CompileEventType, eventID string, in CompileInput) (RuntimeInputEnvelope, error) {
	return New(DefaultPolicy()).CompileThreadContract(ctx, eventType, eventID, in)
}

// CompileThreadContract compiles persistent thread instructions for a runtime.
func (c *Compiler) CompileThreadContract(ctx context.Context, eventType CompileEventType, eventID string, in CompileInput) (RuntimeInputEnvelope, error) {
	if err := contextError(ctx); err != nil {
		return RuntimeInputEnvelope{}, err
	}
	return c.compileThreadContract(CompileDescriptor{
		EventID:   eventID,
		EventKind: EventThreadContract,
		EventType: eventType,
	}, in)
}

// CompileTurnContext compiles per-turn context with default policy.
func CompileTurnContext(ctx context.Context, eventType CompileEventType, eventID string, turnID string, in CompileInput) (RuntimeInputEnvelope, error) {
	return New(DefaultPolicy()).CompileTurnContext(ctx, eventType, eventID, turnID, in)
}

// CompileTurnContext compiles per-turn context for a runtime session.
func (c *Compiler) CompileTurnContext(ctx context.Context, eventType CompileEventType, eventID string, turnID string, in CompileInput) (RuntimeInputEnvelope, error) {
	if err := contextError(ctx); err != nil {
		return RuntimeInputEnvelope{}, err
	}
	return c.compileTurnContext(CompileDescriptor{
		EventID:   eventID,
		EventKind: EventTurnContext,
		EventType: eventType,
		TurnID:    strings.TrimSpace(turnID),
	}, in)
}

// CompileDeliveryEnvelope compiles one delivered message with default policy.
func CompileDeliveryEnvelope(ctx context.Context, eventType CompileEventType, eventID string, in CompileInput) (RuntimeInputEnvelope, error) {
	return New(DefaultPolicy()).CompileDeliveryEnvelope(ctx, eventType, eventID, in)
}

// CompileDeliveryEnvelope compiles one delivered message into a runtime envelope.
func (c *Compiler) CompileDeliveryEnvelope(ctx context.Context, eventType CompileEventType, eventID string, in CompileInput) (RuntimeInputEnvelope, error) {
	if err := contextError(ctx); err != nil {
		return RuntimeInputEnvelope{}, err
	}
	return c.compileDeliveryEnvelope(CompileDescriptor{
		EventID:   eventID,
		EventKind: EventDeliveryEnvelope,
		EventType: eventType,
	}, in)
}

func contextError(ctx context.Context) error {
	if ctx == nil {
		return nil
	}
	return ctx.Err()
}

func (c *Compiler) compileThreadContract(desc CompileDescriptor, in CompileInput) (env RuntimeInputEnvelope, err error) {
	defer func() {
		if recovered := recover(); recovered != nil {
			if recoveredErr, ok := recovered.(error); ok {
				env = RuntimeInputEnvelope{}
				err = recoveredErr
				return
			}
			panic(recovered)
		}
	}()
	agentName := strings.TrimSpace(in.AgentName)
	if agentName == "" {
		agentName = strings.TrimSpace(in.AgentID)
	}

	var blocks []PromptBlock
	blocks = append(blocks, c.mustConstructBlock(desc, "platform.identity", newIdentityPromptInput(IdentityTemplateData{AgentName: agentName, ChannelDisplayName: agentteamDisplayName()})))

	if role := strings.TrimSpace(in.AgentDescription); role != "" {
		blocks = append(blocks, c.mustConstructBlock(desc, "agent_config.initial_role", newInitialRolePromptInput(InitialRoleTemplateData{Description: role})))
	}

	blocks = append(blocks, c.mustConstructBlock(desc, "platform.who_you_are", newEmptyPromptInput(PromptInputIdentity)))

	blocks = append(blocks, c.mustConstructBlock(desc, "platform.cooperation", newEmptyPromptInput(PromptInputIdentity)))

	blocks = append(blocks, c.mustConstructBlock(desc, "platform.agentteam.startup", newPlatformLoopPromptInput(agentteamPlatformPromptData(in.Runtime))))

	if in.SkillPlan.CoreData != nil {
		blocks = append(blocks, c.mustConstructBlock(desc, "platform.agentteam.core", newAgentTeamCorePromptInput(*in.SkillPlan.CoreData)))
	}
	if in.SkillPlan.MemorySearchData != nil {
		blocks = append(blocks, c.mustConstructBlock(desc, "platform.agentteam.memory_search", newMemorySearchGuidancePromptInput(*in.SkillPlan.MemorySearchData)))
	}
	if payload := strings.TrimSpace(in.ExternalSkillCorePayload); payload != "" {
		blocks = append(blocks, c.mustConstructBlock(desc, "platform.agentteam.external_skill_core", newExternalSkillCorePromptInput(ExternalSkillPayloadTemplateData{SkillText: payload})))
	}

	blocks = append(blocks, c.mustConstructBlock(desc, "platform.agentteam.task_boards", newEmptyPromptInput(PromptInputTask)))
	blocks = append(blocks, c.mustConstructBlock(desc, "platform.agentteam.review_context", newPlatformLoopPromptInput(agentteamPlatformPromptData(in.Runtime))))

	if in.Exec.Enabled && !in.Exec.Guidance.Empty() {
		blocks = append(blocks, c.mustConstructBlock(desc, "platform.agentteam.exec", newExecGuidancePromptInput(in.Exec.Guidance)))
	}
	if in.Extension.Enabled && !in.Extension.Guidance.Empty() {
		blocks = append(blocks, c.mustConstructBlock(desc, "platform.agentteam.extension", newExtensionCatalogGuidancePromptInput(in.Extension.Guidance)))
	}

	memoryPrompt := resolveMemoryPrompt(in.WorkDir)
	blocks = append(blocks, c.mustConstructBlock(desc, "platform.workspace_memory", newWorkspaceMemoryPromptInput(WorkspaceMemoryTemplateData{MemoryIndexPath: memoryPrompt.IndexPath, MemoryRootAbsPath: remoteMemoryRootDisplayPath(in.WorkDir), MemoryRootRelPath: remoteMemoryRootPath + "/", Remote: memoryPrompt.Kind == "remote"})))
	blocks = append(blocks, c.mustConstructBlock(desc, "platform.agent_identity", newEmptyPromptInput(PromptInputIdentity)))

	identityContent, identityPath, _, identityByteOver, err := c.readIdentityDoc(in.WorkDir)
	if err != nil {
		return RuntimeInputEnvelope{}, err
	}
	identityConstraints := byteOversizeConstraint(identityByteOver)
	if identityContent != "" {
		identityText, ctConstraints, ok := c.enforceDocBudget(identityContent, buildIdentityDocText, docBudget{
			CharBudget:  c.policy.MaxIdentityDocCharacters,
			TokenBudget: c.policy.MaxBlockTokens,
		})
		identityConstraints = append(identityConstraints, ctConstraints...)
		if ok && identityText != "" {
			blocks = append(blocks, c.mustConstructBlock(desc, "workspace.identity", newWorkspaceIdentityPromptInput(WorkspaceIdentityTemplateData{Content: identityText, AlreadyWrapped: true})))
		}
	}
	if len(identityConstraints) > 0 {
		blocks = append(blocks, c.validateConstructedBlock(desc, c.oversizeNoticeBlock(identityConstraints, identityPath, "", "workspace.identity")))
	}

	if !runtimeUsesNativeContractCarrier(in.Runtime) {
		agentsDoc, _, agentsByteOver, err := c.readWorkspaceDoc(in.WorkDir, "AGENTS.md")
		if err != nil {
			return RuntimeInputEnvelope{}, err
		}
		agentsConstraints := byteOversizeConstraint(agentsByteOver)
		if agentsDoc != "" {
			agentsText, ctConstraints, ok := c.enforceDocBudget(agentsDoc, buildAgentsDocText, docBudget{
				TokenBudget: c.policy.MaxBlockTokens,
			})
			agentsConstraints = append(agentsConstraints, ctConstraints...)
			if ok && agentsText != "" {
				blocks = append(blocks, c.mustConstructBlock(desc, "agent_config.agents_md", newWorkspaceDocumentPromptInput(WorkspaceDocumentTemplateData{Content: strings.TrimPrefix(agentsText, "## Agent instructions\n\n")})))
			}
		}
		if len(agentsConstraints) > 0 {
			blocks = append(blocks, c.validateConstructedBlock(desc, c.oversizeNoticeBlock(agentsConstraints, "AGENTS.md", "", "agent_config.agents_md")))
		}
	}

	blocks = normalizeBlocks(blocks)
	bundle := PromptBundle{
		SchemaVersion: SchemaVersion,
		EventID:       strings.TrimSpace(desc.EventID),
		EventType:     desc.EventType,
		TurnID:        strings.TrimSpace(desc.TurnID),
		AgentID:       strings.TrimSpace(in.AgentID),
		Blocks:        blocks,
	}
	bundle.Fingerprint = fingerprintBundle(bundle, desc.EventKind)
	return c.validatedEnvelope(RuntimeInputEnvelope{
		SchemaVersion: SchemaVersion,
		EventID:       bundle.EventID,
		EventType:     desc.EventType,
		TurnID:        bundle.TurnID,
		AgentID:       bundle.AgentID,
		EventKind:     EventThreadContract,
		Bundle:        &bundle,
		Fingerprint:   fingerprintEnvelope(EventThreadContract, desc.EventType, bundle.Fingerprint),
	})
}

func (c *Compiler) compileTurnContext(desc CompileDescriptor, in CompileInput) (env RuntimeInputEnvelope, err error) {
	defer func() {
		if recovered := recover(); recovered != nil {
			if recoveredErr, ok := recovered.(error); ok {
				env = RuntimeInputEnvelope{}
				err = recoveredErr
				return
			}
			panic(recovered)
		}
	}()
	if !in.PromptFacts.initialized() {
		in.PromptFacts = normalizeMessagePromptData(strings.TrimSpace(in.Message.Target), in.Message)
	}
	replyContext := messageReplyContext(desc.EventType, in)
	var blocks []PromptBlock
	if noticeData, _ := loadIdentityNoticeTemplateData(in.WorkDir); noticeData.CurrentName != "" {
		blocks = append(blocks, c.mustConstructBlock(desc, "notice.identity", newIdentityNoticePromptInput(noticeData)))
	}

	switch desc.EventType {
	case EventFreshStart:
		blocks = append(blocks, c.referenceBlocks(desc, in, 20)...)
		if data, ok := startUserInputTemplateData(in.Runtime, in.Binding, in.PromptFacts); ok {
			blocks = append(blocks, c.mustConstructBlock(desc, "event.current_message", newCurrentMessagePromptInput(data)))
		}
	case EventWake:
		blocks = append(blocks, c.referenceBlocks(desc, in, 20)...)
		if messagePromptHasExplicit(in.PromptFacts) && in.PromptFacts.Author.ActorType != "system" {
			blocks = append(blocks, c.mustConstructBlock(desc, "event.wake_context", newWakeContextPromptInput(wakeContextTemplateData(in.Runtime, in.Binding, in.PromptFacts, toolNamePrefix(in.Runtime, "agentteam")("message_send"), toolNamePrefix(in.Runtime, "agentteam")("message_send"), ""))))
		} else if unread := unreadSummaryTemplateData(in.UnreadSummary); len(unread.Channels) > 0 {
			blocks = append(blocks, c.mustConstructBlock(desc, "event.unread_summary", newUnreadSummaryPromptInput(unread)))
		} else {
			blocks = append(blocks, c.mustConstructBlock(desc, "event.resume_context", newResumeContextPromptInput(ResumeContextTemplateData{SendMessageTool: toolNamePrefix(in.Runtime, "agentteam")("message_send")})))
		}
	case EventResume:
		blocks = append(blocks, c.referenceBlocks(desc, in, 20)...)
		if messagePromptHasExplicit(in.PromptFacts) && in.PromptFacts.Author.ActorType != "system" {
			wake := wakeContextTemplateData(in.Runtime, in.Binding, in.PromptFacts, toolNamePrefix(in.Runtime, "agentteam")("message_send"), toolNamePrefix(in.Runtime, "agentteam")("message_send"), in.MemoryResumeReminder)
			blocks = append(blocks, c.mustConstructBlock(desc, "event.resume_context", newResumeContextPromptInput(ResumeContextTemplateData{Message: wake.Message, ThreadContext: wake.ThreadContext, ActiveTaskTarget: wake.ActiveTaskTarget, AgentAuthoredWakeGuidance: wake.AgentAuthoredWakeGuidance, SendMessageTool: wake.SendMessageTool, MemoryResumeReminder: wake.MemoryResumeReminder})))
		} else if unread := unreadSummaryTemplateData(in.UnreadSummary); len(unread.Channels) > 0 {
			blocks = append(blocks, c.mustConstructBlock(desc, "event.unread_summary", newUnreadSummaryPromptInput(unread)))
		} else {
			blocks = append(blocks, c.mustConstructBlock(desc, "event.resume_context", newResumeContextPromptInput(ResumeContextTemplateData{SendMessageTool: toolNamePrefix(in.Runtime, "agentteam")("message_send")})))
		}
	case EventDeferredContinuation:
		blocks = append(blocks, c.referenceBlocks(desc, in, 20)...)
		replyTool := toolNamePrefix(in.Runtime, "agentteam")("message_send")
		discussionReplyTool := toolNamePrefix(in.Runtime, "agentteam")("message_send")
		data := deferredContinuationTemplateData(in.PromptFacts, in.Delivery.PendingBehind, replyTool, discussionReplyTool, in.MemoryResumeReminder)
		data.AgentAuthoredWakeGuidance = formatAgentAuthoredWakeGuidanceData(in.PromptFacts)
		blocks = append(blocks, c.mustConstructBlock(desc, "event.deferred_continuation", newDeferredContinuationPromptInput(data)))
	default:
		return RuntimeInputEnvelope{}, fmt.Errorf("unsupported turn context event type %q", desc.EventType)
	}
	if attachmentID, attachmentInput, ok := attachmentPromptBlockInput(in.Attachments); ok {
		blocks = append(blocks, c.mustConstructBlock(desc, attachmentID, attachmentInput))
	}
	if block, ok := c.deliveryContractTurnBlock(desc, in, 25); ok {
		blocks = append(blocks, block)
	}

	blocks = normalizeBlocks(blocks)
	bundle := PromptBundle{
		SchemaVersion: SchemaVersion,
		EventID:       strings.TrimSpace(desc.EventID),
		EventType:     desc.EventType,
		TurnID:        strings.TrimSpace(desc.TurnID),
		AgentID:       strings.TrimSpace(in.AgentID),
		Blocks:        blocks,
		ReplyContext:  replyContext,
	}
	bundle.Fingerprint = fingerprintBundle(bundle, desc.EventKind)
	taskPayloadDigest, taskPayloadPresent := controlledTaskPayloadIdentity(in.Binding, in.Message)
	return c.validatedEnvelope(RuntimeInputEnvelope{
		SchemaVersion:      SchemaVersion,
		EventID:            bundle.EventID,
		EventType:          desc.EventType,
		TurnID:             bundle.TurnID,
		AgentID:            bundle.AgentID,
		EventKind:          EventTurnContext,
		Bundle:             &bundle,
		Fingerprint:        fingerprintEnvelope(EventTurnContext, desc.EventType, bundle.Fingerprint),
		TaskPayloadPresent: taskPayloadPresent,
		TaskPayloadDigest:  taskPayloadDigest,
	})
}

func messageReplyContext(eventType CompileEventType, in CompileInput) *ReplyContext {
	messageID := strings.TrimSpace(in.Message.MessageID)
	if messageID == "" {
		return nil
	}
	target := strings.TrimSpace(in.Message.Target)
	if target == "" && eventType != EventDeferredContinuation {
		target = strings.TrimSpace(in.Binding.TargetID)
		if target == "" {
			target = strings.TrimSpace(in.Binding.ChannelID)
		}
	} else if target == "" {
		target = inferredTarget(in.Message)
	}
	authorityMessage := in.Message
	authorityMessage.Target = target
	if !types.MessageHasReplyAuthority(authorityMessage) {
		return nil
	}
	parsed, err := domain.ParseMessageTargetReference(target)
	if err != nil {
		return nil
	}
	return &ReplyContext{Target: parsed.String()}
}

func (c *Compiler) deliveryContractTurnBlock(desc CompileDescriptor, in CompileInput, priority int) (PromptBlock, bool) {
	return c.mustConstructBlock(desc, "platform.agentteam.visible_reply_contract", newVisibleReplyPromptInput(VisibleReplyContractTemplateData{SendMessageTool: toolNamePrefix(in.Runtime, "agentteam")("message_send")})), true
}
func (c *Compiler) compileDeliveryEnvelope(desc CompileDescriptor, in CompileInput) (RuntimeInputEnvelope, error) {
	promptFacts := in.Delivery.PromptFacts
	if !promptFacts.initialized() {
		promptFacts = normalizeDeliveryPromptData(in.Delivery)
	}
	metadata := cloneMetadata(in.Delivery.Metadata)
	noticeData, noticeVersion := loadIdentityNoticeTemplateData(in.WorkDir)
	if strings.TrimSpace(in.Delivery.Timestamp) != "" {
		metadata["timestamp"] = strings.TrimSpace(in.Delivery.Timestamp)
	}
	if noticeData.CurrentName != "" {
		metadata[identityNoticeVersionMetadataKey] = noticeVersion
	}
	deliveryID := strings.TrimSpace(in.Delivery.MessageID)
	if nestedDeliveryID := deliveryMetadataValue(metadata, "deliveryId"); nestedDeliveryID != "" {
		deliveryID = nestedDeliveryID
	}
	attachments := normalizeDeliveryAttachments(in.Delivery)
	activeTaskTarget := SelectPromptActiveTaskTarget(types.ActiveTaskTarget{}, promptFactsInboxMessage(promptFacts))
	var activeTaskTargetPayload *types.ActiveTaskTarget
	if activeTaskTarget.Valid() {
		activeTaskTargetPayload = &activeTaskTarget
	}
	replyAuthorityMessage := types.InboxMessage{
		MessageID:   strings.TrimSpace(in.Delivery.MessageID),
		Target:      strings.TrimSpace(in.Delivery.Target),
		MessageType: strings.TrimSpace(in.Delivery.MessageType),
		AuthorProfile: types.AuthorProfile{
			ActorType:   strings.TrimSpace(in.Delivery.AuthorIdentity.ActorType),
			PrincipalID: strings.TrimSpace(in.Delivery.AuthorIdentity.PrincipalID),
			MemberID:    strings.TrimSpace(in.Delivery.AuthorIdentity.MemberID),
			TargetID:    strings.TrimSpace(in.Delivery.AuthorIdentity.TargetID),
			DisplayName: strings.TrimSpace(in.Delivery.AuthorIdentity.DisplayName),
		},
		WorkflowContext: in.Delivery.WorkflowContext,
	}
	delivery := DeliveryEnvelope{
		SourceKind: DeliverySourceRoutedMessage,
		Binding:    buildConversationBinding(in.Delivery.Target, in.Delivery.ChannelType),
		Transport: DeliveryTransportBinding{
			DeliveryMode:     strings.TrimSpace(in.Delivery.DeliveryMode),
			RuntimeSessionID: strings.TrimSpace(in.Delivery.RuntimeSessionID),
			ThreadID:         strings.TrimSpace(in.Delivery.ThreadID),
			ActiveTurnID:     strings.TrimSpace(in.Delivery.ActiveTurnID),
			WakeToken:        strings.TrimSpace(in.Delivery.WakeToken),
			Attempt:          in.Delivery.Attempt,
			DeferredBatchID:  strings.TrimSpace(in.Delivery.DeferredBatchID),
		},
		Correlation: DeliveryCorrelation{
			AgentID:         strings.TrimSpace(in.AgentID),
			MessageID:       strings.TrimSpace(in.Delivery.MessageID),
			DeliveryID:      deliveryID,
			SourceMessageID: deliveryMetadataValue(metadata, "targetMessageId"),
			TargetKey:       strings.TrimSpace(in.Delivery.TargetKey),
			TargetSeq:       in.Delivery.TargetSeq,
			WakeToken:       strings.TrimSpace(in.Delivery.WakeToken),
		},
		Replay: DeliveryReplayContext{
			ReplayState:       strings.TrimSpace(in.Delivery.ReplayState),
			DeferredReason:    strings.TrimSpace(in.Delivery.DeferredReason),
			AcceptedAt:        in.Delivery.AcceptedAt,
			OriginalOrderKey:  strings.TrimSpace(in.Delivery.OriginalOrderKey),
			PriorAttemptCount: in.Delivery.PriorAttemptCount,
		},
		EvidenceClass:       in.Delivery.EvidenceClass,
		AuthorMentionName:   promptDeliveryAuthorMentionName(in.Delivery.AuthorIdentity),
		AuthorDisplayName:   strings.TrimSpace(in.Delivery.AuthorIdentity.DisplayName),
		MessageType:         strings.TrimSpace(in.Delivery.MessageType),
		Timestamp:           strings.TrimSpace(in.Delivery.Timestamp),
		PayloadText:         in.Delivery.Content,
		Attachments:         attachments,
		ResolvedAttachments: append([]types.RuntimeAttachment(nil), in.Delivery.ResolvedAttachments...),
		AttachmentFailure:   cloneAttachmentFailure(in.Delivery.AttachmentFailure),
		Metadata:            metadata,
		WorkflowContext: DeliveryWorkflowContext{
			Available: in.Delivery.WorkflowContext.Normalize().Valid(),
			RunID:     in.Delivery.WorkflowContext.Normalize().RunID,
			StepID:    in.Delivery.WorkflowContext.Normalize().EffectID,
		},
		ActiveTaskTarget:   activeTaskTargetPayload,
		TeamContexts:       cloneTeamContexts(in.Delivery.TeamContexts),
		PromptFacts:        promptFacts,
		IdentityNoticeData: noticeData,
		ReplyUnavailable:   !types.MessageHasReplyAuthority(replyAuthorityMessage),
	}
	if desc.EventType == EventReactionDelivery {
		delivery.SourceKind = DeliverySourceReaction
	}
	delivery.Fingerprint = fingerprintDelivery(desc.EventType, delivery)
	taskPayloadDigest, taskPayloadPresent := controlledDeliveryPayloadIdentity(in.Delivery)
	return c.validatedEnvelope(RuntimeInputEnvelope{
		SchemaVersion:      SchemaVersion,
		EventID:            strings.TrimSpace(desc.EventID),
		EventType:          desc.EventType,
		TurnID:             strings.TrimSpace(desc.TurnID),
		AgentID:            strings.TrimSpace(in.AgentID),
		EventKind:          EventDeliveryEnvelope,
		Delivery:           &delivery,
		Fingerprint:        fingerprintEnvelope(EventDeliveryEnvelope, desc.EventType, delivery.Fingerprint),
		TaskPayloadPresent: taskPayloadPresent,
		TaskPayloadDigest:  taskPayloadDigest,
	})
}

func (c *Compiler) validatedEnvelope(env RuntimeInputEnvelope) (RuntimeInputEnvelope, error) {
	if err := validateRuntimeInputEnvelope(env); err != nil {
		return RuntimeInputEnvelope{}, err
	}
	return env, nil
}

func deliveryMetadataValue(metadata map[string]any, key string) string {
	if value := promptStringMapValue(metadata, key); value != "" {
		return value
	}
	rawReaction, ok := metadata["reaction"].(map[string]any)
	if !ok {
		return ""
	}
	return promptStringMapValue(rawReaction, key)
}

func normalizeDeliveryAttachments(input DeliveryCompileInput) []types.MessageAttachment {
	return types.NormalizeMessageAttachments(input.Attachments)
}

func cloneAttachmentFailure(failure *AttachmentPreparationFailure) *AttachmentPreparationFailure {
	if failure == nil {
		return nil
	}
	return &AttachmentPreparationFailure{
		Attachments: append([]types.MessageAttachment(nil), failure.Attachments...),
		Error:       strings.TrimSpace(failure.Error),
	}
}

func cloneTeamContexts(contexts []types.TeamContext) []types.TeamContext {
	if len(contexts) == 0 {
		return nil
	}
	cloned := make([]types.TeamContext, len(contexts))
	for i, context := range contexts {
		cloned[i] = context
		if len(context.MemberRoster) > 0 {
			cloned[i].MemberRoster = append([]types.TeamRosterMember(nil), context.MemberRoster...)
		}
	}
	return cloned
}

// docByteOversize records that a workspace document exceeded the workspace-doc
// byte budget and was truncated to fit so the agent could still start.
type docByteOversize struct {
	ActualBytes int
	LimitBytes  int
}

// readWorkspaceDoc reads a workspace document under the byte budget. The byte
// budget is a soft ceiling: a document larger than the budget is bounded-read
// (so a pathologically large file can never blow up memory) and truncated on a
// rune boundary to fit, returning a docByteOversize so the caller can inject a
// notice. It never fails on size — workspace documents degrade gracefully.
func (c *Compiler) readWorkspaceDoc(workDir, relativePath string) (string, string, *docByteOversize, error) {
	if strings.TrimSpace(workDir) == "" {
		return "", "", nil, nil
	}
	path := filepath.Join(workDir, relativePath)
	info, err := os.Stat(path)
	if err != nil {
		if os.IsNotExist(err) {
			return "", "", nil, nil
		}
		return "", "", nil, err
	}
	budget := c.policy.MaxWorkspaceDocBytes
	f, err := os.Open(path)
	if err != nil {
		return "", "", nil, err
	}
	data, err := io.ReadAll(io.LimitReader(f, int64(budget)+1))
	f.Close()
	if err != nil {
		return "", "", nil, err
	}
	var byteOversize *docByteOversize
	// Normalize to valid UTF-8 so an invalid byte or a truncation-split rune
	// never reaches the prompt, and so the source version/fingerprint describe
	// the same text that is actually delivered.
	normalized := []byte(strings.ToValidUTF8(string(data), "�"))
	if info.Size() > int64(budget) {
		normalized = truncateBytesToRuneBoundary(normalized, budget)
		byteOversize = &docByteOversize{ActualBytes: int(info.Size()), LimitBytes: budget}
	}
	trimmed := strings.TrimSpace(string(normalized))
	// Return byteOversize even when the trimmed content is empty: a document
	// whose first budget-bytes are all whitespace was still over budget and still
	// had meaningful content truncated away, so the caller must be able to notice.
	return trimmed, versionForContent(normalized), byteOversize, nil
}

// truncateBytesToRuneBoundary cuts data to at most budget bytes on a valid UTF-8
// rune boundary so truncation never splits a multibyte character.
func truncateBytesToRuneBoundary(data []byte, budget int) []byte {
	if budget <= 0 || len(data) <= budget {
		return data
	}
	cut := budget
	for cut > 0 && !utf8.RuneStart(data[cut]) {
		cut--
	}
	return data[:cut]
}

// readIdentityDoc reads the persistent identity document. It applies only the
// shared workspace-doc byte ceiling; the product character budget and the block
// token budget are enforced later by enforceDocBudget, so an oversized identity
// degrades gracefully instead of blocking compilation.
func (c *Compiler) readIdentityDoc(workDir string) (string, string, string, *docByteOversize, error) {
	docPath := identitydoc.ActiveIdentityPath
	content, version, byteOversize, err := c.readWorkspaceDoc(workDir, docPath)
	if err != nil {
		return "", docPath, "", nil, err
	}
	return content, docPath, version, byteOversize, nil
}

// docBudget is the set of soft size budgets enforceDocBudget keeps a workspace
// document within. A zero field means "not enforced".
type docBudget struct {
	CharBudget  int // product character budget (identity); 0 = none
	TokenBudget int // materialized block token budget (MaxBlockTokens); 0 = none
}

// oversizeConstraint describes one budget that a document exceeded. A document
// can exceed several budgets at once (e.g. bytes and characters), so the notice
// lists every constraint that bound, giving the agent all targets it must meet.
type oversizeConstraint struct {
	Actual int
	Limit  int
	Unit   string // "characters", "tokens", or "bytes"
}

// enforceDocBudget wraps content with wrap and trims the content so the wrapped
// block fits the character budget (when set) and the block token budget (when
// set). It returns the wrapped block text, the budgets that were exceeded (and
// thus caused truncation), and ok. ok is false only when the wrapper's own
// overhead already exceeds the token budget — i.e. the document cannot be
// materialized within budget at all — in which case the caller must skip the doc
// block but should still inject the notice. The token-budget post-condition
// (wrapped text fits the budget) is strictly guaranteed whenever ok is true.
// enforceDocBudget never returns an error: oversized documents degrade
// gracefully so prompt compilation never blocks on length.
func (c *Compiler) enforceDocBudget(content string, wrap func(string) string, budget docBudget) (string, []oversizeConstraint, bool) {
	content = strings.TrimSpace(content)
	if content == "" {
		return "", nil, true
	}
	var constraints []oversizeConstraint
	originalRunes := utf8.RuneCountInString(content)
	if budget.CharBudget > 0 && originalRunes > budget.CharBudget {
		content = truncateDocToBudget(content, budget.CharBudget)
		constraints = append(constraints, oversizeConstraint{Actual: originalRunes, Limit: budget.CharBudget, Unit: "characters"})
	}
	wrapped := wrap(content)
	if budget.TokenBudget > 0 && EstimateTokens(wrapped) > budget.TokenBudget {
		originalTokens := EstimateTokens(wrapped)
		content = fitDocToTokenBudget(content, wrap, budget.TokenBudget)
		wrapped = wrap(content)
		constraints = append(constraints, oversizeConstraint{Actual: originalTokens, Limit: budget.TokenBudget, Unit: "tokens"})
	}
	if budget.TokenBudget > 0 && EstimateTokens(wrapped) > budget.TokenBudget {
		// The wrapper's own overhead exceeds the budget: even empty content
		// cannot fit. Mark not-ok so the caller drops the block but keeps the
		// notice telling the agent to shorten the document.
		return wrapped, constraints, false
	}
	return wrapped, constraints, true
}

// truncateDocToBudget shrinks content to at most limit Unicode runes by keeping
// the longest rune prefix that fits. It hard-cuts at the limit instead of
// backing up to an earlier line boundary, so a document only marginally over
// budget never loses almost all of its content just to land on a newline.
func truncateDocToBudget(content string, limit int) string {
	if limit <= 0 {
		return content
	}
	runes := []rune(content)
	if len(runes) <= limit {
		return content
	}
	return strings.TrimSpace(string(runes[:limit]))
}

// fitDocToTokenBudget trims content so wrap(content) fits tokenBudget tokens.
// It estimates the wrapper overhead, cuts to a rune budget that reserves room
// for it, then verifies the post-condition and trims rune-by-rune until it
// holds. The guarantee is bounded: if the wrapper's own overhead already exceeds
// the budget, the agentteam terminates at empty content and the caller (via
// enforceDocBudget) must treat the result as not materializable rather than emit
// a block that would still exceed the budget. EstimateTokens is ceil(runes/4).
func fitDocToTokenBudget(content string, wrap func(string) string, tokenBudget int) string {
	if tokenBudget <= 0 || EstimateTokens(wrap(content)) <= tokenBudget {
		return content
	}
	// Probe with a non-empty payload because some wrappers return "" for empty
	// content and would undercount their own overhead.
	probe := "x"
	overhead := utf8.RuneCountInString(wrap(probe)) - utf8.RuneCountInString(probe)
	if overhead < 0 {
		overhead = 0
	}
	maxRunes := tokenBudget*4 - overhead
	if maxRunes < 1 {
		maxRunes = tokenBudget * 4
	}
	content = truncateDocToBudget(content, maxRunes)
	for EstimateTokens(wrap(content)) > tokenBudget {
		runes := []rune(content)
		if len(runes) == 0 {
			break
		}
		content = strings.TrimSpace(string(runes[:len(runes)-1]))
	}
	return content
}

// byteOversizeConstraint converts a byte-budget overrun into a notice
// constraint list (nil when there was no overrun).
func byteOversizeConstraint(byteOver *docByteOversize) []oversizeConstraint {
	if byteOver == nil {
		return nil
	}
	return []oversizeConstraint{{Actual: byteOver.ActualBytes, Limit: byteOver.LimitBytes, Unit: "bytes"}}
}

// oversizeNoticeBlock renders the persistent notice injected alongside a
// workspace document that had to be truncated, asking the agent to shorten the
// on-disk document so future compiles stop truncating it.
func (c *Compiler) oversizeNoticeBlock(constraints []oversizeConstraint, docPath, docTitle, sourceID string) PromptBlock {
	id := "notice." + sourceID + "_oversize"
	block, err := DefaultPromptBlockRegistry().Construct(id, newOversizePromptInput(WorkspaceDocOversizeTemplateData{DocTitle: docTitle, SourceID: sourceID, Path: docPath, Constraints: workspaceDocConstraintTemplateItems(constraints)}))
	if err != nil {
		panic(err)
	}
	return block
}

func loadIdentityNoticeTemplateData(workDir string) (IdentityNoticeTemplateData, string) {
	if strings.TrimSpace(workDir) == "" {
		return IdentityNoticeTemplateData{}, ""
	}
	pending, ok, err := identitynotice.Load(workDir)
	if err != nil || !ok {
		return IdentityNoticeTemplateData{}, ""
	}
	data := IdentityNoticeTemplateData{CurrentName: pending.CurrentName, OldName: pending.OldName}
	return data, prompts.Version(identitynotice.PromptTemplatePath) + ":" + versionForContent([]byte(pending.CurrentName+"\n"+pending.OldName))
}

func (c *Compiler) referenceBlocks(desc CompileDescriptor, in CompileInput, basePriority int) []PromptBlock {
	_ = basePriority
	blocks := make([]PromptBlock, 0, 2)
	if in.SkillPlan.ReferenceData != nil {
		blocks = append(blocks, c.mustConstructBlock(desc, "platform.agentteam.reference", newLoopReferencePromptInput(*in.SkillPlan.ReferenceData)))
	}
	if payload := strings.TrimSpace(in.ExternalSkillReferencePayload); payload != "" {
		blocks = append(blocks, c.mustConstructBlock(desc, "platform.agentteam.external_skill_reference", newExternalSkillReferencePromptInput(ExternalSkillPayloadTemplateData{SkillText: payload})))
	}
	return blocks
}

func normalizeBlocks(blocks []PromptBlock) []PromptBlock {
	if len(blocks) == 0 {
		return nil
	}
	seen := make(map[string]PromptBlock, len(blocks))
	order := make([]string, 0, len(blocks))
	for _, block := range blocks {
		if strings.TrimSpace(block.ID) == "" {
			continue
		}
		if _, ok := seen[block.ID]; !ok {
			order = append(order, block.ID)
		}
		seen[block.ID] = block
	}
	result := make([]PromptBlock, 0, len(order))
	for _, id := range order {
		result = append(result, seen[id])
	}
	sort.SliceStable(result, func(i, j int) bool {
		if result[i].Slot != result[j].Slot {
			return result[i].Slot < result[j].Slot
		}
		if result[i].Priority != result[j].Priority {
			return result[i].Priority < result[j].Priority
		}
		return result[i].ID < result[j].ID
	})
	return result
}

// InstructionFingerprint returns a stable fingerprint for rendered instruction text.
func InstructionFingerprint(baseInstructions, developerInstructions string) string {
	base := strings.TrimSpace(baseInstructions)
	developer := strings.TrimSpace(developerInstructions)
	if base == "" && developer == "" {
		return ""
	}
	return shortFingerprint(base + "\x00" + developer)
}

// ThreadContractInstructionsFingerprint returns the instruction fingerprint for a thread contract.
func ThreadContractInstructionsFingerprint(input RuntimeInputEnvelope) (string, error) {
	if err := validateRuntimeInputEnvelope(input); err != nil {
		return "", err
	}
	if input.EventKind != EventThreadContract || input.Bundle == nil {
		return "", fmt.Errorf("instruction fingerprint requires a thread-contract bundle")
	}
	return InstructionFingerprint(
		renderSlot(input.Bundle.Blocks, SlotBase),
		renderSlot(input.Bundle.Blocks, SlotDeveloper),
	), nil
}

func versionForContent(content []byte) string {
	sum := sha256.Sum256(content)
	return hex.EncodeToString(sum[:8])
}

func fingerprintBundle(bundle PromptBundle, kind CompileEventKind) string {
	var b strings.Builder
	b.WriteString(bundle.AgentID)
	b.WriteByte(0)
	b.WriteString(string(kind))
	b.WriteByte(0)
	b.WriteString(string(bundle.EventType))
	for _, block := range bundle.Blocks {
		blockText := block.text
		sourceVersion := block.Source.Version
		b.WriteByte(0)
		b.WriteString(block.ID)
		b.WriteByte(0)
		b.WriteString(sourceVersion)
		b.WriteByte(0)
		b.WriteString(string(block.Type))
		b.WriteByte(0)
		b.WriteString(string(block.Layer))
		b.WriteByte(0)
		b.WriteString(string(block.Slot))
		b.WriteByte(0)
		b.WriteString(string(block.Durability))
		b.WriteByte(0)
		b.WriteString(fmt.Sprintf("%d", block.Priority))
		b.WriteByte(0)
		b.WriteString(versionForContent([]byte(blockText)))
	}
	return shortFingerprint(b.String())
}

func fingerprintDelivery(eventType CompileEventType, delivery DeliveryEnvelope) string {
	var b strings.Builder
	b.WriteString(string(eventType))
	b.WriteByte(0)
	b.WriteString(string(delivery.SourceKind))
	b.WriteByte(0)
	b.WriteString(delivery.Binding.Target)
	b.WriteByte(0)
	b.WriteString(delivery.Binding.ThreadRef)
	b.WriteByte(0)
	b.WriteString(delivery.Binding.ScopeKind)
	b.WriteByte(0)
	b.WriteString(delivery.Transport.DeliveryMode)
	b.WriteByte(0)
	b.WriteString(delivery.Transport.RuntimeSessionID)
	b.WriteByte(0)
	b.WriteString(delivery.Transport.ThreadID)
	b.WriteByte(0)
	b.WriteString(delivery.Transport.ActiveTurnID)
	b.WriteByte(0)
	b.WriteString(delivery.Transport.WakeToken)
	b.WriteByte(0)
	b.WriteString(fmt.Sprintf("%d", delivery.Transport.Attempt))
	b.WriteByte(0)
	b.WriteString(delivery.Transport.DeferredBatchID)
	b.WriteByte(0)
	b.WriteString(delivery.Correlation.MessageID)
	b.WriteByte(0)
	b.WriteString(delivery.Correlation.DeliveryID)
	b.WriteByte(0)
	b.WriteString(delivery.Correlation.SourceMessageID)
	b.WriteByte(0)
	if delivery.Correlation.TargetKey != "" || delivery.Correlation.TargetSeq > 0 {
		b.WriteString(delivery.Correlation.TargetKey)
		b.WriteByte(0)
		b.WriteString(fmt.Sprintf("%d", delivery.Correlation.TargetSeq))
		b.WriteByte(0)
	}
	b.WriteString(delivery.Correlation.AgentID)
	b.WriteByte(0)
	b.WriteString(delivery.Correlation.WakeToken)
	b.WriteByte(0)
	b.WriteString(delivery.Replay.ReplayState)
	b.WriteByte(0)
	b.WriteString(delivery.Replay.DeferredReason)
	b.WriteByte(0)
	b.WriteString(delivery.Replay.AcceptedAt.UTC().Format(time.RFC3339Nano))
	b.WriteByte(0)
	b.WriteString(delivery.Replay.OriginalOrderKey)
	b.WriteByte(0)
	b.WriteString(fmt.Sprintf("%d", delivery.Replay.PriorAttemptCount))
	b.WriteByte(0)
	b.WriteString(string(delivery.EvidenceClass))
	b.WriteByte(0)
	if delivery.AuthorMentionName != "" {
		b.WriteString(delivery.AuthorMentionName)
		b.WriteByte(0)
	}
	b.WriteString(delivery.AuthorDisplayName)
	b.WriteByte(0)
	b.WriteString(delivery.MessageType)
	b.WriteByte(0)
	if delivery.ReplyUnavailable {
		b.WriteString("reply-unavailable")
		b.WriteByte(0)
	}
	b.WriteString(versionForContent([]byte(delivery.PayloadText)))
	b.WriteByte(0)
	for _, attachment := range delivery.Attachments {
		b.WriteString(strings.TrimSpace(attachment.ID))
		b.WriteByte(0)
		b.WriteString(strings.TrimSpace(attachment.Filename))
		b.WriteByte(0)
	}
	if len(delivery.TeamContexts) > 0 {
		b.WriteString(jsonValueFingerprint(delivery.TeamContexts))
		b.WriteByte(0)
	}
	if delivery.WorkflowContext.Available {
		b.WriteString("workflow-context")
		b.WriteByte(0)
	}
	b.WriteString(jsonValueFingerprint(delivery.Metadata))
	return shortFingerprint(b.String())
}
func fingerprintEnvelope(kind CompileEventKind, eventType CompileEventType, core string) string {
	return shortFingerprint(string(kind) + "\x00" + string(eventType) + "\x00" + core)
}

func shortFingerprint(value string) string {
	sum := sha256.Sum256([]byte(value))
	return hex.EncodeToString(sum[:16])
}

func buildConversationBinding(target, channelType string) ConversationBindingRef {
	trimmedTarget := strings.TrimSpace(target)
	ref := ConversationBindingRef{
		Target:    trimmedTarget,
		ScopeKind: "channel",
	}
	if parsed, err := domain.ParseMessageTargetReference(trimmedTarget); err == nil {
		switch parsed.Kind {
		case domain.MessageTargetKindDM:
			ref.ScopeKind = "dm"
		case domain.MessageTargetKindReplyThread:
			ref.ScopeKind = "thread"
			ref.ThreadRef = parsed.String()
		case domain.MessageTargetKindTask:
			ref.ScopeKind = "task"
		case domain.MessageTargetKindWorkflow:
			ref.ScopeKind = "workflow"
		case domain.MessageTargetKindTeam:
			if strings.TrimSpace(parsed.Team.ShortID) != "" {
				ref.ScopeKind = "thread"
				ref.ThreadRef = parsed.String()
			}
		}
		return ref
	}
	// channelType is a trusted fallback for canonical storage projections, not
	// a way to reinterpret malformed author-facing syntax as another kind.
	if strings.HasPrefix(strings.ToLower(trimmedTarget), "workflow:") {
		ref.ScopeKind = ""
		return ref
	}
	if strings.HasPrefix(trimmedTarget, "#") || strings.HasPrefix(strings.ToLower(trimmedTarget), "dm:@") {
		return ref
	}
	switch strings.TrimSpace(channelType) {
	case "task":
		ref.ScopeKind = "task"
	case "dm":
		ref.ScopeKind = "dm"
	case "thread":
		ref.ScopeKind = "thread"
		ref.ThreadRef = trimmedTarget
	case "workflow":
		ref.ScopeKind = ""
	}
	return ref
}

func normalizedChannelID(channelType string) string {
	trimmed := strings.TrimSpace(channelType)
	if trimmed == "" {
		return "unknown"
	}
	return strings.ReplaceAll(trimmed, " ", "_")
}

// EstimateTokens exposes the compiler's lightweight token estimator so callers
// can persist metrics using the same approximation used for block validation.
func EstimateTokens(text string) int {
	if strings.TrimSpace(text) == "" {
		return 0
	}
	runes := len([]rune(text))
	if runes <= 0 {
		return 0
	}
	tokens := runes / 4
	if runes%4 != 0 {
		tokens++
	}
	if tokens == 0 {
		return 1
	}
	return tokens
}
func cloneMetadata(metadata map[string]any) map[string]any {
	if len(metadata) == 0 {
		return map[string]any{}
	}
	cloned := make(map[string]any, len(metadata))
	for key, value := range metadata {
		cloned[key] = value
	}
	return cloned
}

func cloneStringMap(metadata map[string]string) map[string]string {
	if len(metadata) == 0 {
		return nil
	}
	cloned := make(map[string]string, len(metadata))
	for key, value := range metadata {
		cloned[key] = value
	}
	return cloned
}

func jsonValueFingerprint(value any) string {
	data, err := json.Marshal(value)
	if err != nil {
		return shortFingerprint(fmt.Sprintf("%v", value))
	}
	return versionForContent(data)
}

func (c *Compiler) mustConstructBlock(desc CompileDescriptor, id string, input PromptBlockInput) PromptBlock {
	block, err := DefaultPromptBlockRegistry().Construct(id, input)
	if err != nil {
		panic(&PromptCompileError{Kind: ErrInvalidBlockSemantic, BlockID: id, Reason: err.Error(), EventID: strings.TrimSpace(desc.EventID), EventType: desc.EventType, SourceID: id})
	}
	return c.validateConstructedBlock(desc, block)
}

func (c *Compiler) validateConstructedBlock(desc CompileDescriptor, block PromptBlock) PromptBlock {
	spec, ok := DefaultPromptBlockRegistry().Lookup(block.ID)
	if !ok {
		panic(&PromptCompileError{Kind: ErrUnregisteredPromptBlock, BlockID: block.ID, Reason: "all AgentTeam-authored prompt blocks must be declared in the central registry", EventID: strings.TrimSpace(desc.EventID), EventType: desc.EventType, SourceID: block.Source.ID})
	}
	if block.registryKey != spec.ID {
		panic(&PromptCompileError{Kind: ErrInvalidBlockSemantic, BlockID: block.ID, Reason: "prompt block was not materialized by its registered constructor", EventID: strings.TrimSpace(desc.EventID), EventType: desc.EventType, SourceID: block.Source.ID})
	}
	if block.Title != spec.Title || block.Type != spec.Type || block.Layer != spec.Layer || block.Slot != spec.Slot || block.Priority != spec.Priority || block.Durability != spec.Durability || block.Source.ID != spec.ID || block.Source.SourceKind != spec.SourceKind || block.Source.Path != "desktop/prompts/"+spec.TemplatePath || block.Source.Version != specTemplateVersion(spec) || block.Source.Owner != spec.Owner || block.Source.Description != spec.Description {
		panic(&PromptCompileError{Kind: ErrInvalidBlockSemantic, BlockID: block.ID, Reason: "registered block metadata or provenance was altered after construction", EventID: strings.TrimSpace(desc.EventID), EventType: desc.EventType, SourceID: block.Source.ID})
	}
	block.text = strings.TrimSpace(block.text)
	if block.TokenEstimate != EstimateTokens(block.text) {
		panic(&PromptCompileError{Kind: ErrInvalidBlockSemantic, BlockID: block.ID, Reason: "registered block token estimate is not canonical", EventID: strings.TrimSpace(desc.EventID), EventType: desc.EventType, SourceID: block.Source.ID})
	}
	if reason := invalidBlockSemanticReason(block); reason != "" {
		panic(&PromptCompileError{Kind: ErrInvalidBlockSemantic, BlockID: block.ID, Reason: reason, EventID: strings.TrimSpace(desc.EventID), EventType: desc.EventType, SourceID: block.Source.ID})
	}
	if block.TokenEstimate > c.policy.MaxBlockTokens {
		panic(&PromptCompileError{Kind: ErrBlockTooLarge, BlockID: block.ID, Limit: c.policy.MaxBlockTokens, Tokens: block.TokenEstimate, EventID: strings.TrimSpace(desc.EventID), EventType: desc.EventType, SourceID: block.Source.ID})
	}
	return block
}

func invalidBlockSemanticReason(block PromptBlock) string {
	switch block.Type {
	case BlockInstruction:
		if block.Durability != DurabilityPersistent {
			return fmt.Sprintf("instruction blocks must use %q durability, got %q", DurabilityPersistent, block.Durability)
		}
		if block.Slot != SlotBase && block.Slot != SlotDeveloper {
			return fmt.Sprintf("instruction blocks must target %q or %q, got %q", SlotBase, SlotDeveloper, block.Slot)
		}
	case BlockTurnContext:
		if block.Durability != DurabilityTurnOnly {
			return fmt.Sprintf("turn-context blocks must use %q durability, got %q", DurabilityTurnOnly, block.Durability)
		}
		if block.Slot != SlotTurn {
			return fmt.Sprintf("turn-context blocks must target %q, got %q", SlotTurn, block.Slot)
		}
	case BlockNotice:
		if block.Durability != DurabilityTurnOnly && block.Durability != DurabilityUntilAcknowledged {
			return fmt.Sprintf("notice blocks must use %q or %q durability, got %q", DurabilityTurnOnly, DurabilityUntilAcknowledged, block.Durability)
		}
		if block.Slot != SlotTurn {
			return fmt.Sprintf("notice blocks must target %q, got %q", SlotTurn, block.Slot)
		}
	case BlockReference:
		if block.Durability != DurabilityCompactable {
			return fmt.Sprintf("reference blocks must use %q durability, got %q", DurabilityCompactable, block.Durability)
		}
		if block.Slot != SlotReference {
			return fmt.Sprintf("reference blocks must target %q, got %q", SlotReference, block.Slot)
		}
	}
	return ""
}
