"""External admin domain - Pydantic schemas."""
from __future__ import annotations

import re
from datetime import datetime
from typing import Any, Optional
from uuid import UUID

from pydantic import BaseModel, Field, field_validator

# ── Installation ───────────────────────────────────────────────────────────────

class ExternalAdminInstallationResponse(BaseModel):
    id: UUID
    project_id: UUID
    workspace_id: UUID
    installation_type: str
    status: str
    shop_domain: Optional[str]
    site_url: Optional[str]
    companion_version: Optional[str]
    last_heartbeat_at: Optional[datetime]
    connected_at: Optional[datetime]
    disconnected_at: Optional[datetime]
    tampered_at: Optional[datetime]
    last_error: Optional[str]
    created_at: datetime
    updated_at: datetime

    model_config = {"from_attributes": True}


# ── Credentials ────────────────────────────────────────────────────────────────

class ExternalCredentialSummary(BaseModel):
    id: UUID
    credential_type: str
    version: int
    status: str
    rotated_at: Optional[datetime]
    revoked_at: Optional[datetime]
    created_at: datetime

    model_config = {"from_attributes": True}


class RotateCredentialRequest(BaseModel):
    credential_type: str = Field(..., min_length=1, max_length=40)


class RotateCredentialResponse(BaseModel):
    credential_id: UUID
    credential_type: str
    version: int
    status: str
    created_at: datetime


# ── Assistant settings ─────────────────────────────────────────────────────────

class AssistantProjectSettingsResponse(BaseModel):
    project_id: UUID
    ai_usage_mode: str
    usage_budget: Optional[int]
    chat_session_ttl_hours: int
    action_approval_required: bool
    allowed_capabilities: list[str]
    notify_on_approval_required: bool
    created_at: datetime
    updated_at: datetime

    model_config = {"from_attributes": True}


class AssistantProjectSettingsUpdate(BaseModel):
    ai_usage_mode: Optional[str] = Field(default=None, pattern="^(p2hs_credits|byok)$")
    usage_budget: Optional[int] = Field(default=None, ge=0)
    chat_session_ttl_hours: Optional[int] = Field(default=None, ge=1, le=72)
    action_approval_required: Optional[bool] = None
    allowed_capabilities: Optional[list[str]] = None
    notify_on_approval_required: Optional[bool] = None


# ── Action sessions ────────────────────────────────────────────────────────────

class AssistantActionSessionResponse(BaseModel):
    id: UUID
    project_id: UUID
    installation_id: UUID
    correlation_id: str
    action: str
    status: str
    requested_by: Optional[str]
    reviewed_by_user_id: Optional[UUID]
    payload: dict[str, Any]
    result: Optional[dict[str, Any]]
    expires_at: datetime
    applied_at: Optional[datetime]
    reviewed_at: Optional[datetime]
    created_at: datetime
    updated_at: datetime

    model_config = {"from_attributes": True}


class ActionSessionReviewRequest(BaseModel):
    note: Optional[str] = Field(default=None, max_length=500)


class ActionSessionListResponse(BaseModel):
    items: list[AssistantActionSessionResponse]
    total: int


# ── Consents ───────────────────────────────────────────────────────────────────

class AssistantDataConsentResponse(BaseModel):
    id: UUID
    project_id: UUID
    consent_type: str
    status: str
    granted_at: Optional[datetime]
    revoked_at: Optional[datetime]
    granted_by_user_id: UUID
    notes: Optional[str]
    created_at: datetime
    updated_at: datetime

    model_config = {"from_attributes": True}


class GrantConsentRequest(BaseModel):
    consent_type: str = Field(..., min_length=1, max_length=40)
    notes: Optional[str] = Field(default=None, max_length=500)


# ── External-facing (plugin → Core) ───────────────────────────────────────────

class WordPressConnectCompleteRequest(BaseModel):
    connect_code: str = Field(..., min_length=16, max_length=4096)
    site_url: str = Field(..., min_length=1, max_length=2048)


