package message

import (
	"fmt"
	"sort"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
)

const (
	WorkflowContextMetadataWorkspaceIDKey       = "workflow_workspace_id"
	WorkflowContextMetadataPlanIDKey            = "workflow_plan_id"
	WorkflowContextMetadataPlanVersionIDKey     = "workflow_plan_version_id"
	WorkflowContextMetadataRunIDKey             = "workflow_run_id"
	WorkflowContextMetadataEffectIDKey          = "workflow_effect_id"
	WorkflowContextMetadataNodeIDKey            = "workflow_node_id"
	WorkflowContextMetadataSourceKindKey        = "workflow_source_kind"
	WorkflowContextMetadataDeliveryScopeKey     = "workflow_delivery_scope"
	WorkflowContextMetadataCallbackExpectedKey  = "workflow_callback_expected"
	WorkflowContextMetadataResponderAgentRefKey = "workflow_responder_agent_ref"
	// WorkflowContextMetadataResponderAgentIDKey is server-derived inside the
	// ordinary message transaction after the visible resolved mention has been
	// resolved. It is the durable callback authority recovered on replay; the
	// authored AgentRef above remains only the immutable selection input.
	WorkflowContextMetadataResponderAgentIDKey = "workflow_responder_agent_id"
	// WorkflowAgentEditMetadataKey carries the server-owned Workflow Builder
	// agent-edit request/response envelope. Generic IM writers must not be able
	// to forge it, including with null or empty values.
	WorkflowAgentEditMetadataKey = "workflowEdit"
	// WorkflowContextMetadataTeamMemberContextKey freezes the server-authorized
	// minimal Workflow-specific member context of the callback responder inside
	// the same message transaction that authorizes it. It is persisted with the
	// durable message/outbox receipt so callback, retry, recovery, replay, and
	// fan-out always reuse the first-frozen snapshot and never re-read mutable
	// Team/member/role state.
	WorkflowContextMetadataTeamMemberContextKey = "workflow_team_member_context"

	// WorkflowContextMetadataCallbackExpiresAtKey is retired. send_message no
	// longer carries a callback expiry, so this key must never surface as
	// ordinary metadata. It is quarantined: rejected on inbound validation and
	// never emitted by the builder, so stale producers cannot smuggle a frozen
	// callback deadline into the current protocol.
	WorkflowContextMetadataCallbackExpiresAtKey = "workflow_callback_expires_at"
)

var reservedWorkflowContextMetadataKeys = [...]string{
	WorkflowContextMetadataWorkspaceIDKey,
	WorkflowContextMetadataPlanIDKey,
	WorkflowContextMetadataPlanVersionIDKey,
	WorkflowContextMetadataRunIDKey,
	WorkflowContextMetadataEffectIDKey,
	WorkflowContextMetadataNodeIDKey,
	WorkflowContextMetadataSourceKindKey,
	WorkflowContextMetadataDeliveryScopeKey,
	WorkflowContextMetadataCallbackExpectedKey,
	WorkflowContextMetadataResponderAgentRefKey,
	WorkflowContextMetadataResponderAgentIDKey,
	WorkflowAgentEditMetadataKey,
	WorkflowContextMetadataTeamMemberContextKey, domain.MessagePresentationMetadataKey,
}

// retiredWorkflowContextMetadataKeys are keys removed from the current protocol
// that must never be re-introduced as ordinary metadata. Inbound metadata
// containing a retired key is rejected rather than silently passed through, and
// the builder never emits them, so a retired key cannot be parsed back into a
// callback deadline or any other trusted value.
var retiredWorkflowContextMetadataKeys = [...]string{
	WorkflowContextMetadataCallbackExpiresAtKey,
}

// WorkflowTeamMemberContextWire is the minimal, frozen per-responder Team
// context persisted in workflow message metadata. It carries only the
// responder's own membership plus minimal Team identity/purpose and never the
// team roster, other members' duties, wake configuration, or division hints.
// It is persisted as a manually-built authoritative metadata map (see
// EncodeWorkflowTeamMemberContextMetadata), so it deliberately carries no JSON
// tags.
type WorkflowTeamMemberContextWire struct {
	TeamID          string
	TeamHandle      string
	TeamDisplayName string
	TeamPurpose     string
	TeamMemberID    string
	Role            string
	RoleLabel       string
	RoleOverlay     string
}

