from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator

from services.shared.core import Role


Channel = Literal["voice", "telegram", "whatsapp", "email", "webchat"]
InteractionStatus = Literal["new", "in_progress", "escalated", "closed", "abandoned"]
AIThreadState = Literal["queued", "thinking", "active", "handoff_required", "human_owned", "closed", "error"]
VoiceAIState = Literal[
    "queued",
    "greeting",
    "listening",
    "thinking",
    "speaking",
    "active",
    "handoff_requested",
    "handoff_required",
    "human_owned",
    "completed",
    "closed",
    "error",
]
RecordingStatus = Literal["ready", "archived", "missing"]
IvrSessionStatus = Literal["active", "completed", "abandoned"]
EventOutboxStatus = Literal["pending", "published", "failed"]
AsteriskForwardStatus = Literal["received", "processing", "forwarded", "failed"]
TelephonyStatus = Literal["ringing", "claimed", "connected", "ended", "failed"]
CallActionResultStatus = Literal["ok", "failed", "rejected"]
VoiceStartNameStatus = Literal["name_obtained", "name_not_obtained", "name_followup_required"]
VoiceStartNameSource = Literal["known_customer", "voice_start", "voice_followup", "none"]
VoiceNameKnownCustomerBehavior = Literal["trust_and_handoff", "confirm_in_downstream", "ask_on_start"]
VoiceNameUnknownCustomerBehavior = Literal["ask_on_start", "skip_to_downstream"]
VoiceNameMissingNameBehavior = Literal["ask_inline_once", "do_not_ask"]
VoiceNameUncertainNameBehavior = Literal["confirm_then_finalize", "finalize_immediately", "discard_and_collect"]


class VoiceNameCollectionLanguageTexts(BaseModel):
    start_prompt: str = Field(min_length=1)
    personalized_greeting_template: str = Field(min_length=1)
    confirmation_greeting_template: str = Field(min_length=1)
    inline_followup_prompt: str = Field(min_length=1)

    @field_validator(
        "start_prompt",
        "personalized_greeting_template",
        "confirmation_greeting_template",
        "inline_followup_prompt",
        mode="before",
    )
    @classmethod
    def _trim_required_text(cls, value: str) -> str:
        normalized = str(value or "").strip()
        if not normalized:
            raise ValueError("Text value is required")
        return normalized

    @model_validator(mode="after")
    def _validate_name_templates(self):
        if "{name}" not in self.personalized_greeting_template:
            raise ValueError("personalized_greeting_template must contain {name}")
        if "{name}" not in self.confirmation_greeting_template:
            raise ValueError("confirmation_greeting_template must contain {name}")
        return self


class VoiceNameCollectionStartConfig(BaseModel):
    ask_name_on_start: bool = True
    known_customer_behavior: VoiceNameKnownCustomerBehavior = "trust_and_handoff"
    unknown_customer_behavior: VoiceNameUnknownCustomerBehavior = "ask_on_start"


class VoiceNameCollectionDownstreamConfig(BaseModel):
    missing_name_behavior: VoiceNameMissingNameBehavior = "ask_inline_once"
    uncertain_name_behavior: VoiceNameUncertainNameBehavior = "confirm_then_finalize"
    finalize_on_explicit_name: bool = True
    finalize_on_confirmation: bool = True


class VoiceNameCollectionTextsConfig(BaseModel):
    ru: VoiceNameCollectionLanguageTexts
    kz: VoiceNameCollectionLanguageTexts


class VoiceNameCollectionConfig(BaseModel):
    enabled: bool = True
    start: VoiceNameCollectionStartConfig = Field(default_factory=VoiceNameCollectionStartConfig)
    downstream: VoiceNameCollectionDownstreamConfig = Field(default_factory=VoiceNameCollectionDownstreamConfig)
    texts: VoiceNameCollectionTextsConfig


class VoiceNameCollectionConfigOut(BaseModel):
    config: VoiceNameCollectionConfig
    updated_at: str | None = None
    source: Literal["defaults", "database"] = "defaults"


VoiceTTSProviderName = Literal["yandex", "elevenlabs", "openai"]
VoiceTTSLanguage = Literal["ru", "kz"]


class VoiceTTSLanguageConfig(BaseModel):
    model_config = ConfigDict(protected_namespaces=())

    voice: str | None = None
    model_id: str | None = None
    language_code: str | None = None

    @field_validator("voice", "model_id", "language_code", mode="before")
    @classmethod
    def _trim_optional_text(cls, value: str | None) -> str | None:
        if value is None:
            return None
        normalized = str(value).strip()
        return normalized or None


class VoiceTTSProviderConfig(BaseModel):
    ru: VoiceTTSLanguageConfig = Field(default_factory=VoiceTTSLanguageConfig)
    kz: VoiceTTSLanguageConfig = Field(default_factory=VoiceTTSLanguageConfig)


class VoiceTTSVoiceOption(BaseModel):
    value: str = Field(min_length=1)
    label: str = Field(min_length=1)

    @field_validator("value", "label", mode="before")
    @classmethod
    def _trim_required_option_text(cls, value: str) -> str:
        normalized = str(value or "").strip()
        if not normalized:
            raise ValueError("Option value is required")
        return normalized


class VoiceTTSConfig(BaseModel):
    provider: VoiceTTSProviderName = "yandex"
    yandex: VoiceTTSProviderConfig = Field(default_factory=VoiceTTSProviderConfig)
    elevenlabs: VoiceTTSProviderConfig = Field(default_factory=VoiceTTSProviderConfig)
    openai: VoiceTTSProviderConfig = Field(default_factory=VoiceTTSProviderConfig)


class VoiceTTSConfigOut(BaseModel):
    config: VoiceTTSConfig
    updated_at: str | None = None
    source: Literal["defaults", "database"] = "defaults"
    provider_options: list[VoiceTTSProviderName] = Field(default_factory=list)
    voice_options: dict[str, dict[str, list[VoiceTTSVoiceOption]]] = Field(default_factory=dict)