class WordPressConnectStartRequest(BaseModel):
    site_url: str = Field(..., min_length=1, max_length=2048)
    return_url: str = Field(..., min_length=1, max_length=2048)


class WordPressConnectCodeRequest(BaseModel):
    site_url: str = Field(..., min_length=1, max_length=2048)
    # Where the browser will be sent back to with the code. Validated against site_url
    # (same origin, inside wp-admin). Optional only so an older Dashboard keeps working;
    # to be made required once the Dashboard always sends it.
    return_url: Optional[str] = Field(default=None, max_length=2048)


class WordPressConnectCodeResponse(BaseModel):
    ok: bool
    installation_id: str
    connect_code: str
    app_url: str
    core_api_url: str
    # Deprecated and always empty: connect completion goes only through the WP Gateway.
    core_direct_api_url: str = ""


class WordPressConnectProjectRequest(BaseModel):
    site_url: str = Field(..., min_length=1, max_length=2048)
    name: str = Field(..., min_length=1, max_length=120)
    description: Optional[str] = Field(default=None, max_length=500)
    # See WordPressConnectCodeRequest.return_url.
    return_url: Optional[str] = Field(default=None, max_length=2048)


class WordPressConnectProjectResponse(WordPressConnectCodeResponse):
    project_id: str


class WordPressHostedUrlRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)
    site_url: str = Field(..., min_length=1, max_length=2048)
    return_url: Optional[str] = Field(default=None, max_length=2048)
    flow: Optional[str] = Field(default=None, max_length=80)


class WordPressHostedUrlResponse(BaseModel):
    ok: bool
    url: str


class WordPressConnectCompleteResponse(BaseModel):
    ok: bool
    installation_id: str
    project_id: str
    credential_token: str
    core_api_url: str
    app_url: str
    embed_url: str


class WordPressEmbedExchangeRequest(BaseModel):
    embed_token: str = Field(..., min_length=16, max_length=4096)
    project_id: str = Field(..., min_length=1, max_length=64)


class WordPressEmbedExchangeResponse(BaseModel):
    ok: bool
    access_token: str
    expires_at: int
    user_id: str
    workspace_id: str
    project_id: str
    installation_id: str
    site_url: str


class WordPressHeartbeatRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)
    site_url: str = Field(..., min_length=1, max_length=2048)
    plugin_version: str = Field(..., min_length=1, max_length=40)
    companion_version: Optional[str] = Field(default=None, max_length=40)
    plugin_file_hash: Optional[str] = Field(default=None, min_length=64, max_length=64)
    companion_file_hash: Optional[str] = Field(default=None, min_length=64, max_length=64)
    php_version: Optional[str] = Field(default=None, max_length=20)
    wp_version: Optional[str] = Field(default=None, max_length=20)

    @field_validator("plugin_file_hash", "companion_file_hash", mode="before")
    @classmethod
    def _hex_only(cls, v: Any) -> Any:
        if v == "":
            return None
        if v is not None and not re.fullmatch(r"[a-fA-F0-9]{64}", str(v)):
            raise ValueError("hash must be a 64-character SHA-256 hex digest")
        return str(v).lower() if v is not None else None


class WordPressHeartbeatResponse(BaseModel):
    ok: bool
    status: str
    installation_id: str
    companion_update_available: bool
    companion_update_url: Optional[str]
    companion_update_signature: Optional[str]
    companion_update_version: Optional[str] = None
    companion_update_sha256: Optional[str] = None
    site_ready: bool = False
    readiness: Optional[dict[str, Any]] = None


class ShopifyOAuthCallbackRequest(BaseModel):
    code: str = Field(..., min_length=1, max_length=512)
    shop: str = Field(..., min_length=1, max_length=255)
    state: str = Field(..., min_length=1, max_length=512)


class ShopifyOAuthCallbackResponse(BaseModel):
    ok: bool
    installation_id: str
    shop_domain: str
    redirect_url: str
    # Per-installation credential for the BFF's later calls. Minted on every (re)install, returned
    # here only, and never stored in the clear by Core (hash + encrypted copy, as for WordPress).
    installation_token: str


