"""Wire models for the gateway connector routes.

Tolerate added gateway fields; callers receive plain dictionaries. Correlate results by position, never wire ``index``.
"""

from __future__ import annotations

from typing import Any, Literal, Optional

from pydantic import BaseModel, ConfigDict, Field

CONNECTORS_PATH = "v1/connectors"
CONNECTOR_SEARCH_PATH = f"{CONNECTORS_PATH}/search"
CONNECTOR_SCHEMAS_PATH = f"{CONNECTORS_PATH}/schemas"
CONNECTOR_EXECUTE_PATH = f"{CONNECTORS_PATH}/execute"
CONNECTOR_CONNECTIONS_PATH = f"{CONNECTORS_PATH}/connections"
CONNECTOR_ACCOUNTS_PATH = f"{CONNECTORS_PATH}/accounts"
CONNECTOR_FILE_UPLOAD_PATH = f"{CONNECTORS_PATH}/files/upload"
# The gateway's cap on a staged file's declared size.
CONNECTOR_FILE_MAX_BYTES = 20 * 1024 * 1024

# The gateway's six-state account status (contract `CONNECTOR_CONNECTION_STATUSES`; the vendor's
# INITIALIZING and INITIATED both arrive as `pending`). Present only once the session binds an
# account; a value outside this set is a contract break and fails validation.
ConnectionStatus = Literal["pending", "active", "failed", "expired", "revoked", "inactive"]
# Where the vendor's done page sends the browser after consent; the dev desktop registers hermes-dev://.
ConnectorReturnTarget = Literal["hermes-desktop", "hermes-desktop-dev", "portal"]
# Who asked for a connect, so the gateway can attribute each minted link. The gateway sets
# ``execute_link`` and ``portal`` itself; an older gateway ignores the field.
ConnectorInitiator = Literal["agent_tool", "desktop_page", "card_retry"]

# Hermes dispatch caps batches lower, so client-side chunking is deliberately absent.
WIRE_BATCH_MAX = 25

# Any string: a gateway newer than this client may add codes, and an unknown code still renders.
ConnectorErrorCode = str
# `product`: the request or the user's setup is the cause. `backend`: the gateway or the vendor failed.
ErrorFamily = Literal["product", "backend"]

# Single-call execute failures: code -> (family, HTTP status). The legacy array envelope carries only
# TOOL_NOT_ALLOWED, CONNECTION_REQUIRED, TOOL_NOT_FOUND and PROVIDER_ERROR, always under a 200.
EXECUTE_ERROR_CODES: dict[str, tuple[ErrorFamily, int]] = {
    "TOOL_NOT_ALLOWED": ("product", 403),
    "CONNECTION_REQUIRED": ("product", 409),
    "CONNECTION_EXPIRED": ("product", 409),
    "ALIAS_REQUIRED": ("product", 422),
    "ALIAS_UNKNOWN": ("product", 422),
    "INVALID_ARGUMENTS": ("product", 422),
    "TOOL_NOT_FOUND": ("product", 404),
    "PROVIDER_ERROR": ("backend", 502),
    "PROVIDER_RATE_LIMITED": ("backend", 429),
    "PROVIDER_TIMEOUT": ("backend", 504),
    "POLICY_UNAVAILABLE": ("backend", 503),
    "GATEWAY_ERROR": ("backend", 500),
}


# Failures the agent reports itself while staging a file input, before the call reaches execute.
FILE_ERROR_FAMILIES: dict[str, ErrorFamily] = {
    "FILE_NOT_ALLOWED": "product",
    "FILE_NOT_FOUND": "product",
    "FILE_TOO_LARGE": "product",
    "FILE_UPLOAD_FAILED": "backend",
}


def execute_error_family(code: str, family: Optional[str] = None) -> Optional[str]:
    """The gateway's family when it sent one, else the table's; ``None`` for an unknown code."""
    if family in ("product", "backend"):
        return family
    known = EXECUTE_ERROR_CODES.get(code)
    return known[0] if known else FILE_ERROR_FAMILIES.get(code)