class AIOperatorConfig(BaseModel):
    agent_name: str = Field(default="Айнур", min_length=1)
    company_name: str = Field(default="DigiOps", min_length=1)
    base_system_prompt: str = Field(min_length=1)
    identity_reply_ru: str = Field(min_length=1)
    identity_reply_kz: str = Field(min_length=1)
    voice_greeting_ru: str = Field(min_length=1)
    voice_greeting_kz: str = Field(min_length=1)

    @field_validator(
        "agent_name",
        "company_name",
        "base_system_prompt",
        "identity_reply_ru",
        "identity_reply_kz",
        "voice_greeting_ru",
        "voice_greeting_kz",
        mode="before",
    )
    @classmethod
    def _trim_required_text(cls, value: str) -> str:
        normalized = str(value or "").strip()
        if not normalized:
            raise ValueError("Text value is required")
        return normalized


class AIOperatorConfigOut(BaseModel):
    config: AIOperatorConfig
    updated_at: str | None = None
    source: Literal["defaults", "database"] = "defaults"


class HealthResponse(BaseModel):
    status: str
    service: str
    version: str = "v1"


class LoginRequest(BaseModel):
    username: str
    password: str


class LoginResponse(BaseModel):
    access_token: str
    token_type: str = "bearer"
    role: Role
    auth_source: str | None = None
    provider: str | None = None
    full_name: str | None = None


class UserCreate(BaseModel):
    username: str = Field(min_length=3)
    password: str = Field(min_length=6)
    full_name: str = Field(min_length=2)
    role: Role


class UserUpdate(BaseModel):
    password: str | None = Field(default=None, min_length=6)
    full_name: str | None = Field(default=None, min_length=2)
    role: Role | None = None


class UserOut(BaseModel):
    user_id: str
    username: str
    full_name: str
    role: Role


class AuditEventIn(BaseModel):
    actor: str
    action: str
    entity: str
    metadata: dict = Field(default_factory=dict)


class AuditEvent(AuditEventIn):
    event_id: str
    created_at: str


class CustomerCreate(BaseModel):
    display_name: str = Field(min_length=2)
    phones: list[str] = Field(default_factory=list)
    preferred_phone: str | None = None
    tags: list[str] = Field(default_factory=list)


class CustomerNameUpdateIn(BaseModel):
    display_name: str = Field(min_length=2)
    source: str | None = None


class CustomerOut(CustomerCreate):
    customer_id: str
    created_at: str


class CustomerHistoryEventOut(BaseModel):
    timestamp: str
    kind: str
    title: str
    body: str
    note: str = ""
    interaction_id: str | None = None
    thread_id: str | None = None
    call_id: str | None = None


class CustomerHistorySummaryOut(BaseModel):
    contact_points: int = 0
    open_cases: int = 0
    active_channels: list[str] = Field(default_factory=list)
    latest_event_at: str | None = None
    latest_event_title: str | None = None
    primary_phone: str | None = None
    primary_telegram_thread_id: str | None = None


class CustomerHistoryOut(BaseModel):
    customer: CustomerOut
    summary: CustomerHistorySummaryOut
    interactions: list["InteractionOut"] = Field(default_factory=list)
    telegram_threads: list["TelegramThreadOut"] = Field(default_factory=list)
    live_calls: list["VoiceLiveCallOut"] = Field(default_factory=list)
    history: list[CustomerHistoryEventOut] = Field(default_factory=list)


class InteractionCreate(BaseModel):
    channel: Channel
    subject: str = Field(min_length=3)
    customer_id: str | None = None
    queue_id: str | None = None
    priority: int = Field(default=3, ge=1, le=5)


class InteractionOut(InteractionCreate):
    interaction_id: str
    status: InteractionStatus
    assigned_to: str | None = None
    created_at: str
    updated_at: str


class InteractionDrilldownFiltersOut(BaseModel):
    from_ts: str
    to_ts: str
    queue_id: str | None = None
    channel: str | None = None
    agent_id: str | None = None
    status: str | None = None
    q: str | None = None
    sort_by: str | None = None
    sort_dir: str | None = None


class InteractionDrilldownOut(BaseModel):
    items: list[InteractionOut] = Field(default_factory=list)
    total: int
    limit: int
    offset: int
    filters: InteractionDrilldownFiltersOut


class AssignRequest(BaseModel):
    assignee: str


class StatusRequest(BaseModel):
    status: InteractionStatus
    resolved_first_contact: bool | None = None


class EscalateRequest(BaseModel):
    target_queue_id: str


class InteractionTimelineAppendIn(BaseModel):
    action: str = Field(min_length=1)
    metadata: dict = Field(default_factory=dict)


class QueueRule(BaseModel):
    channel: Channel
    priority: int = Field(default=3, ge=1, le=5)
    strategy: Literal["round_robin", "least_loaded", "skill_based"] = "round_robin"
    sla_seconds: int = Field(default=30, ge=5, le=3600)


class QueueCreate(BaseModel):
    name: str
    description: str = ""
    rules: list[QueueRule] = Field(default_factory=list)


class QueueOut(QueueCreate):
    queue_id: str
    created_at: str


class VoiceEventIn(BaseModel):
    event_type: Literal[
        "call.started",
        "ivr.completed",
        "call.ended",
        "recording.ready",
        "call.connected",
        "call.transferred",
    ]
    call_id: str
    interaction_id: str | None = None
    source_event_id: str | None = None
    payload: dict = Field(default_factory=dict)


class VoiceEventOut(VoiceEventIn):
    event_id: str
    created_at: str


class RecordingRegisterIn(BaseModel):
    call_id: str
    interaction_id: str | None = None
    source_path: str
    file_name: str | None = None
    mime_type: str | None = None
    duration_seconds: int | None = Field(default=None, ge=0)
    recorded_at: str | None = None
    source_event_id: str | None = None


class RecordingOut(BaseModel):
    recording_id: str
    channel: Literal["voice"]
    call_id: str
    interaction_id: str | None = None
    source_event_id: str | None = None
    file_name: str
    mime_type: str
    size_bytes: int
    duration_seconds: int | None = None
    storage_backend: Literal["local_fs"]
    status: RecordingStatus
    recorded_at: str
    created_at: str
    updated_at: str
    archived_at: str | None = None


class AsteriskBridgeStatusOut(BaseModel):
    status: str
    ami_connected: bool
    ami_host: str | None = None
    last_event_at: str | None = None
    queue_codes_loaded: list[str] = Field(default_factory=list)
    sftp_enabled: bool = False
    bridge_auth_mode: str | None = None
    callcontrol_enabled: bool | None = None
    webrtc_enabled: bool = False
    webrtc_ws_url: str | None = None