class ShopifyWebhookEnvelope(BaseModel):
    topic: str = Field(..., min_length=1, max_length=120)
    shopDomain: str = Field(..., min_length=1, max_length=255)
    payload: Any
    receivedAt: str = Field(..., min_length=1, max_length=64)

    @property
    def shop_domain(self) -> str:
        return self.shopDomain

    @property
    def received_at(self) -> str:
        return self.receivedAt


class ShopifyConnectProjectRequest(BaseModel):
    shop_domain: str = Field(..., min_length=1, max_length=255)
    name: str = Field(..., min_length=1, max_length=120)
    description: Optional[str] = Field(default=None, max_length=500)


class ShopifyConnectProjectResponse(BaseModel):
    ok: bool
    project_id: str
    installation_id: str
    shop_domain: str


class ShopifyHeartbeatRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)
    shop_domain: str = Field(..., min_length=1, max_length=255)


class ShopifyHeartbeatResponse(BaseModel):
    ok: bool
    status: str
    installation_id: str
    shop_domain: str
    site_ready: bool = False
    readiness: Optional[dict[str, Any]] = None


class ShopifyChatSessionOpenRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)
    shop_domain: str = Field(..., min_length=1, max_length=255)


class ShopifyChatSessionOpenResponse(BaseModel):
    ok: bool
    session_id: str
    expires_at: str
    poll_interval_ms: int = 2000


class ShopifyChatMessageItem(BaseModel):
    id: str
    role: str
    content: str
    created_at: str


class ShopifyChatMessageSendRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)
    shop_domain: str = Field(..., min_length=1, max_length=255)
    session_id: str = Field(..., min_length=1, max_length=64)
    message: str = Field(..., min_length=1, max_length=16000)
    idempotency_key: Optional[str] = Field(default=None, max_length=128)


class ShopifyChatMessageSendResponse(BaseModel):
    ok: bool
    job_id: str
    status: str
    user_message_id: Optional[str] = None
    assistant_message: Optional[ShopifyChatMessageItem] = None


class ShopifyChatMessageListRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)
    shop_domain: str = Field(..., min_length=1, max_length=255)
    session_id: str = Field(..., min_length=1, max_length=64)
    cursor: Optional[str] = Field(default=None, max_length=64)
    limit: int = Field(default=50, ge=1, le=100)


class ShopifyChatMessageListResponse(BaseModel):
    ok: bool
    messages: list[ShopifyChatMessageItem]
    next_cursor: Optional[str] = None


class ShopifyChatJobPollRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)
    shop_domain: str = Field(..., min_length=1, max_length=255)
    job_id: str = Field(..., min_length=1, max_length=64)


class ShopifyChatJobPollResponse(BaseModel):
    ok: bool
    job_id: str
    status: str
    assistant_message: Optional[ShopifyChatMessageItem] = None
    action_session_id: Optional[str] = None
    error: Optional[str] = None


class ShopifySiteUnderstandingSyncRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)
    shop_domain: str = Field(..., min_length=1, max_length=255)
    reason: str = Field(default="manual", max_length=40)
    payload: Optional[dict[str, Any]] = None


class ShopifySiteUnderstandingSyncResponse(BaseModel):
    ok: bool
    sync_id: str
    status: str
    message: str
    poll_interval_ms: int = 3000


class ShopifySiteUnderstandingStatusRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)
    shop_domain: str = Field(..., min_length=1, max_length=255)


class ShopifySiteUnderstandingStatusResponse(BaseModel):
    ok: bool
    status: str
    sync_id: Optional[str] = None
    last_sync_at: Optional[str] = None
    message: Optional[str] = None
    site_ready: bool = False
    readiness: Optional[dict[str, Any]] = None


class ShopifyApplyPollRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)


class ShopifyApplyPollResponse(BaseModel):
    ok: bool
    has_mutation: bool
    action_id: Optional[str] = None
    session_id: Optional[str] = None
    sequence: Optional[int] = None
    action: Optional[str] = None
    description: Optional[str] = None
    payload: Optional[dict[str, Any]] = None
    correlation_id: Optional[str] = None


class ShopifyApplyReportRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)
    action_id: str = Field(..., min_length=1, max_length=64)
    ok: bool
    result: dict[str, Any] = Field(default_factory=dict)


class ShopifyApplyReportResponse(BaseModel):
    ok: bool
    session_id: str
    session_status: str
    has_more: bool


class CompanionInstallRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)
    requested_version: Optional[str] = Field(default=None, max_length=40)


class CompanionInstallResponse(BaseModel):
    ok: bool
    download_url: str
    signature: Optional[str] = None
    version: str
    sha256: str


class WordPressChatSessionOpenRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)
    site_url: str = Field(..., min_length=1, max_length=2048)


class WordPressChatSessionOpenResponse(BaseModel):
    ok: bool
    session_id: str
    expires_at: str
    poll_interval_ms: int = 2000


class WordPressChatMessageSendRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)
    site_url: str = Field(..., min_length=1, max_length=2048)
    session_id: str = Field(..., min_length=1, max_length=64)
    message: str = Field(..., min_length=1, max_length=16000)
    idempotency_key: Optional[str] = Field(default=None, max_length=128)


class WordPressChatMessageItem(BaseModel):
    id: str
    role: str
    content: str
    created_at: str


class WordPressChatMessageSendResponse(BaseModel):
    ok: bool
    job_id: str
    status: str
    user_message_id: Optional[str] = None
    assistant_message: Optional[WordPressChatMessageItem] = None


class WordPressChatMessageListRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)
    site_url: str = Field(..., min_length=1, max_length=2048)
    session_id: str = Field(..., min_length=1, max_length=64)
    cursor: Optional[str] = Field(default=None, max_length=64)
    limit: int = Field(default=50, ge=1, le=100)


class WordPressChatMessageListResponse(BaseModel):
    ok: bool
    messages: list[WordPressChatMessageItem]
    next_cursor: Optional[str] = None


class WordPressChatJobPollRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)
    site_url: str = Field(..., min_length=1, max_length=2048)
    job_id: str = Field(..., min_length=1, max_length=64)


class WordPressChatJobPollResponse(BaseModel):
    ok: bool
    job_id: str
    status: str
    assistant_message: Optional[WordPressChatMessageItem] = None
    action_session_id: Optional[str] = None
    error: Optional[str] = None


class WordPressSiteUnderstandingSyncRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)
    site_url: str = Field(..., min_length=1, max_length=2048)
    reason: str = Field(default="manual", max_length=40)
    payload: Optional[dict[str, Any]] = None


class WordPressSiteUnderstandingSyncResponse(BaseModel):
    ok: bool
    sync_id: str
    status: str
    message: str
    poll_interval_ms: int = 3000


class WordPressSiteUnderstandingStatusRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)
    site_url: str = Field(..., min_length=1, max_length=2048)


class WordPressSiteUnderstandingStatusResponse(BaseModel):
    ok: bool
    status: str
    sync_id: Optional[str] = None
    last_sync_at: Optional[str] = None
    message: Optional[str] = None
    site_ready: bool = False
    readiness: Optional[dict[str, Any]] = None


class WordPressCompanionMutationPollRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)


class WordPressCompanionMutationPollResponse(BaseModel):
    ok: bool
    has_mutation: bool
    action_id: Optional[str] = None
    session_id: Optional[str] = None
    sequence: Optional[int] = None
    action: Optional[str] = None
    description: Optional[str] = None
    payload: Optional[dict[str, Any]] = None
    correlation_id: Optional[str] = None


class WordPressCompanionMutationReportRequest(BaseModel):
    installation_id: str = Field(..., min_length=1, max_length=64)
    action_id: str = Field(..., min_length=1, max_length=64)
    ok: bool
    result: dict[str, Any] = Field(default_factory=dict)


class WordPressCompanionMutationReportResponse(BaseModel):
    ok: bool
    session_id: str
    session_status: str
    has_more: bool