// Normalize returns a copy with trimmed fields and drops the whole context when
// the frozen identity is incomplete.
func (context WorkflowTeamMemberContextWire) Normalize() *WorkflowTeamMemberContextWire {
	normalized := WorkflowTeamMemberContextWire{
		TeamID:          strings.TrimSpace(context.TeamID),
		TeamHandle:      strings.TrimSpace(context.TeamHandle),
		TeamDisplayName: strings.TrimSpace(context.TeamDisplayName),
		TeamPurpose:     strings.TrimSpace(context.TeamPurpose),
		TeamMemberID:    strings.TrimSpace(context.TeamMemberID),
		Role:            strings.TrimSpace(context.Role),
		RoleLabel:       strings.TrimSpace(context.RoleLabel),
		RoleOverlay:     strings.TrimSpace(context.RoleOverlay),
	}
	if normalized.TeamID == "" || normalized.TeamMemberID == "" {
		return nil
	}
	return &normalized
}

// EncodeWorkflowTeamMemberContextMetadata renders the frozen member context as
// the authoritative metadata value. A nil or empty context returns nil.
func EncodeWorkflowTeamMemberContextMetadata(context *WorkflowTeamMemberContextWire) map[string]any {
	if context == nil {
		return nil
	}
	normalized := context.Normalize()
	if normalized == nil {
		return nil
	}
	return map[string]any{
		"teamId":          normalized.TeamID,
		"teamHandle":      normalized.TeamHandle,
		"teamDisplayName": normalized.TeamDisplayName,
		"teamPurpose":     normalized.TeamPurpose,
		"teamMemberId":    normalized.TeamMemberID,
		"role":            normalized.Role,
		"roleLabel":       normalized.RoleLabel,
		"roleOverlay":     normalized.RoleOverlay,
	}
}

// WorkflowTeamMemberContextFromMetadata decodes the frozen member context from
// message metadata. A missing value is accepted; malformed present data fails
// closed so a corrupted snapshot can never be re-frozen.
func WorkflowTeamMemberContextFromMetadata(metadata map[string]any) (*WorkflowTeamMemberContextWire, error) {
	if len(metadata) == 0 {
		return nil, nil
	}
	raw, present := metadata[WorkflowContextMetadataTeamMemberContextKey]
	if !present {
		return nil, nil
	}
	value, ok := raw.(map[string]any)
	if !ok {
		return nil, fmt.Errorf("workflow Team member context metadata is invalid")
	}
	context := WorkflowTeamMemberContextWire{}
	for _, field := range []struct {
		key  string
		dest *string
	}{
		{"teamId", &context.TeamID},
		{"teamHandle", &context.TeamHandle},
		{"teamDisplayName", &context.TeamDisplayName},
		{"teamPurpose", &context.TeamPurpose},
		{"teamMemberId", &context.TeamMemberID},
		{"role", &context.Role},
		{"roleLabel", &context.RoleLabel},
		{"roleOverlay", &context.RoleOverlay},
	} {
		if rawValue, exists := value[field.key]; exists {
			if stringValue, ok := rawValue.(string); ok {
				*field.dest = stringValue
			}
		}
	}
	normalized := context.Normalize()
	if normalized == nil {
		return nil, nil
	}
	return normalized, nil
}

type WorkflowContextMetadataParams struct {
	WorkspaceID       string
	PlanID            string
	PlanVersionID     string
	RunID             string
	EffectID          string
	NodeID            string
	SourceKind        string
	DeliveryScope     *domain.MessageScopeValue
	CallbackExpected  bool
	ResponderAgentRef string
	Presentation      *domain.MessagePresentation
}