class AsteriskEventOut(BaseModel):
    bridge_event_id: str
    ami_event_name: str
    call_id: str
    linked_id: str | None = None
    interaction_id: str | None = None
    recording_id: str | None = None
    forward_status: AsteriskForwardStatus
    payload: dict = Field(default_factory=dict)
    last_error: str | None = None
    created_at: str
    updated_at: str


class VoiceLiveCallOut(BaseModel):
    call_id: str
    interaction_id: str
    queue_id: str
    queue_code: str
    caller_number: str | None = None
    caller_name: str | None = None
    status: str
    telephony_status: TelephonyStatus
    claimed_by_user: str | None = None
    claimed_at: str | None = None
    operator_extension: str | None = None
    channel_name: str | None = None
    started_at: str
    connected_at: str | None = None
    ended_at: str | None = None
    updated_at: str
    last_transition_at: str | None = None
    hangup_cause: str | None = None
    terminal_action: str | None = None
    terminal_target: str | None = None
    voice_session_id: str | None = None
    ai_session_id: str | None = None
    ai_state: VoiceAIState | None = None
    ai_handoff_reason: str | None = None
    ai_last_model_at: str | None = None
    has_recording: bool = False


class VoiceCallClaimIn(BaseModel):
    operator_extension: str | None = None


class VoiceCallBlindTransferIn(BaseModel):
    target_type: Literal["extension", "queue_code"] = "extension"
    target_value: str = Field(min_length=1)


class VoiceCallActionOut(BaseModel):
    action_id: str
    call_id: str
    interaction_id: str | None = None
    action_type: str
    actor_user: str
    actor_role: str
    request: dict = Field(default_factory=dict)
    result_status: CallActionResultStatus
    ami_action_id: str | None = None
    error: str | None = None
    created_at: str


class VoiceAISummaryOut(BaseModel):
    call_id: str
    session_id: str | None = None
    voice_session_id: str | None = None
    status_label: str
    status_tone: Literal["answered", "handoff"]
    customer_name_status: VoiceStartNameStatus | None = None
    customer_name_value: str | None = None
    customer_name_source: VoiceStartNameSource | None = None
    voice_start_language: str | None = None
    customer_request_text: str
    ai_outcome_text: str
    handoff_reason: str
    recommended_next_step: str
    generated_at: str
    transcript_segments: list["VoiceAISummaryTranscriptSegmentOut"] = Field(default_factory=list)


class VoiceAISummaryTranscriptSegmentOut(BaseModel):
    speaker: Literal["caller", "assistant"]
    text: str
    sequence_no: int
    source_type: str
    created_at: str
    interrupted: bool = False


class BrowserSoftphoneConfigOut(BaseModel):
    enabled: bool = False
    ws_url: str | None = None
    sip_uri: str | None = None
    authorization_username: str | None = None
    password: str | None = None
    display_name: str | None = None
    ice_servers: list[dict] = Field(default_factory=list)
    operator_extension: str | None = None


class IvrFlowCreate(BaseModel):
    name: str = Field(min_length=2)
    description: str = ""
    queue_id: str
    entry_node_id: str
    flow_json: dict = Field(default_factory=dict)
    is_active: bool = True


class IvrFlowUpdate(BaseModel):
    name: str | None = Field(default=None, min_length=2)
    description: str | None = None
    entry_node_id: str | None = None
    flow_json: dict | None = None
    is_active: bool | None = None


class IvrFlowOut(BaseModel):
    flow_id: str
    name: str
    description: str
    channel: Literal["voice"]
    queue_id: str
    version: int
    is_active: bool
    entry_node_id: str
    flow_json: dict
    created_at: str
    updated_at: str


class IvrSessionStartIn(BaseModel):
    call_id: str
    queue_id: str
    interaction_id: str | None = None


class IvrDtmfIn(BaseModel):
    digit: str = Field(min_length=1, max_length=1)


class IvrSessionOut(BaseModel):
    session_id: str
    call_id: str
    interaction_id: str | None = None
    flow_id: str
    queue_id: str
    current_node_id: str
    entered_digits: list[str] = Field(default_factory=list)
    status: IvrSessionStatus
    outcome_code: str | None = None
    resolved_queue_id: str | None = None
    resolved_queue_code: str | None = None
    completed_at: str | None = None
    created_at: str
    updated_at: str


class EventEnvelope(BaseModel):
    event_id: str
    event_type: str
    event_version: int = 1
    occurred_at: str
    producer: str
    entity_type: str
    entity_id: str
    correlation_id: str | None = None
    routing_key: str
    payload: dict = Field(default_factory=dict)


class EventOutboxItem(BaseModel):
    event_id: str
    event_type: str
    event_version: int
    producer_service: str
    entity_type: str
    entity_id: str
    correlation_id: str | None = None
    routing_key: str
    payload: dict = Field(default_factory=dict)
    status: EventOutboxStatus
    attempt_count: int
    last_error: str | None = None
    available_at: str
    published_at: str | None = None
    created_at: str
    updated_at: str


class TelegramWebhookIn(BaseModel):
    chat_id: str
    text: str
    customer_external_id: str | None = None
    payload: dict = Field(default_factory=dict)


class TelegramWebhookOut(TelegramWebhookIn):
    message_id: str
    thread_id: str | None = None
    interaction_id: str | None = None
    direction: Literal["inbound", "outbound", "system"] = "inbound"
    created_at: str


class TelegramThreadOut(BaseModel):
    thread_id: str
    chat_id: str
    interaction_id: str
    telegram_user_id: str | None = None
    username: str | None = None
    display_name: str | None = None
    queue_id: str | None = None
    status: InteractionStatus
    claimed_by_user: str | None = None
    claimed_at: str | None = None
    ai_session_id: str | None = None
    ai_state: AIThreadState | None = None
    ai_handoff_reason: str | None = None
    ai_last_model_at: str | None = None
    last_message_at: str
    last_message_preview: str
    created_at: str
    updated_at: str