class _Wire(BaseModel):

    model_config = ConfigDict(extra="ignore", populate_by_name=True)


class ConnectorSearchQuery(_Wire):
    use_case: str = Field(alias="useCase")
    known_fields: Optional[str] = Field(default=None, alias="knownFields")


class ConnectorSearchRequest(_Wire):
    queries: list[ConnectorSearchQuery]


class ConnectorToolSchema(_Wire):
    connector: str
    tool: str
    description: str = ""
    input_schema: dict[str, Any] = Field(default_factory=dict, alias="inputSchema")


class ConnectorSearchResult(_Wire):
    # Vendor passthrough; result correlation uses array position.
    index: int
    use_case: str = Field(default="", alias="useCase")
    tools: list[str] = Field(default_factory=list)
    related_tools: list[str] = Field(default_factory=list, alias="relatedTools")
    connectors: list[str] = Field(default_factory=list)
    guidance: Optional[str] = None
    plan_steps: Optional[list[str]] = Field(default=None, alias="planSteps")
    pitfalls: Optional[list[str]] = None
    error: Optional[str] = None


class ConnectorConnectionStatus(_Wire):
    connector: str
    connected: bool = False
    description: str = ""


class ConnectorSearchResponse(_Wire):
    results: list[ConnectorSearchResult] = Field(default_factory=list)
    schemas: dict[str, ConnectorToolSchema] = Field(default_factory=dict)
    connections: list[ConnectorConnectionStatus] = Field(default_factory=list)
    next_steps: list[str] = Field(default_factory=list, alias="nextSteps")


class ConnectorSchemasRequest(_Wire):
    tools: list[str]


class ConnectorSchemasResponse(_Wire):
    schemas: dict[str, ConnectorToolSchema] = Field(default_factory=dict)
    not_found: list[str] = Field(default_factory=list, alias="notFound")
    suggestions: dict[str, list[str]] = Field(default_factory=dict)


class ConnectorFileUploadRequest(_Wire):
    toolkit: str
    tool: str
    filename: str
    mimetype: str
    size: int


class ConnectorFileDescriptor(_Wire):
    """The value a ``file_uploadable`` tool input takes on the wire."""

    name: str
    mimetype: str
    s3key: str


class ConnectorFileUploadResponse(_Wire):
    upload_url: str = Field(alias="uploadUrl")
    headers: dict[str, str] = Field(default_factory=dict)
    descriptor: ConnectorFileDescriptor
    expires_at: str = Field(alias="expiresAt")


class ConnectorToolError(_Wire):
    code: ConnectorErrorCode
    message: str
    connector: Optional[str] = None
    connect_url: Optional[str] = Field(default=None, alias="connectUrl")
    # The account the link was minted for; absent when the link mint failed. Absent means nothing to watch.
    connection_id: Optional[str] = Field(default=None, alias="connectionId")
    hint: Optional[str] = None
    family: Optional[str] = None


class ConnectorExecuteFailure(ConnectorToolError):
    """The body of a non-2xx single-call execute answer."""

    request_id: Optional[str] = Field(default=None, alias="requestId")


class ConnectorExecuteCall(_Wire):
    connector: str
    tool: str
    arguments: dict[str, Any] = Field(default_factory=dict)
    # Never sent by hermes: the model picks an account with the ``connector_alias`` argument, which
    # the gateway strips and resolves itself.
    account: Optional[str] = None


class ConnectorExecuteRequest(_Wire):
    tools: list[ConnectorExecuteCall]
    # Ride any CONNECTION_REQUIRED link the call mints back to the surface that asked.
    return_to: Optional[ConnectorReturnTarget] = Field(default=None, alias="returnTo")
    op: Optional[str] = Field(default=None, min_length=1, max_length=128)


class ConnectorExecuteSingleRequest(_Wire):
    call: ConnectorExecuteCall
    return_to: Optional[ConnectorReturnTarget] = Field(default=None, alias="returnTo")
    op: Optional[str] = Field(default=None, min_length=1, max_length=128)


# What a model receiving this result must guarantee; absent when the connector asks nothing.
DataUseRequirement = Literal["no_training", "zero_retention"]


