using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
using Akka.Actor;
using Morgana.AI.Attributes;

namespace Morgana.AI;

/// <summary>
/// Central repository of immutable record types (DTOs) used throughout the Morgana framework.
/// Records are organized by functional area: agent communication, classification, prompts, tools and presentation.
/// </summary>
/// <remarks>
/// <para><strong>Design Philosophy:</strong></para>
/// <list type="bullet">
/// <item>Immutable records for thread-safety in actor message passing</item>
/// <item>Explicit types prevent message routing errors in actor system</item>
/// <item>JSON serialization support for LLM interactions and configuration loading</item>
/// <item>Context wrappers preserve sender references across async operations (PipeTo pattern)</item>
/// </list>
/// </remarks>
public static class Records
{
    /// <summary>
    /// Standard STJ deserialization options used by Morgana
    /// </summary>
    internal static readonly JsonSerializerOptions DefaultJsonSerializerOptions =
        new JsonSerializerOptions
        {
            AllowOutOfOrderMetadataProperties = true,
            Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
            PropertyNameCaseInsensitive = true
        };

    // ==========================================================================
    // CONVERSATION LIFECYCLE MESSAGES
    // ==========================================================================

    /// <summary>
    /// Final response message sent from ConversationSupervisorActor to ConversationManagerActor after processing a user message.
    /// Contains the AI response, metadata, agent information, and optional quick reply buttons.
    /// </summary>
    /// <param name="Response">AI-generated response text</param>
    /// <param name="Classification">Intent classification result (e.g., "billing", "contract")</param>
    /// <param name="Metadata">Additional metadata from classification (confidence, error codes, etc.)</param>
    /// <param name="AgentName">Name of the agent that generated the response (e.g., "Morgana", "Morgana (Billing)")</param>
    /// <param name="AgentCompleted">Flag indicating if the agent completed its multi-turn interaction</param>
    /// <param name="QuickReplies">Optional list of quick reply buttons for guided user interactions</param>
    /// <param name="OriginalTimestamp">Optional timestamp of the message when created at UI level</param>
    /// <param name="RichCard">Optional rich card for structured data visualization</param>
    public record ConversationResponse(
        string Response,
        string? Classification,
        Dictionary<string, string>? Metadata,
        string? AgentName = null,
        bool AgentCompleted = false,
        List<QuickReply>? QuickReplies = null,
        DateTime? OriginalTimestamp = null,
        RichCard? RichCard = null);

    /// <summary>
    /// Request to create a new conversation and initialize the actor hierarchy.
    /// </summary>
    /// <param name="ConversationId">Unique identifier for the new conversation</param>
    /// <param name="IsRestore">Flag indicating that the conversation is being created or restored</param>
    /// <param name="ChannelMetadata">Channel metadata declared by the originating client at start
    /// (channel name + capability budget). Required on fresh starts — Morgana refuses to
    /// create a conversation for a channel that does not announce itself. Null on restore,
    /// where the manager loads the persisted entry instead.</param>
    public record CreateConversation(
        string ConversationId,
        bool IsRestore,
        ChannelMetadata? ChannelMetadata = null);

    /// <summary>
    /// Request to terminate a conversation and stop all associated actors.
    /// </summary>
    /// <param name="ConversationId">Unique identifier of the conversation to terminate</param>
    public record TerminateConversation(
        string ConversationId);

    // ==========================================================================
    // CONVERSATION PERSISTENCE
    // ==========================================================================

    /// <summary>
    /// Configuration options for conversation persistence.
    /// </summary>
    /// <remarks>
    /// <para><strong>Configuration Example:</strong></para>
    /// <code>
    /// {
    ///   "Morgana": {
    ///     "ConversationPersistence": {
    ///       "StoragePath": "C:/MorganaData",
    ///       "EncryptionKey": "your-base64-encoded-256-bit-key"
    ///     }
    ///   }
    /// }
    /// </code>
    /// <para><strong>Generating an Encryption Key:</strong></para>
    /// <code>
    /// // C# code to generate a secure 256-bit key
    /// using System.Security.Cryptography;
    /// byte[] key = new byte[32];
    /// RandomNumberGenerator.Fill(key);
    /// string base64Key = Convert.ToBase64String(key);
    /// Console.WriteLine(base64Key);
    /// </code>
    /// </remarks>
    public record ConversationPersistenceOptions
    {
        /// <summary>
        /// Directory path where conversation files will be stored.
        /// Directory will be created if it doesn't exist.
        /// </summary>
        /// <example>C:/MorganaData</example>
        public string StoragePath { get; set; } = string.Empty;

        /// <summary>
        /// Base64-encoded 256-bit AES encryption key for conversation data.<br/>
        /// CRITICAL: Keep this key secure and never commit it to source control.
        /// </summary>
        /// <example>3q2+7w8e9r0t1y2u3i4o5p6a7s8d9f0g1h2j3k4l5z6x7c8v9b0n1m2==</example>
        public string EncryptionKey { get; set; } = string.Empty;
    }

    /// <summary>
    /// Request to restore active agent state when resuming a conversation from persistence.
    /// Sent to ConversationSupervisor after conversation resume to set activeAgent and activeAgentIntent.
    /// </summary>
    /// <param name="AgentIntent">Intent of the agent that was last active (e.g., "billing", "contract")</param>
    public record RestoreActiveAgent(string AgentIntent);

    /// <summary>
    /// Request sent to RouterActor to restore/resolve an agent by intent.
    /// Returns the agent reference if successful, or null if intent is invalid.
    /// Router will cache the agent for future routing operations.
    /// </summary>
    /// <param name="AgentIntent">Intent name to resolve agent for</param>
    public record RestoreAgentRequest(string AgentIntent);

    /// <summary>
    /// Response from RouterActor containing the resolved agent reference.
    /// Null AgentRef indicates the intent could not be resolved to a valid agent.
    /// </summary>
    /// <param name="AgentIntent">Original intent requested</param>
    /// <param name="AgentRef">Resolved agent reference, or null if not found</param>
    public record RestoreAgentResponse(string AgentIntent, IActorRef? AgentRef);

    /// <summary>
    /// Chat message record for UI consumption (Cauldron).
    /// Represents a single message in the conversation history, mapped from Microsoft.Agents.AI.ChatMessage.
    /// </summary>
    /// <remarks>
    /// This record is optimized for Blazor UI rendering and includes UI-specific fields like AgentName, QuickReplies, etc.
    /// </remarks>
    public record MorganaChatMessage
    {
        /// <summary>
        /// Unique identifier of the conversation this message belongs to.
        /// </summary>
        public required string ConversationId { get; init; }

        /// <summary>
        /// Message text content displayed to the user.
        /// Extracted from TextContent blocks in ChatMessage.Content.
        /// </summary>
        public required string Text { get; init; }

        /// <summary>
        /// Timestamp when the message was created or received.
        /// Mapped from ChatMessage.CreatedAt.
        /// </summary>
        public required DateTime Timestamp { get; init; }

        /// <summary>
        /// Type of message determining styling and behavior.
        /// Derived from ChatMessage.Role (user/assistant).
        /// </summary>
        public required MessageType Type { get; init; }

        /// <summary>
        /// Gets the message role for CSS styling ("user" or "assistant").
        /// </summary>
        public string Role => Type switch
        {
            MessageType.User => "user",
            _ => "assistant"
        };

        /// <summary>
        /// Name of the agent that generated this message.
        /// Examples: "User", "Morgana (billing)", "Morgana (contract)", ...
        /// </summary>
        public required string AgentName { get; init; }

        /// <summary>
        /// Indicates whether the agent has completed its task.
        /// Mapped from SQLite is_active column: true when is_active = 0, false when is_active = 1.
        /// </summary>
        public required bool AgentCompleted { get; init; }

        /// <summary>
        /// Optional list of quick reply buttons attached to this message.
        /// Reconstructed from SetQuickReplies tool calls when loading conversation history.
        /// </summary>
        public List<QuickReply>? QuickReplies { get; init; }

        /// <summary>
        /// Optional flag indicating that this is the last message of a resumed conversation.
        /// </summary>
        public bool? IsLastHistoryMessage { get; init; }

        /// <summary>
        /// Optional rich card attached to this message.
        /// Reconstructed from SetRichCard tool calls when loading conversation history.
        /// </summary>
        public RichCard? RichCard { get; init; }
    }

    /// <summary>
    /// Enumeration of message types for styling and behavior differentiation.
    /// </summary>
    public enum MessageType
    {
        /// <summary>
        /// Message from the user.
        /// Displayed on the right side with user styling.
        /// </summary>
        User,