// BuildWorkflowContextMetadata stores trusted Workflow message context.
func BuildWorkflowContextMetadata(params WorkflowContextMetadataParams) map[string]any {
	metadata := make(map[string]any, 10)
	if value := strings.TrimSpace(params.WorkspaceID); value != "" {
		metadata[WorkflowContextMetadataWorkspaceIDKey] = value
	}
	if value := strings.TrimSpace(params.PlanID); value != "" {
		metadata[WorkflowContextMetadataPlanIDKey] = value
	}
	if value := strings.TrimSpace(params.PlanVersionID); value != "" {
		metadata[WorkflowContextMetadataPlanVersionIDKey] = value
	}
	if value := strings.TrimSpace(params.RunID); value != "" {
		metadata[WorkflowContextMetadataRunIDKey] = value
	}
	if value := strings.TrimSpace(params.EffectID); value != "" {
		metadata[WorkflowContextMetadataEffectIDKey] = value
	}
	if value := strings.TrimSpace(params.NodeID); value != "" {
		metadata[WorkflowContextMetadataNodeIDKey] = value
	}
	if value := strings.TrimSpace(params.SourceKind); value != "" {
		metadata[WorkflowContextMetadataSourceKindKey] = value
	}
	if params.DeliveryScope != nil {
		if scope, err := domain.NormalizeMessageScopeValue(*params.DeliveryScope); err == nil {
			metadata[WorkflowContextMetadataDeliveryScopeKey] = map[string]any{
				"kind":    string(scope.Kind),
				"scopeId": scope.ScopeID,
			}
		}
	}
	if params.CallbackExpected {
		metadata[WorkflowContextMetadataCallbackExpectedKey] = true
	}
	if value := strings.TrimSpace(params.ResponderAgentRef); value != "" {
		metadata[WorkflowContextMetadataResponderAgentRefKey] = value
	}
	if params.Presentation != nil {
		if value, err := params.Presentation.MetadataValue(); err == nil {
			metadata[domain.MessagePresentationMetadataKey] = value
		}
	}
	if len(metadata) == 0 {
		return nil
	}
	return metadata
}

// ValidateWorkflowContextMetadata fences reserved Workflow metadata and retired keys.
func ValidateWorkflowContextMetadata(metadata map[string]any, allowReserved bool) error {
	if len(metadata) == 0 {
		return nil
	}
	if !allowReserved {
		for _, key := range reservedWorkflowContextMetadataKeys {
			if _, exists := metadata[key]; exists {
				return fmt.Errorf("message metadata key %q is reserved for workflow runtime", key)
			}
		}
	}
	for _, key := range retiredWorkflowContextMetadataKeys {
		if _, exists := metadata[key]; exists {
			return fmt.Errorf("message metadata key %q is retired and must not be supplied", key)
		}
	}
	if _, err := domain.MessagePresentationFromMetadata(metadata); err != nil {
		return fmt.Errorf("message metadata presentation is invalid: %w", err)
	}
	return nil
}

// ValidateWorkflowCallbackResponderRouting fences the trusted callback mode
// that freezes one selected Agent as callback authority. The selected Agent is
// materialized as a visible resolved mention and then follows ordinary
// message routing; other ordinary recipients do not gain callback authority.
func ValidateWorkflowCallbackResponderRouting(metadata map[string]any, allowWorkflowContext bool, requireSingleResponder bool) error {
	if !requireSingleResponder {
		return nil
	}
	if !allowWorkflowContext {
		return fmt.Errorf("single workflow callback responder routing requires trusted workflow context metadata")
	}
	callbackExpected, ok := metadata[WorkflowContextMetadataCallbackExpectedKey].(bool)
	if !ok || !callbackExpected {
		return fmt.Errorf("single workflow callback responder routing requires callbackExpected workflow metadata")
	}
	return nil
}

