package domain

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"strings"
)

// MessagePresentationMetadataKey is the only metadata key interpreted by the
// browser as a server-controlled message presentation. It is reserved from
// ordinary message authors and is deliberately not a client-provided i18n
// lookup key.
const MessagePresentationMetadataKey = "messagePresentation"

const (
	MessagePresentationVersion  = 1
	WorkflowRecoveryMaxAttempts = 5

	MessagePresentationKeyWorkflowRecovery            = "workflow.recovery.nudge"
	MessagePresentationKeyWorkflowRunApprovalPending  = "workflow.run.approval.pending"
	MessagePresentationKeyWorkflowRunApprovalApproved = "workflow.run.approval.approved"
	MessagePresentationKeyWorkflowRunApprovalRejected = "workflow.run.approval.rejected"
	messagePresentationIdentifierMaxBytes             = 255
	messagePresentationSummaryMaxRunes                = 4000
)

// MessagePresentation is a versioned, server-owned presentation envelope. Args
// are kept as raw JSON at the generic boundary so validation can apply a
// key-specific typed schema before a client is allowed to interpret them.
// Unknown versions, keys, fields, and argument types are invalid.
type MessagePresentation struct {
	Version int             `json:"version"`
	Key     string          `json:"key"`
	Args    json.RawMessage `json:"args"`
}

// WorkflowRecoveryResponderMention is the server-derived identity of the
// callback responder rendered in a recovery nudge. Type is intentionally
// allowlisted to agent: the callback responder is the only mention that may be
// carried by this presentation. ID and Token are the same identity used by the
// canonical Markdown mention persisted in the message body and mention hints.
type WorkflowRecoveryResponderMention struct {
	Type  string `json:"type"`
	ID    string `json:"id"`
	Token string `json:"token"`
}

// WorkflowRecoveryPresentationArgs are the typed arguments for a recovery
// nudge. StepID is the trusted selector used by the recovery prompt and
// EffectID identifies the durable callback effect that owns the attempt.
// ResponderMention is populated by the message transaction after it resolves
// the frozen callback responder; it is optional only for legacy persisted
// presentations that predate localized responder interpolation.
type WorkflowRecoveryPresentationArgs struct {
	RunID            string                            `json:"runId"`
	StepID           string                            `json:"stepId"`
	EffectID         string                            `json:"effectId"`
	Attempt          int                               `json:"attempt"`
	TotalAttempts    int                               `json:"totalAttempts"`
	ResponderMention *WorkflowRecoveryResponderMention `json:"responderMention,omitempty"`
}

// WorkflowRunApprovalPresentationArgs are the typed arguments for Workflow
// Run approval card copy. Summary is authored Workflow/Agent content and is
// carried only as a dynamic value; it is never used as a translation key.
type WorkflowRunApprovalPresentationArgs struct {
	PlanID        string `json:"planId"`
	PlanVersionID string `json:"planVersionId"`
	RunID         string `json:"runId"`
	NodeID        string `json:"nodeId"`
	Summary       string `json:"summary"`
}

// NewWorkflowRecoveryPresentation builds the allowlisted recovery envelope.
func NewWorkflowRecoveryPresentation(runID, stepID, effectID string, attempt, totalAttempts int) *MessagePresentation {
	args, _ := json.Marshal(WorkflowRecoveryPresentationArgs{
		RunID: strings.TrimSpace(runID), StepID: strings.TrimSpace(stepID), EffectID: strings.TrimSpace(effectID),
		Attempt: attempt, TotalAttempts: totalAttempts,
	})
	return &MessagePresentation{Version: MessagePresentationVersion, Key: MessagePresentationKeyWorkflowRecovery, Args: args}
}