        /// <summary>
        /// Regular response from an agent.
        /// Displayed on the left side with agent avatar and assistant styling.
        /// </summary>
        Assistant
    }

    // ==========================================================================
    // RATE LIMITING
    // ==========================================================================

    /// <summary>
    /// Configuration options for conversation rate limiting.
    /// </summary>
    /// <remarks>
    /// <para><strong>Configuration Example:</strong></para>
    /// <code>
    /// {
    ///   "Morgana": {
    ///     "RateLimiting": {
    ///       "Enabled": true,
    ///       "MaxMessagesPerMinute": 5,
    ///       "MaxMessagesPerHour": 30,
    ///       "MaxMessagesPerDay": 80,
    ///       "ErrorMessagePerMinute": "✋ Whoa there! You're casting spells too quickly...",
    ///       "ErrorMessagePerHour": "⏰ You've reached your hourly spell quota...",
    ///       "ErrorMessagePerDay": "🌙 You've exhausted today's magical energy...",
    ///       "ErrorMessageDefault": "⚠️ You're sending messages too quickly..."
    ///     }
    ///   }
    /// }
    /// </code>
    /// </remarks>
    public record RateLimitOptions
    {
        /// <summary>
        /// Master toggle for rate limiting feature.
        /// Set to false to disable all rate limiting (useful for development/testing).
        /// </summary>
        public bool Enabled { get; set; } = true;

        /// <summary>
        /// Maximum messages allowed per minute per conversation.
        /// Prevents burst spam. Set to 0 to disable this check.
        /// </summary>
        /// <example>5</example>
        public int MaxMessagesPerMinute { get; set; } = 5;

        /// <summary>
        /// Maximum messages allowed per hour per conversation.
        /// Prevents sustained abuse. Set to 0 to disable this check.
        /// </summary>
        /// <example>30</example>
        public int MaxMessagesPerHour { get; set; } = 30;

        /// <summary>
        /// Maximum messages allowed per day per conversation.
        /// Enforces daily quotas. Set to 0 to disable this check.
        /// </summary>
        /// <example>100</example>
        public int MaxMessagesPerDay { get; set; } = 80;

        /// <summary>
        /// Error message displayed when per-minute limit is exceeded.
        /// Supports placeholders: {limit} for the actual limit value.
        /// </summary>
        /// <example>✋ Whoa there! You're casting spells too quickly. Please wait a moment before trying again.</example>
        public string ErrorMessagePerMinute { get; set; } =
            "✋ Whoa there! You're casting spells too quickly. Please wait a moment before trying again.";

        /// <summary>
        /// Error message displayed when per-hour limit is exceeded.
        /// Supports placeholders: {limit} for the actual limit value.
        /// </summary>
        /// <example>⏰ You've reached your hourly spell quota. The magic cauldron needs time to recharge!</example>
        public string ErrorMessagePerHour { get; set; } =
            "⏰ You've reached your hourly spell quota. The magic cauldron needs time to recharge!";

        /// <summary>
        /// Error message displayed when per-day limit is exceeded.
        /// Supports placeholders: {limit} for the actual limit value.
        /// </summary>
        /// <example>🌙 You've exhausted today's magical energy. Return tomorrow for more spells!</example>
        public string ErrorMessagePerDay { get; set; } =
            "🌙 You've exhausted today's magical energy. Return tomorrow for more spells!";

        /// <summary>
        /// Default error message for unknown/generic rate limit violations.
        /// </summary>
        /// <example>⚠️ You're sending messages too quickly. Please slow down.</example>
        public string ErrorMessageDefault { get; set; } =
            "⚠️ You're sending messages too quickly. Please slow down.";
    }

    /// <summary>
    /// Result of a rate limit check operation.
    /// </summary>
    /// <param name="IsAllowed">Whether the request is allowed to proceed</param>
    /// <param name="ViolatedLimit">Description of which limit was exceeded (null if allowed)</param>
    /// <param name="RetryAfterSeconds">Suggested wait time in seconds before retrying (null if allowed)</param>
    public record RateLimitResult(
        bool IsAllowed,
        string? ViolatedLimit = null,
        int? RetryAfterSeconds = null);

    // ==========================================================================
    // MAGIC DUST (TOKEN BUDGET)
    // ==========================================================================

    /// <summary>
    /// Per-provider pricing: how many tokens of each direction equal one dust unit, plus
    /// cache cost-weights. Lives under <c>Morgana:LLM:{Provider}:MagicDust</c> because cost
    /// is a property of the concrete model. Zero on either axis means that direction does
    /// not consume dust (e.g. Ollama local models, set both to 0 for "free").
    /// <para>The MEAI Anthropic adapter reports <c>InputTokenCount</c> as the <em>total</em>
    /// prompt (fresh + cache-read + cache-write). Charging it flat would over-count cache
    /// reads (real cost ~0.1×) and under-count 1h cache writes (~2×). The two weights below
    /// let the limiter track real cache economics; defaults are 1.0 (cache-unaware no-op, so
    /// behaviour is unchanged unless a deployment configures them).</para>
    /// </summary>
    public record MagicDustPricing
    {
        /// <summary>Input tokens that equal one dust unit. 0 disables input charging.</summary>
        public int InputTokensPerDustUnit { get; set; }

        /// <summary>Output tokens that equal one dust unit. 0 disables output charging.</summary>
        public int OutputTokensPerDustUnit { get; set; }

        /// <summary>
        /// Cost weight applied to cache-read input tokens (<c>CachedInputTokenCount</c>)
        /// relative to a fresh input token. Anthropic cache-read ≈ 0.10; OpenAI ≈ 0.50.
        /// </summary>
        public double CachedInputWeight { get; set; }

        /// <summary>
        /// Cost weight applied to cache-creation input tokens
        /// (<c>AdditionalCounts["CacheCreationInputTokens"]</c>) relative to a fresh input
        /// token. Anthropic 1h cache write ≈ 2.0; providers with no separate write cost = 1.0.
        /// </summary>
        public double CacheCreationWeight { get; set; }
    }

    /// <summary>
    /// Policy for the per-conversation lifetime dust budget — a token-consumption guard
    /// orthogonal to <see cref="RateLimitOptions"/>. The budget is a lifetime resource: no
    /// sliding window, no reset. Once exhausted the conversation is done; the only way
    /// forward is a brand-new conversation.
    /// <para>Message templates are framework-neutral English defaults; deployments override
    /// them in <c>Morgana:DustLimiting</c> with their own copy and personality, exactly like
    /// <see cref="RateLimitOptions"/>. The warning templates support one placeholder,
    /// <c>{percent}</c> (remaining budget as a 0–100 integer — fuel-gauge semantics, the
    /// same number the gauge shows; users reason in "how much is left", not dust units).</para>
    /// </summary>
    public record DustLimitingOptions
    {
        /// <summary>Master toggle. When false the limiter is fully bypassed (fail open).</summary>
        public bool Enabled { get; set; }

        /// <summary>Total dust a conversation may consume over its lifetime.</summary>
        public double BudgetPerConversation { get; set; }

        /// <summary>One-shot advisory shown when consumption crosses 70%.</summary>
        public string Warning70Message { get; set; }

        /// <summary>One-shot advisory shown when consumption crosses 90%.</summary>
        public string Warning90Message { get; set; }

        /// <summary>Blocking message shown when the budget is exhausted (100%).</summary>
        public string ErrorMessage { get; set; }
    }

    /// <summary>
    /// Conversation-level metadata carried by every outbound <see cref="ChannelMessage"/>.
    /// Holds dimensions that characterise the conversation itself, not the message content
    /// (remaining budget today; active agent, health, … as the framework grows).
    /// </summary>
    /// <param name="DustLevel">REMAINING dust as a fraction of the budget, fuel-gauge
    /// semantics: 1.0 = full (nothing consumed), 0.0 = empty (budget spent or overshot).
    /// Producers floor to whole-percent steps so gauges (<c>(int)(level * 100)</c>) and
    /// the <c>DustLevel &lt;= 0.0</c> exhaustion gate agree: a fractional remainder under
    /// 1% reads 0% on both sides. Null when dust limiting is disabled (frontends hide the
    /// indicator in that case).</param>
    public record ConversationMetadata(double? DustLevel = null);

    // ==========================================================================
    // AUTHENTICATION
    // ==========================================================================