class TelegramThreadMessageOut(BaseModel):
    message_id: str
    thread_id: str
    interaction_id: str
    chat_id: str
    direction: Literal["inbound", "outbound", "system"]
    text: str
    telegram_message_id_external: str | None = None
    operator_user: str | None = None
    author_type: Literal["customer", "human", "ai", "system"] = "customer"
    author_id: str | None = None
    delivery_status: str | None = None
    payload: dict = Field(default_factory=dict)
    created_at: str


class TelegramThreadAISummaryOut(BaseModel):
    thread_id: str
    session_id: str
    status_label: str
    status_tone: Literal["answered", "handoff"]
    customer_request_text: str
    ai_outcome_text: str
    handoff_reason: str
    recommended_next_step: str
    generated_at: str


class TelegramThreadReplyIn(BaseModel):
    text: str = Field(min_length=1)


class TelegramThreadEscalateIn(BaseModel):
    target_queue_id: str = Field(min_length=1)


class AITelegramEnqueueIn(BaseModel):
    trigger_message_id: str | None = None


class AITelegramPauseIn(BaseModel):
    reason: str = Field(min_length=1)
    actor_user: str | None = None


class VoiceAISessionCreateIn(BaseModel):
    call_id: str = Field(min_length=1)
    linked_id: str | None = None
    interaction_id: str = Field(min_length=1)
    queue_id: str = Field(min_length=1)
    caller_number: str | None = None
    caller_name: str | None = None
    agent_profile: str = "voice_support"
    language_hint: str | None = None
    handoff_queue_id: str | None = None
    metadata: dict = Field(default_factory=dict)


class VoiceAISessionOut(BaseModel):
    voice_session_id: str
    ai_session_id: str | None = None
    status: VoiceAIState


class VoiceAIMediaBridgeEventIn(BaseModel):
    event_type: Literal["requested", "ended"]
    media_uuid: str = Field(min_length=1)
    call_id: str = Field(min_length=1)
    linked_id: str | None = None
    channel: str | None = None
    service_address: str | None = None
    reason: str | None = None


class VoiceAITelephonyEventIn(BaseModel):
    event_type: Literal["call.connected", "call.ended", "recording.ready", "operator.connected"]
    payload: dict = Field(default_factory=dict)


class VoiceAITurnIn(BaseModel):
    voice_session_id: str = Field(min_length=1)
    call_id: str = Field(min_length=1)
    interaction_id: str = Field(min_length=1)
    transcript_text: str = Field(min_length=1)
    language: str | None = None
    sequence_no: int = Field(default=1, ge=1)
    barge_in: bool = False
    metadata: dict = Field(default_factory=dict)


class VoiceAITurnDecisionOut(BaseModel):
    language: str
    intent: str
    reply_text: str
    confidence: float
    needs_handoff: bool
    handoff_reason: str | None = None
    case_action: Literal["none", "close", "escalate", "keep_open"] = "keep_open"
    kb_refs: list[str] = Field(default_factory=list)
    summary_text: str = ""
    model: str | None = None
    latency_ms: int | None = None
    status: VoiceAIState = "active"
    metadata: dict = Field(default_factory=dict)


class VoiceStartResult(BaseModel):
    language: str
    customer_id: str | None = None
    customer_name_status: VoiceStartNameStatus = "name_not_obtained"
    customer_name_value: str | None = None
    customer_name_source: VoiceStartNameSource = "none"
    downstream_queue_id: str | None = None
    downstream_queue_code: str | None = None
    resolved_at: str | None = None


class VoiceAIStartIn(BaseModel):
    voice_session_id: str = Field(min_length=1)
    call_id: str = Field(min_length=1)
    interaction_id: str = Field(min_length=1)
    customer_id: str | None = None
    language_hint: str | None = None
    agent_profile: str = "voice_support"
    metadata: dict = Field(default_factory=dict)


class VoiceAIStartOut(BaseModel):
    session_id: str
    language: str
    greeting_text: str
    disclosure_required: bool = True
    needs_handoff: bool = False
    handoff_reason: str | None = None
    summary_text: str = ""
    start_result: VoiceStartResult | None = None
    metadata: dict = Field(default_factory=dict)


class VoiceAIHandoffRequestIn(BaseModel):
    voice_session_id: str = Field(min_length=1)
    ai_session_id: str | None = None
    interaction_id: str = Field(min_length=1)
    target_queue_id: str | None = None
    reason: str = Field(min_length=1)
    summary: dict = Field(default_factory=dict)
    metadata: dict = Field(default_factory=dict)


class VoiceAICallStateUpdateIn(BaseModel):
    voice_session_id: str | None = None
    ai_session_id: str | None = None
    ai_state: VoiceAIState
    handoff_reason: str | None = None
    metadata: dict = Field(default_factory=dict)


class TelegramThreadAIReplyIn(BaseModel):
    text: str = Field(min_length=1)
    agent_profile: str = "telegram_support"
    model: str | None = None
    trigger_message_id: str | None = None
    language: str | None = None
    confidence: float | None = None
    kb_refs: list[str] = Field(default_factory=list)
    payload: dict = Field(default_factory=dict)


class TelegramThreadAIHandoffIn(BaseModel):
    reason: str = Field(min_length=1)
    agent_profile: str = "telegram_support"
    trigger_message_id: str | None = None
    confidence: float | None = None
    payload: dict = Field(default_factory=dict)


class WhatsAppWebhookIn(BaseModel):
    chat_id: str
    text: str = ""
    external_message_id: str | None = None
    whatsapp_user_id: str | None = None
    phone_number: str | None = None
    display_name: str | None = None
    customer_id: str | None = None
    external_subject: str | None = None
    queue_id: str | None = None
    is_group: bool = False
    payload: dict = Field(default_factory=dict)


class WhatsAppWebhookOut(BaseModel):
    message_id: str
    chat_id: str
    text: str
    external_message_id: str | None = None
    customer_id: str | None = None
    payload: dict = Field(default_factory=dict)
    thread_id: str | None = None
    interaction_id: str | None = None
    direction: Literal["inbound", "outbound", "system"] = "inbound"
    created_at: str