// NewWorkflowRecoveryPresentationWithResponderMention builds a recovery
// envelope with the server-derived callback responder identity. The ordinary
// constructor remains available for legacy fixtures and non-persisted callers;
// message materialization uses WithWorkflowRecoveryResponderMention so the
// resolved Agent identity, rather than authored presentation data, is final.
func NewWorkflowRecoveryPresentationWithResponderMention(
	runID, stepID, effectID string,
	attempt, totalAttempts int,
	mention WorkflowRecoveryResponderMention,
) *MessagePresentation {
	args, _ := json.Marshal(WorkflowRecoveryPresentationArgs{
		RunID: strings.TrimSpace(runID), StepID: strings.TrimSpace(stepID), EffectID: strings.TrimSpace(effectID),
		Attempt: attempt, TotalAttempts: totalAttempts, ResponderMention: &mention,
	})
	return &MessagePresentation{Version: MessagePresentationVersion, Key: MessagePresentationKeyWorkflowRecovery, Args: args}
}

// WithWorkflowRecoveryResponderMention replaces the responder identity on a
// recovery presentation with the canonical identity resolved by the trusted
// callback message transaction. It rejects non-recovery presentations and
// validates the resulting closed envelope before returning it.
func (presentation MessagePresentation) WithWorkflowRecoveryResponderMention(agentID, token string) (*MessagePresentation, error) {
	if err := presentation.Validate(); err != nil {
		return nil, err
	}
	if presentation.Key != MessagePresentationKeyWorkflowRecovery {
		return nil, fmt.Errorf("message presentation key %q does not carry a workflow recovery responder mention", presentation.Key)
	}
	var args WorkflowRecoveryPresentationArgs
	if err := decodeStrictJSON(presentation.Args, &args); err != nil {
		return nil, fmt.Errorf("workflow recovery presentation args are invalid: %w", err)
	}
	args.ResponderMention = &WorkflowRecoveryResponderMention{
		Type:  "agent",
		ID:    strings.TrimSpace(agentID),
		Token: strings.TrimPrefix(strings.TrimSpace(token), "@"),
	}
	encoded, err := json.Marshal(args)
	if err != nil {
		return nil, fmt.Errorf("marshal workflow recovery presentation args: %w", err)
	}
	updated := presentation
	updated.Args = encoded
	if err := updated.Validate(); err != nil {
		return nil, err
	}
	return &updated, nil
}

// WithoutWorkflowRecoveryResponderMention removes only the derived responder
// interpolation field. It is used for idempotency hashing, where the authored
// message request must hash identically before and after the first transaction
// resolves the mutable display token.
func (presentation MessagePresentation) WithoutWorkflowRecoveryResponderMention() (*MessagePresentation, error) {
	if err := presentation.Validate(); err != nil {
		return nil, err
	}
	if presentation.Key != MessagePresentationKeyWorkflowRecovery {
		copy := presentation
		copy.Args = append(json.RawMessage(nil), presentation.Args...)
		return &copy, nil
	}
	var args WorkflowRecoveryPresentationArgs
	if err := decodeStrictJSON(presentation.Args, &args); err != nil {
		return nil, fmt.Errorf("workflow recovery presentation args are invalid: %w", err)
	}
	args.ResponderMention = nil
	encoded, err := json.Marshal(args)
	if err != nil {
		return nil, fmt.Errorf("marshal workflow recovery presentation args: %w", err)
	}
	copy := presentation
	copy.Args = encoded
	if err := copy.Validate(); err != nil {
		return nil, err
	}
	return &copy, nil
}

// NewWorkflowRunApprovalPresentation builds the allowlisted approval envelope.
func NewWorkflowRunApprovalPresentation(decision, planID, planVersionID, runID, nodeID, summary string) *MessagePresentation {
	key := map[string]string{
		"pending":  MessagePresentationKeyWorkflowRunApprovalPending,
		"approved": MessagePresentationKeyWorkflowRunApprovalApproved,
		"rejected": MessagePresentationKeyWorkflowRunApprovalRejected,
	}[strings.TrimSpace(decision)]
	args, _ := json.Marshal(WorkflowRunApprovalPresentationArgs{
		PlanID: strings.TrimSpace(planID), PlanVersionID: strings.TrimSpace(planVersionID), RunID: strings.TrimSpace(runID),
		NodeID: strings.TrimSpace(nodeID), Summary: strings.TrimSpace(summary),
	})
	return &MessagePresentation{Version: MessagePresentationVersion, Key: key, Args: args}
}