    /// <summary>
    /// Configuration options for the Morgana authentication service.
    /// </summary>
    /// <remarks>
    /// <para><strong>Per-Issuer Trust Model:</strong></para>
    /// <para>Each accepted channel is declared as its own <see cref="IssuerOptions"/> entry
    /// with its own signing key. A token is validated against the key of the issuer
    /// declared in its <c>iss</c> claim — so compromise of one channel's key does not
    /// impact the others. Tokens whose <c>iss</c> is not declared here are fail-closed.</para>
    ///
    /// <para><strong>Onboarding a New Channel:</strong></para>
    /// <para>Any channel beyond the reference one (Cauldron) must be registered as an
    /// <see cref="IssuerOptions"/> entry on the destination Morgana instance. Its
    /// <see cref="IssuerOptions.Name"/> must equal the <c>iss</c> claim the channel mints,
    /// and its <see cref="IssuerOptions.SymmetricKey"/> must match the secret the channel
    /// uses to sign tokens. A channel not declared here — or using a different key — is
    /// rejected at the very first request.</para>
    ///
    /// <para><strong>Configuration Example:</strong></para>
    /// <code>
    /// // appsettings.json
    /// {
    ///   "Morgana": {
    ///     "Authentication": {
    ///       "Audience": "morgana.ai",
    ///       "Issuers": [
    ///         { "Name": "cauldron", "SymmetricKey": "your-256-bit-secret-key-here" }
    ///       ]
    ///     }
    ///   }
    /// }
    /// </code>
    /// </remarks>
    public record AuthenticationOptions
    {
        /// <summary>
        /// Declared issuers Morgana will accept tokens from. Each entry carries its own
        /// signing key, so the blast radius of a leaked key is limited to a single channel.
        /// A token whose <c>iss</c> claim is not in this list is rejected.
        /// </summary>
        public List<IssuerOptions> Issuers { get; set; } = [];

        /// <summary>
        /// Expected audience claim (<c>aud</c>) in the token.
        /// Tokens with a different audience will be rejected.
        /// </summary>
        /// <example>morgana-api</example>
        public string Audience { get; set; } = "morgana.ai";
    }

    /// <summary>
    /// Per-issuer authentication entry. Binds an issuer name (the value Morgana
    /// expects in the JWT <c>iss</c> claim) to the signing key used to validate
    /// tokens emitted under that name.
    /// </summary>
    public record IssuerOptions
    {
        /// <summary>
        /// Issuer name as it appears in the JWT <c>iss</c> claim
        /// (lowercase channel identifier, e.g. <c>"cauldron"</c>).
        /// </summary>
        public string Name { get; set; } = string.Empty;

        /// <summary>
        /// Shared symmetric key used to validate this issuer's tokens (HMAC-SHA256).
        /// Must be at least 256 bits (32 bytes). Override via User Secrets or
        /// environment variables in production.
        /// </summary>
        public string SymmetricKey { get; set; } = string.Empty;
    }

    /// <summary>
    /// Result of a token authentication operation.
    /// </summary>
    /// <param name="IsAuthenticated">Whether the token was successfully validated</param>
    /// <param name="UserId">The caller's unique identifier (from the <c>sub</c> claim), null if not authenticated</param>
    /// <param name="DisplayName">The caller's display name (from the <c>name</c> claim), null if not authenticated</param>
    /// <param name="Error">Description of why authentication failed, null if authenticated</param>
    public record AuthenticationResult(
        bool IsAuthenticated,
        string? UserId = null,
        string? DisplayName = null,
        string? Error = null);

    // ==========================================================================
    // USER MESSAGE HANDLING
    // ==========================================================================

    /// <summary>
    /// User message submitted for processing through the conversation pipeline.
    /// </summary>
    /// <param name="ConversationId">Unique identifier of the conversation</param>
    /// <param name="Text">User's message text</param>
    /// <param name="Timestamp">Timestamp when the message was created</param>
    /// <param name="TurnContext">OpenTelemetry activity context for the current turn span.</param>
    /// <param name="UserId">Authenticated caller identity, propagated from the HTTP layer for conversation ownership and audit.</param>
    public record UserMessage(
        string ConversationId,
        string Text,
        DateTime Timestamp,
        ActivityContext TurnContext = default,
        string? UserId = null);

    // ==========================================================================
    // GUARD (CONTENT MODERATION) MESSAGES
    // ==========================================================================

    /// <summary>
    /// Request for content moderation check on a user message.
    /// Sent to GuardActor for two-level filtering (profanity + LLM policy check).
    /// </summary>
    /// <param name="ConversationId">Unique identifier of the conversation</param>
    /// <param name="Message">User message to check for policy violations</param>
    public record GuardCheckRequest(
        string ConversationId,
        string Message);

    /// <summary>
    /// Result of content moderation check from GuardActor.
    /// </summary>
    /// <param name="Compliant">True if message passes policy checks, false if violation detected</param>
    /// <param name="Violation">Description of policy violation if Compliant is false</param>
    public record GuardCheckResponse(
        [property: JsonPropertyName("compliant")] bool Compliant,
        [property: JsonPropertyName("violation")] string? Violation);

    /// <summary>
    /// Result returned by <see cref="Interfaces.IGuardRailService"/> after evaluating
    /// a user message against content and policy rules.
    /// </summary>
    /// <param name="Compliant">
    /// <c>true</c> if the message passes all guard-rail checks; <c>false</c> if it violates a rule.
    /// </param>
    /// <param name="Violation">
    /// Human-readable description of the violated rule when <paramref name="Compliant"/> is <c>false</c>;
    /// <c>null</c> otherwise.
    /// </param>
    /// <remarks>
    /// This record is the public contract of <see cref="Interfaces.IGuardRailService"/> and is
    /// intentionally decoupled from <see cref="GuardCheckResponse"/>, which is an LLM wire-format DTO
    /// used only by <see cref="Services.LLMGuardRailService"/> internally.
    /// </remarks>
    public record GuardRailResult(
        bool Compliant,
        string? Violation);
    
    /// <summary>
    /// Sent by an agent back to the supervisor when the LLM provider rejects the request
    /// due to a content filter (e.g. Azure OpenAI content_filter).
    /// The supervisor treats this identically to a guard rejection and routes the reply
    /// to the user-facing sender captured in its current processing state.
    /// </summary>
    public record ContentFilterRejection;

    // ==========================================================================
    // CLASSIFICATION MESSAGES
    // ==========================================================================

    /// <summary>
    /// LLM response from ClassifierActor containing intent classification.
    /// Deserialized from JSON returned by the LLM.
    /// </summary>
    /// <param name="Intent">Classified intent name (e.g., "billing", "contract", "other")</param>
    /// <param name="Confidence">Confidence score from 0.0 to 1.0</param>
    public record ClassificationResponse(
        [property: JsonPropertyName("intent")] string Intent,
        [property: JsonPropertyName("confidence")] double Confidence);

    /// <summary>
    /// Internal classification result used by the conversation pipeline.
    /// Contains the classified intent and additional metadata for routing and diagnostics.
    /// </summary>
    /// <param name="Intent">Classified intent name</param>
    /// <param name="Metadata">
    /// Additional metadata dictionary (e.g., confidence score, error codes, classification notes)
    /// </param>
    public record ClassificationResult(
        string Intent,
        Dictionary<string, string> Metadata);

    // ==========================================================================
    // AGENT REQUEST/RESPONSE MODELS
    // ==========================================================================

    /// <summary>
    /// Request message sent to MorganaAgent instances for processing user input.
    /// Contains the user's message and optional classification result from ClassifierActor.
    /// </summary>
    /// <param name="ConversationId">Unique identifier of the conversation</param>
    /// <param name="Content">User message text to process (null for tool-only interactions)</param>
    /// <param name="Classification">Optional intent classification result (null for follow-up messages to active agents)</param>
    /// <param name="TurnContext">OpenTelemetry activity context for the current turn span.</param>
    /// <param name="Capabilities">
    /// Expressive capabilities advertised by the outbound channel serving this turn. Agents
    /// consult these to avoid engaging features the channel cannot render — most notably,
    /// skipping LLM streaming entirely when <see cref="ChannelCapabilities.SupportsStreaming"/>
    /// is false. When null, agents assume full capabilities (legacy/test paths).
    /// </param>
    public record AgentRequest(
        string ConversationId,
        string? Content,
        ClassificationResult? Classification,
        ActivityContext TurnContext = default,
        ChannelCapabilities? Capabilities = null);