class WhatsAppThreadOut(BaseModel):
    thread_id: str
    chat_id: str
    interaction_id: str
    whatsapp_user_id: str | None = None
    phone_number: str | None = None
    display_name: str | None = None
    queue_id: str | None = None
    is_group: bool = False
    status: InteractionStatus
    claimed_by_user: str | None = None
    claimed_at: str | None = None
    ai_session_id: str | None = None
    ai_state: AIThreadState | None = None
    ai_handoff_reason: str | None = None
    ai_last_model_at: str | None = None
    unread_count: int = 0
    last_message_at: str
    last_message_preview: str
    created_at: str
    updated_at: str


class WhatsAppThreadMessageOut(BaseModel):
    message_id: str
    thread_id: str
    interaction_id: str
    chat_id: str
    direction: Literal["inbound", "outbound", "system"]
    text: str
    whatsapp_message_id_external: str | None = None
    operator_user: str | None = None
    author_type: Literal["customer", "human", "ai", "system"] = "customer"
    author_id: str | None = None
    customer_id: str | None = None
    delivery_status: str | None = None
    payload: dict = Field(default_factory=dict)
    created_at: str


class WhatsAppThreadAISummaryOut(BaseModel):
    thread_id: str
    session_id: str
    status_label: str
    status_tone: Literal["answered", "handoff"]
    customer_request_text: str
    ai_outcome_text: str
    handoff_reason: str
    recommended_next_step: str
    generated_at: str


class WhatsAppThreadReplyIn(BaseModel):
    text: str = Field(min_length=1)


class WhatsAppThreadEscalateIn(BaseModel):
    target_queue_id: str = Field(min_length=1)


class AIWhatsAppEnqueueIn(BaseModel):
    trigger_message_id: str | None = None


class AIWhatsAppPauseIn(BaseModel):
    reason: str = Field(min_length=1)
    actor_user: str | None = None


class WhatsAppThreadAIReplyIn(BaseModel):
    text: str = Field(min_length=1)
    agent_profile: str = "whatsapp_support"
    model: str | None = None
    trigger_message_id: str | None = None
    language: str | None = None
    confidence: float | None = None
    kb_refs: list[str] = Field(default_factory=list)
    payload: dict = Field(default_factory=dict)


class WhatsAppThreadAIHandoffIn(BaseModel):
    reason: str = Field(min_length=1)
    agent_profile: str = "whatsapp_support"
    trigger_message_id: str | None = None
    confidence: float | None = None
    payload: dict = Field(default_factory=dict)


class WebchatMessageIn(BaseModel):
    session_id: str
    text: str = Field(min_length=1)
    visitor_name: str | None = None
    customer_external_id: str | None = None
    queue_id: str | None = None
    priority: int = Field(default=3, ge=1, le=5)
    payload: dict = Field(default_factory=dict)


class WebchatMessageOut(WebchatMessageIn):
    message_id: str
    interaction_id: str | None = None
    created_at: str


class EmailMessageIn(BaseModel):
    from_email: str
    subject: str = Field(min_length=3)
    body: str = Field(min_length=1)
    customer_external_id: str | None = None
    queue_id: str | None = None
    priority: int = Field(default=3, ge=1, le=5)
    payload: dict = Field(default_factory=dict)


class EmailMessageOut(EmailMessageIn):
    message_id: str
    interaction_id: str | None = None
    created_at: str


class KBCategoryCreate(BaseModel):
    name: str
    description: str = ""


class KBCategoryOut(KBCategoryCreate):
    category_id: str
    created_at: str


class KBArticleCreate(BaseModel):
    category_id: str
    article_group_id: str | None = None
    language: str = "ru"
    title: str
    body: str
    tags: list[str] = Field(default_factory=list)


class KBArticleUpdate(BaseModel):
    article_group_id: str | None = None
    language: str | None = None
    title: str | None = None
    body: str | None = None
    tags: list[str] | None = None


class KBArticleOut(KBArticleCreate):
    article_id: str
    created_at: str
    updated_at: str


class KpiEventIn(BaseModel):
    queue_id: str
    channel: Channel = "voice"
    agent_id: str | None = None
    answered: bool
    wait_seconds: int = Field(ge=0)
    handle_seconds: int = Field(ge=0)
    abandoned: bool = False
    resolved_first_contact: bool = False
    created_at: str | None = None


class ReportingInteractionFactIn(BaseModel):
    interaction_id: str = Field(min_length=1)
    channel: Channel | None = None
    queue_id: str | None = None
    agent_id: str | None = None
    status: InteractionStatus | None = None
    created_at: str | None = None
    closed_at: str | None = None
    answered: bool | None = None
    abandoned: bool | None = None
    wait_seconds: int | None = Field(default=None, ge=0)
    handle_seconds: int | None = Field(default=None, ge=0)
    resolved_first_contact: bool | None = None
    source: str = Field(min_length=1)


class ReportingMetricCoverageOut(BaseModel):
    status: Literal["exact", "partial", "unavailable"] = "unavailable"
    supported_channels: list[str] = Field(default_factory=list)
    exact_rows: int = 0
    total_rows: int = 0
    note: str | None = None


class ReportingKpiCoverageOut(BaseModel):
    metric_status: dict[str, Literal["exact", "partial", "unavailable"]] = Field(default_factory=dict)
    supported_channels: dict[str, list[str]] = Field(default_factory=dict)
    exact_rows: int = 0
    total_rows: int = 0
    note: str | None = None
    metric_details: dict[str, ReportingMetricCoverageOut] = Field(default_factory=dict)


class ReportingDrilldownFiltersOut(BaseModel):
    from_ts: str
    to_ts: str
    metric: str
    queue_id: str | None = None
    channel: str | None = None
    sl_threshold_seconds: int = 30


class ReportingDrilldownItemOut(InteractionOut):
    answered: bool | None = None
    abandoned: bool | None = None
    wait_seconds: int | None = None
    handle_seconds: int | None = None
    within_sla: bool | None = None
    resolved_first_contact: bool | None = None


class ReportingDrilldownOut(BaseModel):
    items: list[ReportingDrilldownItemOut] = Field(default_factory=list)
    total: int
    limit: int
    offset: int
    metric: str
    coverage: ReportingMetricCoverageOut = Field(default_factory=ReportingMetricCoverageOut)
    filters: ReportingDrilldownFiltersOut


class ReportingTimeseriesFiltersOut(BaseModel):
    from_ts: str
    to_ts: str
    metric: str
    interval: Literal["hour", "day"] = "day"
    queue_id: str | None = None
    channel: str | None = None
    sl_threshold_seconds: int = 30