// ValidateWorkflowCallbackResponderAgentRef binds the trusted structured
// responder selection to the immutable AgentRef stored in workflow metadata. This
// makes the authored reference part of the normal message idempotency digest,
// so replaying the same effect with a different responder conflicts. The
// destination message transaction later resolves this ref to one authoritative
// Agent ID and persists that ID in the durable wake receipt.
func ValidateWorkflowCallbackResponderAgentRef(
	metadata map[string]any,
	requireSingleResponder bool,
	responderAgentRef string,
) error {
	responderAgentRef = strings.TrimSpace(responderAgentRef)
	raw, present := metadata[WorkflowContextMetadataResponderAgentRefKey]
	if responderAgentRef == "" {
		if present {
			return fmt.Errorf("workflow responder metadata requires a selected responder Agent")
		}
		return nil
	}
	if !requireSingleResponder {
		return fmt.Errorf("workflow callback responder Agent requires callback authority routing")
	}
	metadataResponderAgentRef, ok := raw.(string)
	if !ok || metadataResponderAgentRef != responderAgentRef || strings.TrimSpace(metadataResponderAgentRef) != metadataResponderAgentRef {
		return fmt.Errorf("workflow callback responder ref does not match workflow metadata")
	}
	return nil
}

// WorkflowCallbackResponderAgentID reads the authoritative callback authority
// frozen by the ordinary mention transaction. Missing data is accepted for
// legacy messages; malformed present data fails closed.
func WorkflowCallbackResponderAgentID(metadata map[string]any) (string, error) {
	if len(metadata) == 0 {
		return "", nil
	}
	raw, present := metadata[WorkflowContextMetadataResponderAgentIDKey]
	if !present {
		return "", nil
	}
	value, ok := raw.(string)
	if !ok || strings.TrimSpace(value) == "" || strings.TrimSpace(value) != value {
		return "", fmt.Errorf("workflow callback responder Agent ID is invalid")
	}
	return value, nil
}

// WorkflowContextMetadataForAgentRecipient projects trusted callback metadata
// onto one concrete Agent delivery. A structured callback message may have
// ordinary co-recipients (for example another direct mention, a default Agent,
// or a Task watcher), but only the server-frozen responder may receive the
// hidden callback authority consumed by the desktop runtime.
//
// Messages without a frozen responder ID are legacy callback deliveries. Their
// historical writer required exactly one wake recipient, so preserve their
// metadata rather than making old durable outbox rows impossible to complete.
// A present but malformed frozen ID fails closed and strips callback authority
// from every recipient.
func WorkflowContextMetadataForAgentRecipient(metadata map[string]any, recipientAgentID string) map[string]any {
	if len(metadata) == 0 {
		return metadata
	}
	metadata = workflowAgentEditMetadataForRecipient(metadata, recipientAgentID)
	if len(metadata) == 0 {
		return metadata
	}
	if callbackExpected, ok := metadata[WorkflowContextMetadataCallbackExpectedKey].(bool); !ok || !callbackExpected {
		return metadataWithoutRetiredKeys(metadata)
	}
	_, frozenPresent := metadata[WorkflowContextMetadataResponderAgentIDKey]
	frozenResponderAgentID, err := WorkflowCallbackResponderAgentID(metadata)
	if !frozenPresent {
		// Pre-structured legacy callbacks have neither field. A message that
		// already carries the authored responder ref is from the new protocol;
		// absence of its derived ID is corruption and must not restore the old
		// "every recipient owns the callback" behavior.
		if _, structured := metadata[WorkflowContextMetadataResponderAgentRefKey]; !structured {
			return metadataWithoutRetiredKeys(metadata)
		}
	}
	if err == nil && recipientAgentID == strings.TrimSpace(recipientAgentID) && frozenResponderAgentID == recipientAgentID {
		// The frozen responder keeps the full trusted callback authority; only
		// retired keys are stripped so a stale marker cannot escape.
		return metadataWithoutRetiredKeys(metadata)
	}

	projected := copyWithoutQuarantinedWorkflowContextKeys(metadata)
	if len(projected) == 0 {
		return nil
	}
	return projected
}

// metadataWithoutRetiredKeys returns a copy without retired workflow keys.
func metadataWithoutRetiredKeys(metadata map[string]any) map[string]any {
	projected := CopyMessageMetadata(metadata)
	for _, key := range retiredWorkflowContextMetadataKeys {
		delete(projected, key)
	}
	return projected
}