    /// <summary>
    /// Response message from MorganaAgent instances after processing a request.
    /// Indicates the agent's response text, completion status, and optional quick reply buttons.
    /// </summary>
    /// <param name="Response">Agent's response text (may contain #INT# token for multi-turn interactions)</param>
    /// <param name="IsCompleted">
    /// True if agent has completed its task (conversation returns to idle).
    /// False if agent needs more user input (agent becomes active for follow-up messages).
    /// </param>
    /// <param name="QuickReplies">
    /// Optional list of quick reply buttons to display to the user.
    /// Agents can provide guided choices for better UX (e.g., contract sections, invoice selection).
    /// If null, no quick replies are shown.
    /// </param>
    /// <param name="RichCard">
    /// Optional rich card to display to the user.
    /// Agents can provide structured contents for more engaging UX (e.g., contract terms,  invoice details).
    /// If null, no rich cards are shown.
    /// </param>
    public record AgentResponse(
        string Response,
        bool IsCompleted = true,
        List<QuickReply>? QuickReplies = null,
        RichCard? RichCard = null);

    /// <summary>
    /// Response from RouterActor containing both the agent's response and a reference to the agent actor.
    /// Used to track which agent is handling the request for multi-turn conversation management.
    /// </summary>
    /// <param name="Response">Agent's response text</param>
    /// <param name="IsCompleted">Whether the agent has completed its task</param>
    /// <param name="AgentRef">Actor reference to the agent that generated this response</param>
    /// <param name="QuickReplies">Optional list of quick reply buttons from the agent</param>
    /// <param name="RichCard">Optional rich card from the agent</param>
    public record ActiveAgentResponse(
        string Response,
        bool IsCompleted,
        IActorRef AgentRef,
        List<QuickReply>? QuickReplies = null,
        RichCard? RichCard = null);

    /// <summary>
    /// Represents a streaming chunk from an agent during real-time response generation.
    /// Sent incrementally to enable progressive UI rendering.
    /// </summary>
    public record AgentStreamChunk(
        string Text);

    // ==========================================================================
    // HTTP REQUEST/RESPONSE MODELS
    // ==========================================================================

    /// <summary>
    /// HTTP request model for starting a new conversation via REST API.
    /// </summary>
    /// <param name="ConversationId">Unique identifier for the conversation to create</param>
    /// <param name="ChannelMetadata">Metadata advertised by the originating channel/client at
    /// the handshake: channel name (e.g. <c>cauldron</c>, <c>twilio-sms</c>, …) plus the expressive
    /// capability budget. Required: Morgana rejects start requests from channels that do not
    /// announce both their name and their capability budget.</param>
    /// <param name="InitialContext">Optional initial context information (reserved for future use)</param>
    public record StartConversationRequest(
        string ConversationId,
        ChannelMetadata? ChannelMetadata,
        Dictionary<string, object>? InitialContext = null);

    /// <summary>
    /// HTTP request model for sending a message to a conversation via REST API.
    /// </summary>
    /// <param name="ConversationId">Unique identifier of the target conversation</param>
    /// <param name="Text">Message text from the user</param>
    /// <param name="Metadata">Optional metadata dictionary (reserved for future use)</param>
    public record SendMessageRequest(
        string ConversationId,
        string Text,
        Dictionary<string, object>? Metadata = null
    );

    // ==========================================================================
    // QUICK REPLY SYSTEM
    // ==========================================================================

    /// <summary>
    /// Interactive button displayed to the user for quick action selection.
    /// Used in presentation messages, agent responses, and JSON deserialization from LLM tool calls.
    /// </summary>
    /// <param name="Id">Unique identifier for the quick reply (typically matches intent name or action)</param>
    /// <param name="Label">Display text shown on the button with emoji (e.g., "📄 View Invoices")</param>
    /// <param name="Value">Message text sent when user clicks the button (e.g., "Show my invoices")</param>
    /// <param name="Termination">Reserved flag indicating that the button is the one for exiting to Morgana</param>
    /// <remarks>
    /// <para><strong>Dual Purpose:</strong></para>
    /// <para>This record serves both as a runtime model and JSON serialization DTO:</para>
    /// <list type="bullet">
    /// <item><term>Runtime Model</term><description>Used by AgentResponse, ConversationResponse, StructuredMessage</description></item>
    /// <item><term>JSON DTO</term><description>Deserialized from LLM SetQuickReplies tool calls</description></item>
    /// </list>
    /// <para><strong>JSON Format:</strong></para>
    /// <code>
    /// {
    ///   "id": "no-internet",
    ///   "label": "🔴 No Internet Connection",
    ///   "value": "Show me the no-internet assistance guide"
    /// }
    /// </code>
    /// </remarks>
    public record QuickReply(
        [property: JsonPropertyName("id")] string Id,
        [property: JsonPropertyName("label")] string Label,
        [property: JsonPropertyName("value")] string Value,
        [property: JsonPropertyName("termination")] bool? Termination=false)
    {
        private const int ButtonPadding = 4;

        /// <summary>
        /// Estimated rendering footprint of this quick reply button in visual characters.
        /// Only the visible label contributes — Id/Value are wire-only and never surface to the user.
        /// </summary>
        public int EstimateCost() => Label.Length + ButtonPadding;
    }

    // ==========================================================================
    // CHANNEL ABSTRACTION
    // ==========================================================================

    /// <summary>
    /// Declares the expressive capabilities of a channel (the transport + client pair
    /// that ultimately renders Morgana's responses to an end user). Producers of
    /// outbound messages (presenter, agents, supervisor) consult this contract to
    /// decide whether a feature can be used as-is or must be degraded for the
    /// target channel.
    /// </summary>
    /// <param name="SupportsRichCards">True if the channel can render <see cref="RichCard"/> payloads with structured components.</param>
    /// <param name="SupportsQuickReplies">True if the channel can render <see cref="QuickReply"/> buttons.</param>
    /// <param name="SupportsStreaming">True if the channel can deliver progressive chunks (e.g. SignalR ReceiveStreamChunk).</param>
    /// <param name="SupportsMarkdown">True if the channel renders Markdown formatting in message text.</param>
    /// <param name="MaxMessageLength">Optional hard limit on message text length in characters; null means no limit.</param>
    public record ChannelCapabilities(
        bool SupportsRichCards,
        bool SupportsQuickReplies,
        bool SupportsStreaming,
        bool SupportsMarkdown,
        int? MaxMessageLength = null)
    {
        /// <summary>
        /// Shared singleton representing the full legacy capability set (all features enabled,
        /// no length cap). Use this anywhere a "rich" channel needs to be described instead of
        /// allocating a new instance, both for the reference channel's static budget and for
        /// fallback paths (e.g. legacy conversations restored without a persisted handshake).
        /// </summary>
        public static readonly ChannelCapabilities Default = new ChannelCapabilities(
            SupportsRichCards: true,
            SupportsQuickReplies: true,
            SupportsStreaming: true,
            SupportsMarkdown: true,
            MaxMessageLength: null);
    }

    /// <summary>
    /// Identity + addressing coordinates declared by a channel at the conversation-start
    /// handshake: who the channel is (<see cref="ChannelName"/>) and how Morgana should route
    /// outbound traffic to it (<see cref="DeliveryMode"/>). Non-capability string fields live
    /// here so they can grow (e.g. callback URLs, routing keys) without perturbing the
    /// capability budget.
    /// </summary>
    /// <remarks>
    /// <para>Coordinates are "where/how to reach the channel", capabilities are "what the
    /// channel can render" — the two concerns evolve on different axes, which is why they sit
    /// in distinct records and are composed by <see cref="ChannelMetadata"/>.</para>
    /// </remarks>
    public record ChannelCoordinates
    {
        /// <summary>Free-form label declared by the channel (e.g. <c>cauldron</c>,
        /// <c>twilio-sms</c>). Persisted alongside the budget and preserved across restarts.
        /// Normalised (trimmed + lowercased) at ingress so the name space stays case-insensitive end-to-end.
        /// Intended for observability, routing and per-channel policy; explicitly NOT a trust boundary.</summary>
        public required string ChannelName { get; init; }

        /// <summary>Transport dispatch key declared by the channel at the handshake (e.g.
        /// <c>signalr</c>, <c>webhook</c>). Stored free-form as a string and normalised
        /// (trimmed + lowercased) at ingress; the finite set of valid keys is owned by the
        /// channel service factory registry, not by this record, so new transports can be added
        /// without reshaping this contract. The start-conversation gate rejects missing or
        /// whitespace-only values; an unregistered key is rejected at dispatch.</summary>
        public required string DeliveryMode { get; init; }