// MessagePresentationFromMetadata decodes the one reserved envelope from a
// decoded metadata object. A present malformed envelope fails closed; an
// absent envelope is represented by nil.
func MessagePresentationFromMetadata(metadata map[string]any) (*MessagePresentation, error) {
	if len(metadata) == 0 {
		return nil, nil
	}
	raw, present := metadata[MessagePresentationMetadataKey]
	if !present {
		return nil, nil
	}
	encoded, err := json.Marshal(raw)
	if err != nil {
		return nil, fmt.Errorf("message presentation metadata is not JSON: %w", err)
	}
	presentation, err := DecodeMessagePresentation(encoded)
	if err != nil {
		return nil, err
	}
	return presentation, nil
}

// DecodeMessagePresentation decodes one strict JSON envelope and validates its
// key-specific typed arguments. It is shared by message metadata and card
// persistence boundaries so both reject the same malformed payloads.
func DecodeMessagePresentation(raw []byte) (*MessagePresentation, error) {
	var presentation MessagePresentation
	if err := decodeStrictJSON(raw, &presentation); err != nil {
		return nil, fmt.Errorf("message presentation envelope is invalid: %w", err)
	}
	if err := presentation.Validate(); err != nil {
		return nil, err
	}
	return &presentation, nil
}

// MetadataValue returns the canonical JSON-object representation suitable for
// embedding in a message metadata map.
func (presentation MessagePresentation) MetadataValue() (map[string]any, error) {
	if err := presentation.Validate(); err != nil {
		return nil, err
	}
	encoded, err := json.Marshal(presentation)
	if err != nil {
		return nil, fmt.Errorf("marshal message presentation: %w", err)
	}
	var value map[string]any
	if err := json.Unmarshal(encoded, &value); err != nil {
		return nil, fmt.Errorf("decode message presentation: %w", err)
	}
	return value, nil
}

// WithoutWorkflowRecoveryResponderMentionMetadata strips the derived Agent
// interpolation field from a copied metadata map. Invalid or absent envelopes
// remain unchanged so idempotency hashing preserves its fail-open legacy path.
func WithoutWorkflowRecoveryResponderMentionMetadata(metadata map[string]any) map[string]any {
	presentation, err := MessagePresentationFromMetadata(metadata)
	if err != nil || presentation == nil {
		return metadata
	}
	stripped, err := presentation.WithoutWorkflowRecoveryResponderMention()
	if err != nil {
		return metadata
	}
	value, err := stripped.MetadataValue()
	if err != nil {
		return metadata
	}
	metadata[MessagePresentationMetadataKey] = value
	return metadata
}

// Validate enforces the closed presentation protocol and key-specific typed
// argument schema. It intentionally accepts no future/unknown keys until a
// server and client release explicitly adds them.
func (presentation MessagePresentation) Validate() error {
	if presentation.Version != MessagePresentationVersion {
		return fmt.Errorf("message presentation version %d is unsupported", presentation.Version)
	}
	if presentation.Key != strings.TrimSpace(presentation.Key) || presentation.Key == "" {
		return fmt.Errorf("message presentation key must be canonical")
	}
	if len(bytes.TrimSpace(presentation.Args)) == 0 || bytes.Equal(bytes.TrimSpace(presentation.Args), []byte("null")) {
		return fmt.Errorf("message presentation args are required")
	}
	switch presentation.Key {
	case MessagePresentationKeyWorkflowRecovery:
		var args WorkflowRecoveryPresentationArgs
		if err := decodeStrictJSON(presentation.Args, &args); err != nil {
			return fmt.Errorf("workflow recovery presentation args are invalid: %w", err)
		}
		var rawArgs map[string]json.RawMessage
		if err := json.Unmarshal(presentation.Args, &rawArgs); err != nil {
			return fmt.Errorf("workflow recovery presentation args are invalid: %w", err)
		}
		if rawMention, present := rawArgs["responderMention"]; present && bytes.Equal(bytes.TrimSpace(rawMention), []byte("null")) {
			return fmt.Errorf("workflow recovery presentation responderMention must be an object when present")
		}
		return validateWorkflowRecoveryPresentationArgs(args)
	case MessagePresentationKeyWorkflowRunApprovalPending,
		MessagePresentationKeyWorkflowRunApprovalApproved,
		MessagePresentationKeyWorkflowRunApprovalRejected:
		var args WorkflowRunApprovalPresentationArgs
		if err := decodeStrictJSON(presentation.Args, &args); err != nil {
			return fmt.Errorf("Workflow Run approval presentation args are invalid: %w", err)
		}
		return validateWorkflowRunApprovalPresentationArgs(args)
	default:
		return fmt.Errorf("message presentation key %q is unsupported", presentation.Key)
	}
}