class ReportingTimeseriesPointOut(BaseModel):
    ts: str
    value: float = 0.0
    sample_size: int = 0


class ReportingTimeseriesOut(BaseModel):
    metric: str
    interval: Literal["hour", "day"] = "day"
    filters: ReportingTimeseriesFiltersOut
    points: list[ReportingTimeseriesPointOut] = Field(default_factory=list)


class ReportingAgentAnalyticsFiltersOut(BaseModel):
    from_ts: str
    to_ts: str
    queue_id: str | None = None
    channel: str | None = None
    sort_by: Literal[
        "interactions_total",
        "answered_total",
        "avg_handle_seconds",
        "fcr_rate",
        "last_activity_at",
        "agent_id",
    ] = "interactions_total"
    sort_dir: Literal["asc", "desc"] = "desc"
    limit: int = 25


class ReportingAgentAnalyticsTotalsOut(BaseModel):
    agents_total: int = 0
    agents_with_activity: int = 0
    interactions_total: int = 0
    answered_total: int = 0
    closed_total: int = 0
    ready_now: int = 0
    busy_now: int = 0
    break_now: int = 0
    offline_now: int = 0
    avg_interactions_per_agent: float = 0.0
    avg_handle_seconds: float | None = None
    avg_fcr_rate: float | None = None


class ReportingAgentStateSnapshotOut(BaseModel):
    by_state: dict[str, int] = Field(default_factory=dict)
    updated_at: str | None = None


class ReportingAgentAnalyticsRowOut(BaseModel):
    agent_id: str
    current_state: str | None = None
    current_queue_id: str | None = None
    current_state_updated_at: str | None = None
    dominant_queue_id: str | None = None
    last_activity_at: str | None = None
    interactions_total: int = 0
    answered_total: int = 0
    closed_total: int = 0
    abandoned_total: int = 0
    avg_wait_seconds: float | None = None
    avg_handle_seconds: float | None = None
    answer_rate: float = 0.0
    fcr_rate: float | None = None
    channels: list[str] = Field(default_factory=list)


class ReportingAgentAnalyticsTeamRowOut(BaseModel):
    team_key: str
    label: str
    agents_total: int = 0
    agents_with_activity: int = 0
    interactions_total: int = 0
    answered_total: int = 0
    avg_handle_seconds: float | None = None
    fcr_rate: float | None = None
    ready_now: int = 0
    busy_now: int = 0
    break_now: int = 0
    offline_now: int = 0


class ReportingAgentAnalyticsShiftRowOut(BaseModel):
    shift_key: Literal["night", "day", "evening"]
    label: str
    agents_with_activity: int = 0
    interactions_total: int = 0
    answered_total: int = 0
    avg_handle_seconds: float | None = None
    fcr_rate: float | None = None


class ReportingAgentAnalyticsBreakdownsOut(BaseModel):
    by_team: list[ReportingAgentAnalyticsTeamRowOut] = Field(default_factory=list)
    by_shift: list[ReportingAgentAnalyticsShiftRowOut] = Field(default_factory=list)


class ReportingAgentAnalyticsTrendFiltersOut(BaseModel):
    from_ts: str
    to_ts: str
    queue_id: str | None = None
    channel: str | None = None
    metric: Literal["agents_with_activity", "interactions_per_agent", "avg_handle_seconds", "fcr_rate"] = "interactions_per_agent"
    interval: Literal["hour", "day"] = "day"


class ReportingAgentAnalyticsTrendPointOut(BaseModel):
    ts: str
    value: float = 0.0
    agents_with_activity: int = 0
    interactions_total: int = 0


class ReportingAgentAnalyticsTimeseriesOut(BaseModel):
    metric: Literal["agents_with_activity", "interactions_per_agent", "avg_handle_seconds", "fcr_rate"] = "interactions_per_agent"
    interval: Literal["hour", "day"] = "day"
    filters: ReportingAgentAnalyticsTrendFiltersOut
    points: list[ReportingAgentAnalyticsTrendPointOut] = Field(default_factory=list)


class ReportingAgentAnalyticsOverviewOut(BaseModel):
    window: AIAnalyticsWindowOut
    filters: ReportingAgentAnalyticsFiltersOut
    totals: ReportingAgentAnalyticsTotalsOut = Field(default_factory=ReportingAgentAnalyticsTotalsOut)
    state_snapshot: ReportingAgentStateSnapshotOut = Field(default_factory=ReportingAgentStateSnapshotOut)
    breakdowns: ReportingAgentAnalyticsBreakdownsOut = Field(default_factory=ReportingAgentAnalyticsBreakdownsOut)
    items: list[ReportingAgentAnalyticsRowOut] = Field(default_factory=list)


class ReportingSavedViewSnapshot(BaseModel):
    preset: str = "7d"
    fromTs: str = ""
    toTs: str = ""
    queueId: str = "all"
    channel: str = "all"
    compareMode: str = "previous"
    trendMetric: str = "volume"
    voiceNameTrendMetric: str = "scenario_calls"
    aiTrendMetric: str = "containment_rate"
    agentTrendMetric: str = "interactions_per_agent"


class ReportingSavedViewIn(BaseModel):
    id: str | None = None
    name: str = Field(min_length=1, max_length=160)
    snapshot: ReportingSavedViewSnapshot = Field(default_factory=ReportingSavedViewSnapshot)


class ReportingSavedViewOut(BaseModel):
    id: str
    name: str
    snapshot: ReportingSavedViewSnapshot = Field(default_factory=ReportingSavedViewSnapshot)
    created_at: str
    updated_at: str


class AgentStateIn(BaseModel):
    agent_id: str
    state: Literal["READY", "BUSY", "BREAK", "OFFLINE"]
    queue_id: str | None = None


class AgentStateOut(AgentStateIn):
    updated_at: str


AgentLevel = Literal["L2", "L3"]
AgentStatus = Literal["OFFLINE", "AVAILABLE", "RESERVED", "RINGING", "TALKING", "AFTER_CALL_WORK", "PAUSED"]


class AgentCreate(BaseModel):
    tenant_ids: list[str] = Field(default_factory=list)
    extension: str = Field(min_length=1)
    endpoint: str | None = None
    display_name: str = Field(min_length=1)
    level: AgentLevel
    skills: list[str] = Field(default_factory=list)
    max_concurrent_calls: int = Field(default=1, ge=1)
    enabled: bool = True