        /// <summary>Absolute URL where Morgana POSTs outbound <see cref="ChannelMessage"/>s for
        /// push-style transports (e.g. <c>deliveryMode=webhook</c>). Null for pull/duplex
        /// transports (e.g. <c>signalr</c>) that do not need a callback target. The
        /// start-conversation gate enforces the presence and well-formedness of this field per
        /// <see cref="DeliveryMode"/>: unknown to the record itself, the requirement is owned by
        /// the transport-specific validation at the ingress.</summary>
        public string? CallbackUrl { get; init; }
    }

    /// <summary>
    /// Wraps a <see cref="ChannelCapabilities"/> budget with the <see cref="ChannelCoordinates"/>
    /// of the channel that declared it, so Morgana can track "who the channel is and how to reach
    /// it" (Cauldron web UI, a Twilio SMS bridge, an IVR gateway, …) in addition to "what the
    /// channel can render".
    /// </summary>
    /// <remarks>
    /// <para>Identity/addressing and expressive budget are kept in two distinct sub-records so
    /// each can evolve independently: new addressing fields (callback URLs, routing keys) land
    /// on <see cref="ChannelCoordinates"/>, new rendering features land on
    /// <see cref="ChannelCapabilities"/>.</para>
    /// </remarks>
    public record ChannelMetadata
    {
        /// <summary>Identity + addressing coordinates of the channel (name, delivery mode, …).</summary>
        public required ChannelCoordinates Coordinates { get; init; }

        /// <summary>Expressive capability budget advertised by the channel.</summary>
        public required ChannelCapabilities Capabilities { get; init; }
    }

    /// <summary>
    /// Transport-agnostic envelope for a structured outbound message delivered to the end user
    /// through an <see cref="Interfaces.IChannelService"/>. Carries the full payload an actor
    /// needs to publish: text, quick replies, rich card, agent identity, error reason, and
    /// rendering metadata (message type, fading duration).
    /// </summary>
    /// <remarks>
    /// <para><strong>Wire contract:</strong></para>
    /// <para>This DTO is serialized by the active channel implementation (for SignalR +
    /// Cauldron, via the "ReceiveMessage" event). Property names and types intentionally match
    /// <c>Cauldron.Messages.SignalRMessage</c> so the wire format stays identical.</para>
    /// <para><strong>Contract sync policy:</strong></para>
    /// <list type="bullet">
    /// <item>Required fields must match exactly between backend and frontend</item>
    /// <item>Optional fields can be added without breaking compatibility (use default values)</item>
    /// <item>Field removal or type changes are BREAKING and require coordinated deployment</item>
    /// </list>
    /// </remarks>
    public sealed class ChannelMessage
    {
        /// <summary>
        /// Unique identifier of the conversation this message belongs to.
        /// Used for channel group routing and conversation correlation.
        /// </summary>
        [JsonPropertyName("conversationId")]
        public required string ConversationId { get; init; }

        /// <summary>
        /// Message text to display to the user.
        /// May contain markdown, emojis, or plain text depending on UI renderer capabilities.
        /// </summary>
        [JsonPropertyName("text")]
        public required string Text { get; init; }

        /// <summary>
        /// UTC timestamp when this message was generated by the backend.
        /// Used for message ordering and display in the UI.
        /// </summary>
        [JsonPropertyName("timestamp")]
        public DateTime Timestamp { get; init; } = DateTime.UtcNow;

        /// <summary>
        /// Type of message for UI rendering and styling.
        /// Valid values: "assistant", "presentation", "system", "system_warning", "error"
        /// Default: "assistant"
        /// </summary>
        [JsonPropertyName("messageType")]
        public string MessageType { get; init; } = "assistant";

        /// <summary>
        /// Optional list of quick reply buttons for user interaction.
        /// When present, UI should render these as clickable buttons.
        /// QuickReplies are generated by the LLM and constitute a stable contract.
        /// Null when no quick replies are available.
        /// </summary>
        [JsonPropertyName("quickReplies")]
        public List<QuickReply>? QuickReplies { get; init; }

        /// <summary>
        /// Optional rich card for structured visual presentation.
        /// When present, renders structured data components instead of plain text formatting.
        /// </summary>
        [JsonPropertyName("richCard")]
        public RichCard? RichCard { get; init; }

        /// <summary>
        /// Optional error reason code when MessageType is "error" or "system_warning".
        /// Examples: "timeout", "rate_limit_exceeded", "guard_violation".
        /// Null for non-error messages.
        /// </summary>
        [JsonPropertyName("errorReason")]
        public string? ErrorReason { get; init; }

        /// <summary>
        /// Name of the agent that generated this message.
        /// Examples: "Morgana", "Morgana (Billing)", "Morgana (Technical Support)".
        /// Default: "Morgana"
        /// </summary>
        [JsonPropertyName("agentName")]
        public string AgentName { get; init; } = "Morgana";

        /// <summary>
        /// Indicates whether this specialized agent has completed its task.
        /// When true, control returns to the base Morgana orchestrator.
        /// Default: false
        /// </summary>
        [JsonPropertyName("agentCompleted")]
        public bool AgentCompleted { get; init; }

        /// <summary>
        /// Duration of the eventual fading message which will be generated
        /// by this channel message (depending on its MessageType).
        /// </summary>
        [JsonPropertyName("fadingMessageDurationSeconds")]
        public int? FadingMessageDurationSeconds { get; set; } = 10;

        /// <summary>
        /// Conversation-level metadata (e.g. remaining dust budget). Characterises the
        /// conversation, not this message. Null when no such metadata is available.
        /// </summary>
        [JsonPropertyName("conversationMetadata")]
        public ConversationMetadata? ConversationMetadata { get; init; }
    }

    /// <summary>
    /// LLM-generated presentation response from ConversationSupervisorActor.
    /// Contains the welcome message and quick reply buttons for user interaction.
    /// Deserialized from JSON returned by the LLM when generating presentation messages.
    /// </summary>
    /// <param name="Message">Welcome/presentation message text (2-4 sentences)</param>
    /// <param name="QuickReplies">List of quick reply button definitions</param>
    public record PresentationResponse(
        [property: JsonPropertyName("message")] string Message,
        [property: JsonPropertyName("quickReplies")] List<QuickReply> QuickReplies);

    /// <summary>
    /// LLM-generated rewrite produced by the ChannelAdapter prompt. Contains the degraded
    /// plain-text rendering of an outbound message and the (optionally surviving) quick replies
    /// that still fit inside the target channel's capabilities.
    /// </summary>
    /// <param name="Text">Rewritten message text, channel-compliant and free of unsupported features.</param>
    /// <param name="QuickReplies">Quick replies preserved by the rewrite, or null when the channel cannot carry them.</param>
    public record ChannelAdapterResponse(
        [property: JsonPropertyName("text")] string Text,
        [property: JsonPropertyName("quickReplies")] List<QuickReply>? QuickReplies);

    /// <summary>
    /// Result returned by <see cref="Interfaces.IPresenterService.GenerateAsync"/> containing
    /// the welcome message and quick reply buttons for the start of a conversation.
    /// </summary>
    /// <param name="Message">
    /// Welcome/presentation message text to display to the user (2-4 sentences).
    /// </param>
    /// <param name="QuickReplies">
    /// List of quick reply buttons derived from displayable intents.
    /// May be empty if no intents are configured; never null.
    /// </param>
    /// <remarks>
    /// This record is the public contract of <see cref="Interfaces.IPresenterService"/> and is
    /// intentionally decoupled from <see cref="PresentationResponse"/>, which is an LLM
    /// wire-format DTO used only by <see cref="Services.LLMPresenterService"/> internally.
    /// </remarks>
    public record PresentationResult(
        string Message,
        List<QuickReply> QuickReplies);

    // ==========================================================================
    // RICH CARD SYSTEM
    // ==========================================================================
    // Add this section to Morgana.AI/Records.cs

    /// <summary>
    /// Rich card container for structured visual presentation of complex data.
    /// Used to render information (invoices, profiles, reports) with visual hierarchy
    /// instead of plain text walls.
    /// </summary>
    /// <param name="Title">Main title of the card</param>
    /// <param name="Subtitle">Optional subtitle or secondary information</param>
    /// <param name="Components">Array of visual components to render</param>
    /// <remarks>
    /// <para><strong>Usage:</strong></para>
    /// <para>LLM generates rich cards via SetRichCard tool when presenting structured data.
    /// Cards flow through actor pipeline (Agent → Router → Supervisor → Manager → SignalR → Cauldron).</para>
    /// <para><strong>Constraints:</strong></para>
    /// <list type="bullet">
    /// <item>Maximum nesting depth: 3 levels (enforced by SetRichCard tool)</item>
    /// <item>Maximum 50 components total (prevents abuse)</item>
    /// <item>Components must be from known dictionary (unknown types fallback to text in Cauldron)</item>
    /// </list>
    /// </remarks>
    public record RichCard(
        [property: JsonPropertyName("title")] string Title,
        [property: JsonPropertyName("subtitle")] string? Subtitle,
        [property: JsonPropertyName("components")] List<CardComponent> Components
    )
    {
        private const int TitleOverhead = 4;
        private const int SubtitleOverhead = 2;

        /// <summary>
        /// Estimated rendering footprint in visual characters. Used by the channel adapter
        /// to decide whether the card fits inside a channel's <c>MaxMessageLength</c> budget.
        /// Conservative: prefer overestimating (trigger a downgrade) over letting an
        /// oversized payload slip through.
        /// </summary>
        public int EstimateCost() =>
            Title.Length + TitleOverhead
            + (Subtitle?.Length + SubtitleOverhead ?? 0)
            + Components.Sum(component => component.EstimateCost());
    }