// copyWithoutQuarantinedWorkflowContextKeys returns a shallow copy with both the
// reserved and retired workflow metadata keys removed. A retired key such as the
// callback-expiry marker must never escape as ordinary metadata on any delivery
// projection, so every recipient path strips it.
func copyWithoutQuarantinedWorkflowContextKeys(metadata map[string]any) map[string]any {
	projected := CopyMessageMetadata(metadata)
	for _, key := range reservedWorkflowContextMetadataKeys {
		delete(projected, key)
	}
	for _, key := range retiredWorkflowContextMetadataKeys {
		delete(projected, key)
	}
	return projected
}

func workflowAgentEditMetadataForRecipient(metadata map[string]any, recipientAgentID string) map[string]any {
	raw, present := metadata[WorkflowAgentEditMetadataKey]
	if !present {
		return metadata
	}
	edit, ok := raw.(map[string]any)
	responderAgentID, responderOK := edit["responderAgentId"].(string)
	recipientAgentID = strings.TrimSpace(recipientAgentID)
	if ok && responderOK &&
		recipientAgentID != "" &&
		responderAgentID == recipientAgentID &&
		strings.TrimSpace(responderAgentID) == responderAgentID {
		return metadata
	}
	projected := CopyMessageMetadata(metadata)
	delete(projected, WorkflowAgentEditMetadataKey)
	if len(projected) == 0 {
		return nil
	}
	return projected
}

// MetadataWithoutDerivedWorkflowResponder returns a shallow copy suitable for
// client-message idempotency hashing. The authoritative ID is server-derived from
// the authored AgentRef and current visibility in the first write transaction;
// it must not make replay preflight depend on mutable routing state.
func MetadataWithoutDerivedWorkflowResponder(metadata map[string]any) map[string]any {
	if len(metadata) == 0 {
		return metadata
	}
	copy := domain.WithoutWorkflowRecoveryResponderMentionMetadata(CopyMessageMetadata(metadata))
	delete(copy, WorkflowContextMetadataResponderAgentIDKey)
	for _, key := range retiredWorkflowContextMetadataKeys {
		delete(copy, key)
	}
	return copy
}

func NormalizeWorkflowCallbackTaskWatchHandoff(
	handoff *WorkflowCallbackTaskWatchHandoff,
	allowWorkflowContext bool,
	requireSingleResponder bool,
	responderAgentRef string,
) (*WorkflowCallbackTaskWatchHandoff, error) {
	if handoff == nil {
		return nil, nil
	}
	if !allowWorkflowContext || !requireSingleResponder || strings.TrimSpace(responderAgentRef) == "" {
		return nil, fmt.Errorf("workflow callback Task watch handoff requires trusted selected-responder context")
	}
	scope, err := domain.NormalizeMessageScopeValue(domain.MessageScopeValue{
		Kind: domain.MessageScopeKindTask, ScopeID: strings.TrimSpace(handoff.TaskScopeID),
	})
	if err != nil {
		return nil, fmt.Errorf("workflow callback Task watch handoff scope is invalid: %w", err)
	}
	seen := make(map[string]struct{}, len(handoff.PredecessorAgentIDs))
	predecessors := make([]string, 0, len(handoff.PredecessorAgentIDs))
	for _, raw := range handoff.PredecessorAgentIDs {
		agentID := strings.TrimSpace(raw)
		if agentID == "" || agentID != raw {
			return nil, fmt.Errorf("workflow callback Task watch predecessor Agent ID is invalid")
		}
		if _, duplicate := seen[agentID]; duplicate {
			continue
		}
		seen[agentID] = struct{}{}
		predecessors = append(predecessors, agentID)
	}
	if len(predecessors) > domain.MaxWorkflowTaskWatchHandoffPredecessors {
		return nil, fmt.Errorf(
			"workflow callback Task watch handoff must not contain more than %d unique predecessors",
			domain.MaxWorkflowTaskWatchHandoffPredecessors,
		)
	}
	sort.Strings(predecessors)
	return &WorkflowCallbackTaskWatchHandoff{
		TaskScopeID: scope.ScopeID, PredecessorAgentIDs: predecessors,
	}, nil
}