class AgentStatusUpdateIn(BaseModel):
    status: AgentStatus


class AgentPoolOut(BaseModel):
    agent_id: str
    tenant_ids: list[str] = Field(default_factory=list)
    extension: str
    endpoint: str | None = None
    display_name: str
    level: AgentLevel
    skills: list[str] = Field(default_factory=list)
    status: AgentStatus
    current_call_id: str | None = None
    max_concurrent_calls: int
    enabled: bool
    calls_handled_count: int = 0
    created_at: str
    updated_at: str


class EscalationRequestIn(BaseModel):
    target_level: AgentLevel
    reason_code: str = Field(min_length=1)
    topic: str | None = None
    required_skills: list[str] = Field(default_factory=list)
    priority: int = Field(default=3, ge=1, le=5)
    summary: str | None = None


class EscalationOut(BaseModel):
    escalation_id: str
    call_id: str
    tenant_id: str | None = None
    from_level: str
    to_level: str
    reason_code: str
    required_skills: list[str] = Field(default_factory=list)
    priority: int
    topic: str | None = None
    summary: str | None = None
    status: str
    assigned_agent_id: str | None = None
    requested_at: str
    connected_at: str | None = None
    completed_at: str | None = None


class RoutingAgentReserveIn(BaseModel):
    call_id: str = Field(min_length=1)
    level: AgentLevel
    tenant_id: str | None = None
    required_skills: list[str] = Field(default_factory=list)
    exclude_agent_ids: list[str] = Field(default_factory=list)


class RoutingAgentReserveOut(BaseModel):
    agent_id: str
    extension: str
    endpoint: str | None = None
    display_name: str


class AIAnalyticsWindowOut(BaseModel):
    from_ts: str
    to_ts: str


class AIAnalyticsFiltersOut(AIAnalyticsWindowOut):
    queue_id: str | None = None
    channel: str | None = None


class AIAnalyticsTotalsOut(BaseModel):
    sessions_started: int = 0
    sessions_contained: int = 0
    sessions_handoff: int = 0
    sessions_closed: int = 0
    sessions_closed_without_operator: int = 0
    assistant_turns: int = 0


class AIAnalyticsMetricsOut(BaseModel):
    containment_rate: float = 0.0
    handoff_rate: float = 0.0
    ai_latency_avg_ms: float | None = None
    ai_latency_p95_ms: float | None = None
    closed_without_operator_rate: float = 0.0
    human_touched_rate: float = 0.0


class AIAnalyticsChannelBreakdownOut(BaseModel):
    channel: str
    sessions_started: int = 0
    sessions_contained: int = 0
    sessions_handoff: int = 0
    sessions_closed_without_operator: int = 0
    assistant_turns: int = 0
    containment_rate: float = 0.0
    handoff_rate: float = 0.0
    closed_without_operator_rate: float = 0.0
    ai_latency_avg_ms: float | None = None
    ai_only_sessions: int = 0
    human_touched_sessions: int = 0


class AIAnalyticsOutcomeBreakdownOut(BaseModel):
    outcome: Literal["contained", "handoff", "human_touched", "closed_without_operator", "active", "error"]
    label: str
    sessions: int = 0
    share: float = 0.0


class AIAnalyticsHandoffReasonBreakdownOut(BaseModel):
    reason_key: str
    label: str
    sessions: int = 0
    share: float = 0.0


class AIAnalyticsBreakdownsOut(BaseModel):
    by_channel: list[AIAnalyticsChannelBreakdownOut] = Field(default_factory=list)
    by_outcome: list[AIAnalyticsOutcomeBreakdownOut] = Field(default_factory=list)
    by_handoff_reason: list[AIAnalyticsHandoffReasonBreakdownOut] = Field(default_factory=list)


class AIAnalyticsCoverageOut(BaseModel):
    sessions_with_interaction_id: int = 0
    sessions_with_queue_id: int = 0
    sessions_with_latency_turns: int = 0
    sessions_with_terminal_state: int = 0
    sessions_with_handoff_reason: int = 0


class AIAnalyticsOverviewOut(BaseModel):
    window: AIAnalyticsWindowOut
    filters: AIAnalyticsFiltersOut
    totals: AIAnalyticsTotalsOut
    metrics: AIAnalyticsMetricsOut
    breakdowns: AIAnalyticsBreakdownsOut
    coverage: AIAnalyticsCoverageOut


class AIAnalyticsTimeseriesPointOut(BaseModel):
    ts: str
    value: float | None = None
    sessions: int = 0
    assistant_turns: int = 0


class AIAnalyticsTimeseriesOut(BaseModel):
    metric: Literal["containment_rate", "handoff_rate", "ai_latency_avg_ms", "closed_without_operator_rate", "human_touched_rate"]
    interval: Literal["hour", "day"]
    filters: AIAnalyticsFiltersOut
    points: list[AIAnalyticsTimeseriesPointOut] = Field(default_factory=list)


class VoiceNameFlowAnalyticsFiltersOut(AIAnalyticsWindowOut):
    queue_id: str | None = None
    language: str | None = None


class VoiceNameFlowAnalyticsTotalsOut(BaseModel):
    scenario_calls: int = 0
    start_obtained: int = 0
    downstream_ai_obtained: int = 0
    followup_required: int = 0
    name_not_obtained: int = 0
    manual_corrected: int = 0
    handoff_confirmed_name: int = 0
    handoff_unconfirmed_name: int = 0
    needed_downstream: int = 0


class VoiceNameFlowAnalyticsMetricsOut(BaseModel):
    start_capture_rate: float = 0.0
    downstream_rescue_rate: float = 0.0
    handoff_unconfirmed_rate: float = 0.0
    manual_correction_rate: float = 0.0


class VoiceNameFlowAnalyticsFunnelStageOut(BaseModel):
    stage: Literal[
        "scenario_calls",
        "start_obtained",
        "needed_downstream",
        "downstream_ai_obtained",
        "handoff_confirmed_name",
        "handoff_unconfirmed_name",
    ]
    label: str
    sessions: int = 0
    share: float = 0.0