class ConnectorDataUse(_Wire):
    requires: list[DataUseRequirement]


class ConnectorExecuteSingleResult(_Wire):
    connector: str = ""
    tool: str = ""
    data: Any = None
    account: Optional[str] = None
    data_use: Optional[ConnectorDataUse] = Field(default=None, alias="dataUse")


class ConnectorExecuteSingleResponse(_Wire):
    result: ConnectorExecuteSingleResult


class ConnectorExecuteResult(_Wire):
    # Never correlate results by wire index.
    index: int = 0
    connector: str = ""
    tool: str = ""
    data: Any = None
    error: Optional[ConnectorToolError] = None
    # The alias of the account the call ran under; null when that account has no alias.
    account: Optional[str] = None
    data_use: Optional[ConnectorDataUse] = Field(default=None, alias="dataUse")


class ConnectorExecuteResponse(_Wire):
    results: list[ConnectorExecuteResult] = Field(default_factory=list)
    success_count: int = Field(default=0, alias="successCount")
    error_count: int = Field(default=0, alias="errorCount")
    total_count: int = Field(default=0, alias="totalCount")


class ConnectorConnectionsRequest(_Wire):
    connectors: list[str]
    reinitiate: bool = False
    alias: Optional[str] = None
    # The existing account a reinitiate repairs; sent instead of ``alias`` when known.
    connection_id: Optional[str] = Field(default=None, alias="connectionId")
    return_to: Optional[ConnectorReturnTarget] = Field(default=None, alias="returnTo")
    # The caller's operation id, echoed on the hermes://connections/done link.
    op: Optional[str] = Field(default=None, min_length=1, max_length=128)
    initiator: Optional[ConnectorInitiator] = None


class ConnectorConnectionResult(_Wire):
    connector: str
    status: Literal["active", "initiated", "failed"]
    connect_url: Optional[str] = Field(default=None, alias="connectUrl")
    # The vendor account the mint created or observed. Optional by vendor semantics (a no-auth toolkit
    # answers active with none); a target without one has nothing to watch.
    connection_id: Optional[str] = Field(default=None, alias="connectionId")
    alias: Optional[str] = None
    instruction: Optional[str] = None
    # Vendor error_message on ``failed``; the list route never carries it.
    status_reason: Optional[str] = Field(default=None, alias="statusReason")
    reinitiated: bool = False


class ConnectorListItem(_Wire):
    connector: str
    enabled: bool = True
    connected: bool = False
    connection_status: Optional[ConnectionStatus] = Field(default=None, alias="connectionStatus")
    status_reason: Optional[str] = Field(default=None, alias="statusReason")
    gateway_disabled_tools: list[str] = Field(default_factory=list, alias="disabledTools")


class ConnectorListResponse(_Wire):
    items: list[ConnectorListItem]
    next_cursor: Optional[str] = Field(default=None, alias="nextCursor")


class ConnectorAccount(_Wire):
    connection_id: str = Field(alias="connectionId")
    connector: str
    status: ConnectionStatus
    status_reason: Optional[str] = Field(default=None, alias="statusReason")
    label: str = Field(min_length=1)
    alias: Optional[str] = None
    # The newest active account for this connector: the one the vendor executes with.
    active: bool
    # Retired by a reconnect that replaced it; kept, never selectable.
    disabled: bool = False
    created_at: str = Field(alias="createdAt")
    updated_at: str = Field(alias="updatedAt")


class ConnectorAccountsResponse(_Wire):
    accounts: list[ConnectorAccount]


class RemovedConnectorAccount(_Wire):
    connection_id: str = Field(alias="connectionId")
    connector: str
    status: Literal["removed"]


class ConnectorConnectionsSummary(_Wire):
    total: int = 0
    active: int = 0
    initiated: int = 0
    failed: int = 0


class ConnectorConnectionsResponse(_Wire):
    results: list[ConnectorConnectionResult] = Field(default_factory=list)
    summary: ConnectorConnectionsSummary = Field(
        default_factory=ConnectorConnectionsSummary
    )