func validateWorkflowRecoveryPresentationArgs(args WorkflowRecoveryPresentationArgs) error {
	if err := validatePresentationIdentifier("runId", args.RunID); err != nil {
		return err
	}
	if err := validatePresentationIdentifier("stepId", args.StepID); err != nil {
		return err
	}
	if err := validatePresentationIdentifier("effectId", args.EffectID); err != nil {
		return err
	}
	if args.TotalAttempts < 1 || args.TotalAttempts > WorkflowRecoveryMaxAttempts {
		return fmt.Errorf("workflow recovery presentation totalAttempts must be between 1 and %d", WorkflowRecoveryMaxAttempts)
	}
	if args.Attempt < 1 || args.Attempt > args.TotalAttempts {
		return fmt.Errorf("workflow recovery presentation attempt must be between 1 and totalAttempts")
	}
	if args.ResponderMention != nil {
		if args.ResponderMention.Type != "agent" {
			return fmt.Errorf("workflow recovery presentation responderMention type must be agent")
		}
		if err := validatePresentationIdentifier("responderMention.id", args.ResponderMention.ID); err != nil {
			return err
		}
		if strings.ContainsAny(args.ResponderMention.ID, "\t\r\n ") {
			return fmt.Errorf("workflow recovery presentation responderMention.id must be a canonical Agent ID")
		}
		token := args.ResponderMention.Token
		if token == "" || token != strings.TrimSpace(token) || strings.HasPrefix(token, "@") || strings.ContainsAny(token, "\t\r\n ") {
			return fmt.Errorf("workflow recovery presentation responderMention.token must be a canonical mention token")
		}
		if len([]byte(token)) > messagePresentationIdentifierMaxBytes {
			return fmt.Errorf("workflow recovery presentation responderMention.token is too long")
		}
	}
	return nil
}

func validateWorkflowRunApprovalPresentationArgs(args WorkflowRunApprovalPresentationArgs) error {
	if err := validatePresentationIdentifier("planId", args.PlanID); err != nil {
		return err
	}
	if err := validatePresentationIdentifier("planVersionId", args.PlanVersionID); err != nil {
		return err
	}
	if err := validatePresentationIdentifier("runId", args.RunID); err != nil {
		return err
	}
	if err := validatePresentationIdentifier("nodeId", args.NodeID); err != nil {
		return err
	}
	if args.Summary == "" || args.Summary != strings.TrimSpace(args.Summary) || len([]rune(args.Summary)) > messagePresentationSummaryMaxRunes {
		return fmt.Errorf("Workflow Run approval presentation summary must contain 1..%d canonical characters", messagePresentationSummaryMaxRunes)
	}
	return nil
}

func validatePresentationIdentifier(name, value string) error {
	if value == "" || value != strings.TrimSpace(value) || len([]byte(value)) > messagePresentationIdentifierMaxBytes {
		return fmt.Errorf("message presentation %s must be a bounded canonical identifier", name)
	}
	return nil
}

func decodeStrictJSON(raw []byte, target any) error {
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(target); err != nil {
		return err
	}
	var trailing any
	if err := decoder.Decode(&trailing); err != io.EOF {
		if err == nil {
			return fmt.Errorf("trailing JSON value")
		}
		return err
	}
	return nil
}