    /// <summary>
    /// Base class for all card components.
    /// Uses JSON polymorphic serialization for type discrimination.
    /// </summary>
    /// <remarks>
    /// <para><strong>Component Dictionary:</strong></para>
    /// <list type="bullet">
    /// <item><term>text_block</term><description>Free-form narrative text</description></item>
    /// <item><term>key_value</term><description>Label-value pairs for structured data</description></item>
    /// <item><term>divider</term><description>Visual separator between sections</description></item>
    /// <item><term>list</term><description>Bulleted, numbered, or plain item lists</description></item>
    /// <item><term>section</term><description>Nestable grouping with title/subtitle</description></item>
    /// <item><term>grid</term><description>2-4 column layout for side-by-side data</description></item>
    /// <item><term>badge</term><description>Status indicators (success, warning, error, info, neutral)</description></item>
    /// <item><term>image</term><description>Multimedia content of type image</description></item>
    /// </list>
    /// <para><strong>Extensibility:</strong></para>
    /// <para>Implementers can add new component types by:</para>
    /// <list type="number">
    /// <item>Adding new record inheriting from CardComponent</item>
    /// <item>Adding JsonDerivedType attribute to CardComponent</item>
    /// <item>Creating corresponding Razor component in Cauldron</item>
    /// </list>
    /// </remarks>
    [JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
    [JsonDerivedType(typeof(TextBlockComponent), "text_block")]
    [JsonDerivedType(typeof(KeyValueComponent), "key_value")]
    [JsonDerivedType(typeof(DividerComponent), "divider")]
    [JsonDerivedType(typeof(ListComponent), "list")]
    [JsonDerivedType(typeof(SectionComponent), "section")]
    [JsonDerivedType(typeof(GridComponent), "grid")]
    [JsonDerivedType(typeof(BadgeComponent), "badge")]
    [JsonDerivedType(typeof(ImageComponent), "image")]
    public abstract record CardComponent
    {
        /// <summary>
        /// Estimated rendering footprint of this component in visual characters.
        /// Conservative by design: overestimating triggers a channel downgrade, while
        /// underestimating risks overflow past the channel's length budget.
        /// </summary>
        public abstract int EstimateCost();
    }

    /// <summary>
    /// Free-form text block component for narrative content within cards.
    /// </summary>
    /// <param name="Content">Text content (supports multiline)</param>
    /// <param name="Style">Visual styling for the text</param>
    public record TextBlockComponent(
        [property: JsonPropertyName("content")] string Content,
        [property: JsonPropertyName("style")] TextStyle Style = TextStyle.Normal
    ) : CardComponent
    {
        private const int Overhead = 2;

        /// <inheritdoc />
        public override int EstimateCost() => Content.Length + Overhead;
    }

    /// <summary>
    /// Key-value pair component for structured label-value data.
    /// </summary>
    /// <param name="Key">Label/field name (e.g., "Cliente", "Totale")</param>
    /// <param name="Value">Corresponding value (e.g., "Acme Corp", "€1.250,00")</param>
    /// <param name="Emphasize">True to highlight this pair visually (e.g., bold, larger font)</param>
    public record KeyValueComponent(
        [property: JsonPropertyName("key")] string Key,
        [property: JsonPropertyName("value")] string Value,
        [property: JsonPropertyName("emphasize")] bool Emphasize = false
    ) : CardComponent
    {
        private const int Overhead = 4;

        /// <inheritdoc />
        public override int EstimateCost() => Key.Length + Value.Length + Overhead;
    }

    /// <summary>
    /// Visual divider/separator component.
    /// Renders as horizontal line to separate logical sections.
    /// </summary>
    public record DividerComponent : CardComponent
    {
        private const int Overhead = 4;

        /// <inheritdoc />
        public override int EstimateCost() => Overhead;
    }

    /// <summary>
    /// List component for displaying multiple related items.
    /// </summary>
    /// <param name="Items">Array of text items to display</param>
    /// <param name="Style">List presentation style (bullet, numbered, plain)</param>
    public record ListComponent(
        [property: JsonPropertyName("items")] List<string> Items,
        [property: JsonPropertyName("style")] ListStyle Style = ListStyle.Bullet
    ) : CardComponent
    {
        private const int ItemOverhead = 3;

        /// <inheritdoc />
        public override int EstimateCost() => Items.Sum(item => item.Length + ItemOverhead);
    }

    /// <summary>
    /// Section component for logical grouping with nesting support.
    /// Enables hierarchical organization of card content (max depth: 3).
    /// </summary>
    /// <param name="Title">Section title/heading</param>
    /// <param name="Subtitle">Optional section subtitle</param>
    /// <param name="Components">Child components within this section (can include nested sections)</param>
    public record SectionComponent(
        [property: JsonPropertyName("title")] string Title,
        [property: JsonPropertyName("subtitle")] string? Subtitle,
        [property: JsonPropertyName("components")] List<CardComponent> Components
    ) : CardComponent
    {
        private const int TitleOverhead = 4;
        private const int SubtitleOverhead = 2;

        /// <inheritdoc />
        public override int EstimateCost() =>
            Title.Length + TitleOverhead
            + (Subtitle?.Length + SubtitleOverhead ?? 0)
            + Components.Sum(component => component.EstimateCost());
    }

    /// <summary>
    /// Grid component for side-by-side data presentation.
    /// </summary>
    /// <param name="Columns">Number of columns (2-4 recommended)</param>
    /// <param name="Items">Grid cells with key-value pairs</param>
    public record GridComponent(
        [property: JsonPropertyName("columns")] int Columns,
        [property: JsonPropertyName("items")] List<GridItem> Items
    ) : CardComponent
    {
        private const int ItemOverhead = 4;

        /// <inheritdoc />
        public override int EstimateCost() =>
            Items.Sum(item => item.Key.Length + item.Value.Length + ItemOverhead);
    }

    /// <summary>
    /// Individual grid cell containing a key-value pair.
    /// </summary>
    /// <param name="Key">Label for this grid cell</param>
    /// <param name="Value">Value for this grid cell</param>
    public record GridItem(
        [property: JsonPropertyName("key")] string Key,
        [property: JsonPropertyName("value")] string Value
    );

    /// <summary>
    /// Badge component for status indicators and categorical labels.
    /// </summary>
    /// <param name="Text">Badge text (e.g., "Pagata", "In sospeso")</param>
    /// <param name="Variant">Visual variant determining color/style</param>
    public record BadgeComponent(
        [property: JsonPropertyName("text")] string Text,
        [property: JsonPropertyName("variant")] BadgeVariant Variant = BadgeVariant.Neutral
    ) : CardComponent
    {
        private const int Overhead = 2;

        /// <inheritdoc />
        public override int EstimateCost() => Text.Length + Overhead;
    }

    /// <summary>
    /// Image component for displaying visual content from URLs.
    /// Supports captions, alt text, and size variants.
    /// </summary>
    /// <param name="Src">Image URL (must be publicly accessible, HTTPS recommended)</param>
    /// <param name="Alt">Alternative text for accessibility (optional but recommended)</param>
    /// <param name="Caption">Optional caption displayed below the image</param>
    /// <param name="Size">Display size variant (small, medium, large, or full)</param>
    public record ImageComponent(
        [property: JsonPropertyName("src")] string Src,
        [property: JsonPropertyName("alt")] string? Alt = null,
        [property: JsonPropertyName("caption")] string? Caption = null,
        [property: JsonPropertyName("size")] ImageSize Size = ImageSize.Medium
    ) : CardComponent
    {
        private const int PlaceholderOverhead = 10;
        private const int CaptionOverhead = 2;

        /// <inheritdoc />
        public override int EstimateCost() =>
            PlaceholderOverhead
            + (Alt?.Length ?? 0)
            + (Caption?.Length + CaptionOverhead ?? 0);
    }

    // Enums for component styling

    /// <summary>
    /// Text styling options for TextBlockComponent.
    /// </summary>
    [JsonConverter(typeof(JsonStringEnumConverter<TextStyle>))]
    public enum TextStyle
    {
        /// <summary>Default body text.</summary>
        [JsonPropertyName("normal")] Normal,
        /// <summary>Bold emphasis.</summary>
        [JsonPropertyName("bold")] Bold,
        /// <summary>De-emphasized / secondary text.</summary>
        [JsonPropertyName("muted")] Muted,
        /// <summary>Reduced font size, typically for captions or footnotes.</summary>
        [JsonPropertyName("small")] Small
    }

    /// <summary>
    /// List presentation styles for ListComponent.
    /// </summary>
    [JsonConverter(typeof(JsonStringEnumConverter<ListStyle>))]
    public enum ListStyle
    {
        /// <summary>Unordered list with bullet markers.</summary>
        [JsonPropertyName("bullet")] Bullet,
        /// <summary>Ordered list with numeric markers.</summary>
        [JsonPropertyName("numbered")] Numbered,
        /// <summary>List with no markers.</summary>
        [JsonPropertyName("plain")] Plain
    }

    /// <summary>
    /// Badge color variants for BadgeComponent.
    /// </summary>
    [JsonConverter(typeof(JsonStringEnumConverter<BadgeVariant>))]
    public enum BadgeVariant
    {
        /// <summary>Positive outcome (green).</summary>
        [JsonPropertyName("success")] Success,
        /// <summary>Cautionary state (amber).</summary>
        [JsonPropertyName("warning")] Warning,
        /// <summary>Failure or blocking condition (red).</summary>
        [JsonPropertyName("error")] Error,
        /// <summary>Informational note (blue).</summary>
        [JsonPropertyName("info")] Info,
        /// <summary>Unstyled / default badge.</summary>
        [JsonPropertyName("neutral")] Neutral
    }

    /// <summary>
    /// Size variants for image components.
    /// Controls maximum width of displayed images with responsive behavior.
    /// </summary>
    /// <remarks>
    /// <para><strong>Size Guidelines:</strong></para>
    /// <list type="bullet">
    /// <item><term>Small</term><description>200px max-width - for thumbnails, icons, small avatars</description></item>
    /// <item><term>Medium</term><description>400px max-width - default, good for most images</description></item>
    /// <item><term>Large</term><description>600px max-width - for detailed images, diagrams</description></item>
    /// <item><term>Full</term><description>100% container width - for banners, wide images</description></item>
    /// </list>
    /// <para>On mobile devices (&lt;768px), Medium and Large automatically scale to 100% width.</para>
    /// </remarks>
    [JsonConverter(typeof(JsonStringEnumConverter<ImageSize>))]
    public enum ImageSize
    {
        /// <summary>200px max-width — thumbnails, icons, avatars.</summary>
        [JsonPropertyName("small")] Small,
        /// <summary>400px max-width — default for most images.</summary>
        [JsonPropertyName("medium")] Medium,
        /// <summary>600px max-width — detailed images or diagrams.</summary>
        [JsonPropertyName("large")] Large,
        /// <summary>100% container width — banners and wide images.</summary>
        [JsonPropertyName("full")] Full
    }

    // ==========================================================================
    // PRESENTATION FLOW MESSAGES
    // ==========================================================================

    /// <summary>
    /// Trigger message to generate and send the initial presentation/welcome message.
    /// Sent automatically when a conversation is created.
    /// </summary>
    public record GeneratePresentationMessage;

    /// <summary>
    /// Context containing the generated presentation message and available intents.
    /// Used internally by ConversationSupervisorActor to send presentation via SignalR.
    /// </summary>
    /// <param name="Message">Welcome message text (either LLM-generated or fallback)</param>
    /// <param name="Intents">List of available intent definitions</param>
    public record PresentationContext(
        string Message,
        List<IntentDefinition> Intents)
    {
        /// <summary>
        /// LLM-generated quick replies (takes precedence over Intents if available).
        /// If null, quick replies are derived from Intents directly.
        /// </summary>
        public List<QuickReply>? LLMQuickReplies { get; init; }
    }

    // ==========================================================================
    // CONTEXT WRAPPERS FOR BECOME/PIPETO PATTERN
    // ==========================================================================
    // These records wrap async operation results with the original sender reference
    // to ensure correct message routing after async operations complete.

    // --- ConversationSupervisorActor Contexts ---

    /// <summary>
    /// Processing context maintained throughout the conversation pipeline.
    /// Captures the original message, sender, and classification result as it flows through states.
    /// </summary>
    /// <param name="OriginalMessage">The user message being processed</param>
    /// <param name="OriginalSender">Actor reference to reply to (typically ConversationManagerActor)</param>
    /// <param name="Classification">Intent classification result (populated after ClassifierActor processes message)</param>
    /// <param name="TurnContext">OpenTelemetry activity context for the current turn span.</param>
    public record ProcessingContext(
        UserMessage OriginalMessage,
        IActorRef OriginalSender,
        ClassificationResult? Classification = null,
        ActivityContext TurnContext = default);

    // --- MorganaAgent Contexts ---

    /// <summary>
    /// Wraps an unhandled agent exception so the agent can route its error-handling
    /// through its own mailbox (Self.Tell) and still remember who to reply to.
    /// </summary>
    /// <param name="Failure">Akka.NET failure status carrying the original exception</param>
    /// <param name="OriginalSender">The actor that sent the AgentRequest (typically RouterActor)</param>
    public record FailureContext(
        Status.Failure Failure,
        IActorRef OriginalSender);

    // ==========================================================================
    // INTENT CONFIGURATION RECORDS
    // ==========================================================================

    /// <summary>
    /// Intent definition for classification and presentation.
    /// Defines what intents the system can recognize and how to present them to users.
    /// </summary>
    /// <param name="Name">Intent identifier (lowercase, e.g., "billing", "contract")</param>
    /// <param name="Description">Intent description for classifier LLM</param>
    /// <param name="Label">User-facing label with emoji (e.g., "📄 Billing") for quick replies</param>
    /// <param name="DefaultValue">Sample user message for this intent (used in quick reply value)</param>
    public record IntentDefinition(
        [property: JsonPropertyName("Name")] string Name,
        [property: JsonPropertyName("Description")] string Description,
        [property: JsonPropertyName("Label")] string? Label,
        [property: JsonPropertyName("DefaultValue")] string? DefaultValue = null);

    // ==========================================================================
    // INTENT CONFIGURATION RECORDS
    // ==========================================================================

    /// <summary>
    /// Collection of intent definitions with utility methods for classification and presentation.
    /// Provides filtering and formatting capabilities for different use cases.
    /// </summary>
    public record IntentCollection
    {
        /// <summary>
        /// List of all intent definitions.
        /// </summary>
        public List<IntentDefinition> Intents { get; set; }

        /// <summary>
        /// Initializes a new instance of IntentCollection.
        /// </summary>
        /// <param name="intents">List of intent definitions to wrap</param>
        public IntentCollection(List<IntentDefinition> intents)
        {
            Intents = intents;
        }

        /// <summary>
        /// Converts intents to a dictionary mapping intent names to descriptions.
        /// Used by ClassifierActor to format intents for LLM classification prompt.
        /// </summary>
        /// <returns>Dictionary with intent name as key and description as value</returns>
        /// <remarks>
        /// <para><strong>Usage in ClassifierActor:</strong></para>
        /// <code>
        /// IntentCollection intentCollection = new IntentCollection(intents);
        /// Dictionary&lt;string, string&gt; intentDict = intentCollection.AsDictionary();
        ///
        /// // Format for LLM: "billing (requests to view invoices)|contract (requests to summarize contract)"
        /// string formattedIntents = string.Join("|", intentDict.Select(kvp => $"{kvp.Key} ({kvp.Value})"));
        /// </code>
        /// </remarks>
        public Dictionary<string, string> AsDictionary()
        {
            return Intents.ToDictionary(i => i.Name, i => i.Description);
        }

        /// <summary>
        /// Gets intents that should be displayed in presentation quick replies.
        /// Excludes the "other" fallback intent and intents without labels.
        /// </summary>
        /// <returns>List of displayable intent definitions</returns>
        /// <remarks>
        /// <para><strong>Filtering Rules:</strong></para>
        /// <list type="bullet">
        /// <item>Exclude "other" intent (special fallback, not user-selectable)</item>
        /// <item>Exclude intents without Label property (not meant for UI display)</item>
        /// </list>
        /// <para><strong>Usage in Presentation:</strong></para>
        /// <code>
        /// IntentCollection intentCollection = new IntentCollection(allIntents);
        /// List&lt;IntentDefinition&gt; displayable = intentCollection.GetDisplayableIntents();
        ///
        /// // Convert to quick replies for SignalR
        /// List&lt;QuickReply&gt; quickReplies = displayable
        ///     .Select(i => new QuickReply(i.Name, i.Label, i.DefaultValue))
        ///     .ToList();
        /// </code>
        /// </remarks>
        public List<IntentDefinition> GetDisplayableIntents()
        {
            return Intents
                .Where(i => !string.Equals(i.Name, "other", StringComparison.OrdinalIgnoreCase)
                              && !string.IsNullOrEmpty(i.Label))
                .ToList();
        }
    }

    // ==========================================================================
    // PROMPT CONFIGURATION RECORDS
    // ==========================================================================

    /// <summary>
    /// Root collection of prompts loaded from configuration files (morgana.json, agents.json).
    /// Used during JSON deserialization.
    /// </summary>
    /// <param name="Prompts">Array of prompt definitions</param>
    public record PromptCollection(
        Prompt[] Prompts);

    /// <summary>
    /// Prompt definition containing instructions, personality, and metadata for agents and actors.
    /// Loaded from morgana.json (framework prompts) or agents.json (domain prompts).
    /// </summary>
    /// <param name="ID">
    /// Unique prompt identifier.
    /// Framework: "Morgana", "Classifier", "Guard", "Presentation"
    /// Domain: Intent names like "billing", "contract", "monkeys"
    /// </param>
    /// <param name="Type">Prompt type (e.g., "SYSTEM", "INTENT")</param>
    /// <param name="SubType">Prompt subtype (e.g., "AGENT", "ACTOR", "PRESENTATION")</param>
    /// <param name="Target">Core prompt text defining role and capabilities</param>
    /// <param name="Instructions">Behavioral rules and guidelines</param>
    /// <param name="Formatting">Output formatting rules (markdown, length limits, etc.)</param>
    /// <param name="Personality">Optional tone and character traits</param>
    /// <param name="Language">Language code (e.g., "en-US", "it-IT")</param>
    /// <param name="Version">Prompt version for tracking changes</param>
    /// <param name="AdditionalProperties">
    /// List of dictionaries containing additional data like GlobalPolicies, Tools, ErrorAnswers, etc.
    /// </param>
    public record Prompt(
        string ID,
        string Type,
        string SubType,
        string Target,
        string Instructions,
        string Formatting,
        string? Personality,
        string Language,
        string Version,
        List<Dictionary<string, object>> AdditionalProperties)
    {
        /// <summary>
        /// Gets a strongly-typed additional property from the prompt configuration.
        /// Searches all AdditionalProperties dictionaries for the specified key.
        /// </summary>
        /// <typeparam name="T">Type to deserialize the property value into</typeparam>
        /// <param name="additionalPropertyName">Name of the property to retrieve (e.g., "Tools", "GlobalPolicies")</param>
        /// <returns>Deserialized property value</returns>
        /// <exception cref="KeyNotFoundException">Thrown if property not found in any AdditionalProperties dictionary</exception>
        /// <remarks>
        /// <para><strong>Common Additional Properties:</strong></para>
        /// <list type="bullet">
        /// <item><term>Tools</term><description>List&lt;ToolDefinition&gt; - Tool configurations for agents</description></item>
        /// <item><term>GlobalPolicies</term><description>List&lt;GlobalPolicy&gt; - Framework-level behavioral policies</description></item>
        /// <item><term>ErrorAnswers</term><description>List&lt;ErrorAnswer&gt; - Error message templates</description></item>
        /// <item><term>ProfanityTerms</term><description>List&lt;string&gt; - Terms for content moderation</description></item>
        /// <item><term>FallbackMessage</term><description>string - Default presentation message</description></item>
        /// </list>
        /// <para><strong>Usage Examples:</strong></para>
        /// <code>
        /// Prompt morganaPrompt = await promptResolver.ResolveAsync("Morgana");
        ///
        /// // Get global policies
        /// List&lt;GlobalPolicy&gt; policies = morganaPrompt.GetAdditionalProperty&lt;List&lt;GlobalPolicy&gt;&gt;("GlobalPolicies");
        ///
        /// // Get tools
        /// ToolDefinition[] tools = morganaPrompt.GetAdditionalProperty&lt;ToolDefinition[]&gt;("Tools");
        ///
        /// // Get error messages
        /// List&lt;ErrorAnswer&gt; errors = morganaPrompt.GetAdditionalProperty&lt;List&lt;ErrorAnswer&gt;&gt;("ErrorAnswers");
        /// </code>
        /// </remarks>
        public T GetAdditionalProperty<T>(string additionalPropertyName)
        {
            foreach (Dictionary<string, object> additionalProperties in AdditionalProperties)
            {
                if (additionalProperties.TryGetValue(additionalPropertyName, out object value))
                {
                    JsonElement element = (JsonElement)value;
                    return element.Deserialize<T>();
                }
            }
            throw new KeyNotFoundException($"AdditionalProperty with key '{additionalPropertyName}' was not found in the prompt with id='{ID}'");
        }
    }

    /// <summary>
    /// Global policy definition specifying framework-level behavioral rules.
    /// Applied to all agents and actors to enforce consistent behavior.
    /// </summary>
    /// <param name="Name">Policy name (e.g., "ContextHandling", "InteractiveToken")</param>
    /// <param name="Description">Detailed policy description with enforcement rules</param>
    /// <param name="Type">Policy type ("Critical" or "Operational")</param>
    /// <param name="Priority">Priority level (lower number = higher priority)</param>
    public record GlobalPolicy(
        string Name,
        string Description,
        string Type,
        int Priority);

    /// <summary>
    /// Error message template with named identifier.
    /// Used to provide consistent, user-friendly error messages across the system.
    /// </summary>
    /// <param name="Name">Error identifier (e.g., "GenericError", "LLMServiceError")</param>
    /// <param name="Content">Error message template (may contain placeholders like ((llm_error)))</param>
    public record ErrorAnswer(
        string Name,
        string Content);

    // ==========================================================================
    // TOOL CONFIGURATION RECORDS
    // ==========================================================================

    /// <summary>
    /// Tool definition specifying a callable tool method with parameters.
    /// Loaded from agents.json and used by MorganaToolAdapter to create AIFunction instances.
    /// </summary>
    /// <param name="Name">Tool method name (must match actual method name in MorganaTool class)</param>
    /// <param name="Description">Tool description for LLM understanding</param>
    /// <param name="Parameters">List of tool parameter definitions</param>
    public record ToolDefinition(
        string Name,
        string Description,
        IReadOnlyList<ToolParameter> Parameters);

    /// <summary>
    /// Tool parameter definition specifying parameter name, description, and behavior.
    /// Controls whether parameter comes from context or request, and if it's shared across agents.
    /// </summary>
    /// <param name="Name">Parameter name (must match method parameter name)</param>
    /// <param name="Description">Parameter description for LLM understanding</param>
    /// <param name="Required">Whether the parameter is required (true) or optional (false)</param>
    /// <param name="Scope">
    /// Parameter scope: "context" (retrieve via GetContextVariable) or "request" (use directly from user input)
    /// </param>
    /// <param name="Shared">
    /// Whether this context variable should be persisted in the conversation-scoped
    /// <c>shared_context</c> registry so other agents of the same conversation can hydrate it
    /// at the start of their next turn. Only applies when Scope="context". Default: false.
    /// </param>
    public record ToolParameter(
        string Name,
        string Description,
        bool Required,
        string Scope,
        bool Shared = false);

    // ==========================================================================
    // MODEL CONTEXT PROTOCOL
    // ==========================================================================

    /// <summary>
    /// Declares the transport mechanism used to communicate with an MCP server.
    /// Used by <see cref="UsesMCPServerAttribute"/> to disambiguate
    /// between remote HTTP servers and local stdio process-based servers.
    /// </summary>
    public enum MCPTransport
    {
        /// <summary>
        /// HTTP or HTTPS transport. The MCP server is a remote process reachable via a URL.
        /// </summary>
        Http,

        /// <summary>
        /// Standard I/O transport. The MCP server is a local executable spawned as a child process.
        /// Communication happens via stdin/stdout streams.
        /// </summary>
        Stdio
    }
}