"""Strict public DTOs for grants, consent, and tool execution."""

from datetime import UTC, datetime
from typing import Literal
from urllib.parse import urlsplit
from uuid import UUID

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

SCOPE_PERMISSIONS = {
    "projects.read": "projects.read",
    "projects.create": "projects.create",
    "builds.run": "codegen.run",
    "billing.read": "invoices.read",
    "credits.checkout": "billing.manage",
    "credits.purchase": "billing.manage",
}


# A workspace can only be delegated to an agent by someone with at least developer-level
# access there (view projects + run builds). Viewer- and billing-only roles cannot.
MIN_DELEGATION_PERMISSIONS = ("projects.read", "codegen.run")
MAX_GRANT_WORKSPACES = 10
# Billing scopes an owner may delegate to a member who lacks the billing permission (see
# models.WorkspaceBillingDelegation). credits.purchase is deliberately absent: unattended
# spending stays with the workspace owner.
DELEGABLE_BILLING_SCOPES = frozenset({"billing.read", "credits.checkout"})


class StrictModel(BaseModel):
    model_config = ConfigDict(extra="forbid")


def validate_redirect(value: str) -> str:
    if any(ord(char) < 33 for char in value) or "\\" in value:
        raise ValueError("Redirect URI contains unsafe characters")
    parsed = urlsplit(value)
    _ = parsed.port
    loopback = parsed.hostname in ("localhost", "127.0.0.1", "::1")
    if (parsed.scheme != "https" and not (parsed.scheme == "http" and loopback)) or (
        not parsed.hostname or parsed.username or parsed.password or parsed.fragment
    ):
        raise ValueError(
            "Redirect URI must be HTTPS or a loopback HTTP URL, without userinfo or fragment"
        )
    return value


class ClientCreate(BaseModel):
    # RFC 7591 §3.2.1: the server MAY ignore client metadata it doesn't understand;
    # it must not reject the registration outright. Real DCR clients (Claude/ChatGPT
    # among them) routinely send extra fields we don't use — scope, client_uri,
    # logo_uri, contacts, tos_uri, policy_uri, jwks_uri, software_id/version — and
    # StrictModel's extra="forbid" would 422 the whole request on any one of them.
    # This is the one schema that deliberately opts out of that project convention.
    model_config = ConfigDict(extra="ignore")

    client_name: str = Field(min_length=1, max_length=100)
    redirect_uris: list[str] = Field(min_length=1, max_length=10)
    grant_types: list[Literal["authorization_code", "refresh_token"]] = Field(
        default_factory=lambda: ["authorization_code", "refresh_token"]
    )
    response_types: list[Literal["code"]] = Field(default_factory=lambda: ["code"])
    token_endpoint_auth_method: Literal["none"] = "none"

    @field_validator("redirect_uris")
    @classmethod
    def redirects(cls, values):
        for value in values:
            if len(value) > 2048:
                raise ValueError("Redirect URI too long")
            validate_redirect(value)
        return list(dict.fromkeys(values))


class PurchasePolicy(StrictModel):
    currency: str = Field(default="usd", pattern=r"^[a-z]{3}$")
    per_purchase_minor: int = Field(default=0, ge=0, le=100_000_000)
    monthly_minor: int = Field(default=0, ge=0, le=100_000_000)
    saved_payment_consent: bool = False

    @model_validator(mode="after")
    def limits(self):
        if self.per_purchase_minor > self.monthly_minor:
            raise ValueError("Per-purchase limit must not exceed the monthly limit")
        if self.monthly_minor and not self.saved_payment_consent:
            raise ValueError("Explicit saved-payment consent is required for unattended purchases")
        return self


class WorkspaceAccess(StrictModel):
    """What a grant may do in one workspace. A workspace is always granted whole:
    every current and future project in it."""

    workspace_id: UUID
    scopes: list[str] = Field(default_factory=lambda: ["projects.read"], min_length=1, max_length=6)
    purchase_policy: PurchasePolicy = Field(default_factory=PurchasePolicy)

    @field_validator("scopes")
    @classmethod
    def known_scopes(cls, values):
        if set(values) - SCOPE_PERMISSIONS.keys():
            raise ValueError("Unknown agent scope")
        return list(dict.fromkeys(values))

    @model_validator(mode="after")
    def purchase_scope(self):
        if self.purchase_policy.monthly_minor and "credits.purchase" not in self.scopes:
            raise ValueError("Purchase limits require the credits.purchase scope")
        return self


def _unique_workspaces(values: list[WorkspaceAccess]) -> list[WorkspaceAccess]:
    if len({value.workspace_id for value in values}) != len(values):
        raise ValueError("Each workspace may be listed only once")
    return values


class GrantCreate(StrictModel):
    name: str = Field(min_length=1, max_length=100)
    workspaces: list[WorkspaceAccess] = Field(min_length=1, max_length=MAX_GRANT_WORKSPACES)
    expires_at: datetime

    @field_validator("workspaces")
    @classmethod
    def unique_workspaces(cls, values):
        return _unique_workspaces(values)

    @field_validator("expires_at")
    @classmethod
    def future_expiry(cls, value):
        if value.tzinfo is None or value <= datetime.now(UTC):
            raise ValueError("Expiration must be a future timezone-aware timestamp")
        return value

    @property
    def all_scopes(self) -> list[str]:
        """Union across workspaces; used for OAuth scope negotiation only."""
        return sorted({scope for access in self.workspaces for scope in access.scopes})


class GrantUpdate(StrictModel):
    """Edit an existing connection without reconnecting. Scopes can be reshaped per
    workspace but never widened past what the connection was originally granted."""

    name: str | None = Field(default=None, min_length=1, max_length=100)
    workspaces: list[WorkspaceAccess] | None = Field(
        default=None, min_length=1, max_length=MAX_GRANT_WORKSPACES
    )

    @field_validator("workspaces")
    @classmethod
    def unique_workspaces(cls, values):
        return None if values is None else _unique_workspaces(values)


class AuthorizationRequest(StrictModel):
    client_id: str = Field(max_length=64)
    redirect_uri: str = Field(max_length=2048)
    response_type: Literal["code"] = "code"
    code_challenge: str = Field(pattern=r"^[A-Za-z0-9_-]{43}$")
    code_challenge_method: Literal["S256"] = "S256"
    resource: str
    scope: str = Field(default="projects.read", max_length=300)
    state: str = Field(default="", max_length=2048)


class ConsentCreate(StrictModel):
    authorization: AuthorizationRequest
    grant: GrantCreate


class ToolCall(StrictModel):
    name: str = Field(max_length=40)
    arguments: dict = Field(default_factory=dict)