class VoiceNameFlowAnalyticsLanguageBreakdownOut(BaseModel):
    language: str
    scenario_calls: int = 0
    start_obtained: int = 0
    downstream_ai_obtained: int = 0
    followup_required: int = 0
    name_not_obtained: int = 0
    manual_corrected: int = 0
    handoff_confirmed_name: int = 0
    handoff_unconfirmed_name: int = 0
    start_capture_rate: float = 0.0
    downstream_rescue_rate: float = 0.0
    handoff_unconfirmed_rate: float = 0.0
    manual_correction_rate: float = 0.0


class VoiceNameFlowAnalyticsQueueBreakdownOut(BaseModel):
    queue_id: str
    scenario_calls: int = 0
    start_obtained: int = 0
    downstream_ai_obtained: int = 0
    followup_required: int = 0
    name_not_obtained: int = 0
    manual_corrected: int = 0
    handoff_confirmed_name: int = 0
    handoff_unconfirmed_name: int = 0
    start_capture_rate: float = 0.0
    downstream_rescue_rate: float = 0.0
    handoff_unconfirmed_rate: float = 0.0
    manual_correction_rate: float = 0.0


class VoiceNameFlowAnalyticsHandoffBreakdownOut(BaseModel):
    outcome: Literal["confirmed_name", "unconfirmed_name"]
    label: str
    sessions: int = 0
    share: float = 0.0


class VoiceNameFlowAnalyticsBreakdownsOut(BaseModel):
    funnel: list[VoiceNameFlowAnalyticsFunnelStageOut] = Field(default_factory=list)
    by_language: list[VoiceNameFlowAnalyticsLanguageBreakdownOut] = Field(default_factory=list)
    by_queue: list[VoiceNameFlowAnalyticsQueueBreakdownOut] = Field(default_factory=list)
    handoff: list[VoiceNameFlowAnalyticsHandoffBreakdownOut] = Field(default_factory=list)


class VoiceNameFlowAnalyticsCoverageOut(BaseModel):
    sessions_with_start_decision: int = 0
    sessions_with_final_ai_state: int = 0
    sessions_with_manual_overlay: int = 0
    note: str | None = None


class VoiceNameFlowAnalyticsOverviewOut(BaseModel):
    window: AIAnalyticsWindowOut
    filters: VoiceNameFlowAnalyticsFiltersOut
    totals: VoiceNameFlowAnalyticsTotalsOut = Field(default_factory=VoiceNameFlowAnalyticsTotalsOut)
    metrics: VoiceNameFlowAnalyticsMetricsOut = Field(default_factory=VoiceNameFlowAnalyticsMetricsOut)
    breakdowns: VoiceNameFlowAnalyticsBreakdownsOut = Field(default_factory=VoiceNameFlowAnalyticsBreakdownsOut)
    coverage: VoiceNameFlowAnalyticsCoverageOut = Field(default_factory=VoiceNameFlowAnalyticsCoverageOut)


class VoiceNameFlowAnalyticsTimeseriesPointOut(BaseModel):
    ts: str
    value: float | None = None
    scenario_calls: int = 0
    denominator: int = 0


class VoiceNameFlowAnalyticsTimeseriesOut(BaseModel):
    metric: Literal[
        "scenario_calls",
        "start_capture_rate",
        "downstream_rescue_rate",
        "handoff_unconfirmed_rate",
        "manual_correction_rate",
    ]
    interval: Literal["hour", "day"]
    filters: VoiceNameFlowAnalyticsFiltersOut
    points: list[VoiceNameFlowAnalyticsTimeseriesPointOut] = Field(default_factory=list)


class AIAnalyticsDrilldownFiltersOut(AIAnalyticsFiltersOut):
    slice: Literal["all", "contained", "handoff", "human_touched", "closed_without_operator", "active", "error"] = "all"
    reason_key: str | None = None
    status: str | None = None
    q: str | None = None
    sort_by: Literal["created_at", "updated_at", "ai_latency_avg_ms", "status"] = "created_at"
    sort_dir: Literal["asc", "desc"] = "desc"


class AIAnalyticsDrilldownItemOut(BaseModel):
    session_id: str
    thread_id: str | None = None
    interaction_id: str | None = None
    channel: str
    queue_id: str | None = None
    status: str
    created_at: str
    updated_at: str
    closed_at: str | None = None
    contained: bool = False
    handoff: bool = False
    human_touched: bool = False
    closed_without_operator: bool = False
    reason_key: str | None = None
    reason_label: str | None = None
    raw_handoff_reason: str | None = None
    assigned_to: str | None = None
    claimed_by_user: str | None = None
    assistant_turns: int = 0
    user_turns: int = 0
    tool_turns: int = 0
    ai_latency_avg_ms: float | None = None
    ai_latency_p95_ms: float | None = None


class AIAnalyticsDrilldownOut(BaseModel):
    items: list[AIAnalyticsDrilldownItemOut] = Field(default_factory=list)
    total: int
    limit: int
    offset: int
    filters: AIAnalyticsDrilldownFiltersOut
    coverage: AIAnalyticsCoverageOut = Field(default_factory=AIAnalyticsCoverageOut)


class AIAnalyticsSessionLinkedInteractionOut(BaseModel):
    interaction_id: str | None = None
    channel: str | None = None
    queue_id: str | None = None
    status: str | None = None
    assigned_to: str | None = None
    subject: str | None = None
    created_at: str | None = None
    updated_at: str | None = None


class AIAnalyticsSessionEventOut(BaseModel):
    ts: str
    event_type: str
    label: str
    role: str | None = None
    source_type: str | None = None
    latency_ms: int | None = None
    finish_reason: str | None = None
    status: str | None = None
    metadata: dict = Field(default_factory=dict)


class AIAnalyticsSessionDetailOut(BaseModel):
    session: AIAnalyticsDrilldownItemOut
    interaction: AIAnalyticsSessionLinkedInteractionOut | None = None
    timeline: list[AIAnalyticsSessionEventOut] = Field(default_factory=list)

VoiceAISummaryOut.model_rebuild()

CustomerHistoryOut.model_rebuild()
import inspect
for name, cls in list(locals().items()):
    if inspect.isclass(cls) and issubclass(cls, BaseModel) and cls is not BaseModel:
        try:
            cls.model_rebuild()
        except:
            pass
