"""
Pydantic request/response schemas for web and API routes.
"""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Any, Dict, List, Literal, Optional

from pydantic import (
    AliasChoices,
    BaseModel,
    ConfigDict,
    Field,
    field_validator,
    model_validator,
)

from ...core.car_context import CarContextProfile
from ...integrations.chat.approval_modes import normalize_approval_mode


class Payload(BaseModel):
    model_config = ConfigDict(extra="ignore", populate_by_name=True)


class ResponseModel(BaseModel):
    model_config = ConfigDict(extra="ignore")


class ContextspaceWriteRequest(Payload):
    content: str = ""


class ContextspaceDocKindInfo(ResponseModel):
    kind: str
    path: str
    label: str
    description: str


class ContextspaceResponse(ResponseModel):
    active_context: str
    decisions: str
    spec: str
    kinds: List[ContextspaceDocKindInfo] = Field(default_factory=list)


class ArchiveSnapshotSummary(ResponseModel):
    snapshot_id: str
    worktree_repo_id: str
    created_at: Optional[str] = None
    status: Optional[str] = None
    branch: Optional[str] = None
    head_sha: Optional[str] = None
    note: Optional[str] = None
    summary: Optional[Dict[str, Any]] = None


class ArchiveSnapshotsResponse(ResponseModel):
    snapshots: List[ArchiveSnapshotSummary]


class ArchiveSnapshotDetailResponse(ResponseModel):
    snapshot: ArchiveSnapshotSummary
    meta: Optional[Dict[str, Any]] = None


class LocalRunArchiveSummary(ResponseModel):
    run_id: str
    archived_at: Optional[str] = None
    has_tickets: bool = False
    has_runs: bool = False


class LocalRunArchivesResponse(ResponseModel):
    archives: List[LocalRunArchiveSummary]


class ArchiveTreeNode(ResponseModel):
    path: str
    name: str
    type: Literal["file", "folder"]
    size_bytes: Optional[int] = None
    mtime: Optional[float] = None


class ArchiveTreeResponse(ResponseModel):
    path: str
    nodes: List[ArchiveTreeNode]


class SpecIngestTicketsResponse(ResponseModel):
    status: str
    created: int
    first_ticket_path: Optional[str] = None


class RunControlRequest(Payload):
    once: bool = False
    agent: Optional[str] = None
    model: Optional[str] = None
    reasoning: Optional[str] = None


class HubCreateRepoRequest(Payload):
    git_url: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("git_url", "gitUrl")
    )
    repo_id: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("repo_id", "id")
    )
    path: Optional[str] = None
    git_init: bool = True
    force: bool = False


class HubRemoveRepoRequest(Payload):
    force: bool = False
    force_attestation: Optional[str] = Field(
        default=None,
        validation_alias=AliasChoices("force_attestation", "forceAttestation"),
    )
    delete_dir: bool = True
    delete_worktrees: bool = False


class HubCreateAgentWorkspaceRequest(Payload):
    workspace_id: Optional[str] = Field(
        default=None,
        validation_alias=AliasChoices("workspace_id", "workspaceId", "id"),
    )
    runtime: str
    enabled: bool = True
    display_name: Optional[str] = Field(
        default=None,
        validation_alias=AliasChoices("display_name", "displayName", "name"),
    )


class HubUpdateAgentWorkspaceRequest(Payload):
    enabled: Optional[bool] = None
    display_name: Optional[str] = Field(
        default=None,
        validation_alias=AliasChoices("display_name", "displayName", "name"),
    )


class HubRemoveAgentWorkspaceRequest(Payload):
    delete_dir: bool = Field(
        default=False, validation_alias=AliasChoices("delete_dir", "deleteDir")
    )


class HubDeleteAgentWorkspaceRequest(Payload):
    delete_dir: bool = Field(
        default=True, validation_alias=AliasChoices("delete_dir", "deleteDir")
    )


class HubCreateWorktreeRequest(Payload):
    base_repo_id: str = Field(
        validation_alias=AliasChoices("base_repo_id", "baseRepoId")
    )
    branch: str
    force: bool = False
    start_point: Optional[str] = Field(
        default=None,
        validation_alias=AliasChoices(
            "start_point", "startPoint", "base_ref", "baseRef"
        ),
    )


class HubCleanupWorktreeRequest(Payload):
    worktree_repo_id: str = Field(
        validation_alias=AliasChoices("worktree_repo_id", "worktreeRepoId")
    )
    delete_branch: bool = False
    delete_remote: bool = False
    force: bool = False
    force_attestation: Optional[str] = Field(
        default=None,
        validation_alias=AliasChoices("force_attestation", "forceAttestation"),
    )
    archive: bool = True
    force_archive: bool = Field(
        default=False, validation_alias=AliasChoices("force_archive", "forceArchive")
    )
    archive_note: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("archive_note", "archiveNote")
    )
    archive_profile: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("archive_profile", "archiveProfile")
    )


class HubArchiveWorktreeRequest(Payload):
    worktree_repo_id: str = Field(
        validation_alias=AliasChoices("worktree_repo_id", "worktreeRepoId")
    )
    archive_note: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("archive_note", "archiveNote")
    )
    archive_profile: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("archive_profile", "archiveProfile")
    )


class HubArchiveRepoStateRequest(Payload):
    repo_id: str = Field(validation_alias=AliasChoices("repo_id", "repoId"))
    archive_note: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("archive_note", "archiveNote")
    )
    archive_profile: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("archive_profile", "archiveProfile")
    )


class HubArchiveWorktreeResponse(ResponseModel):
    snapshot_id: str
    snapshot_path: str
    meta_path: str
    status: str
    file_count: int
    total_bytes: int
    flow_run_count: int
    latest_flow_run_id: Optional[str]


class HubArchiveWorktreeStateResponse(ResponseModel):
    snapshot_id: Optional[str]
    snapshot_path: Optional[str]
    meta_path: Optional[str]
    status: str
    file_count: int
    total_bytes: int
    flow_run_count: int
    latest_flow_run_id: Optional[str]
    archived_paths: list[str]
    reset_paths: list[str]
    archived_thread_ids: list[str] = []
    archived_thread_count: int = 0


class HubArchiveRepoStateResponse(HubArchiveWorktreeStateResponse):
    pass


class AppServerThreadResetRequest(Payload):
    key: str = Field(
        validation_alias=AliasChoices("key", "feature", "feature_key", "featureKey")
    )


class AppServerThreadArchiveRequest(Payload):
    thread_id: str = Field(validation_alias=AliasChoices("thread_id", "threadId", "id"))


class PmaManagedThreadCreateRequest(Payload):
    model_config = ConfigDict(extra="forbid", populate_by_name=True)

    agent: Optional[Literal["codex", "hermes", "opencode", "zeroclaw"]] = None
    profile: Optional[str] = None
    resource_kind: Optional[Literal["repo", "agent_workspace"]] = Field(
        default=None, validation_alias=AliasChoices("resource_kind", "resourceKind")
    )
    resource_id: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("resource_id", "resourceId")
    )
    repo_id: Optional[str] = Field(
        default=None,
        validation_alias=AliasChoices("repo_id", "repoId"),
        exclude=True,
    )
    workspace_root: Optional[str] = None
    name: Optional[str] = None
    notify_on: Optional[Literal["terminal"]] = Field(
        default=None, validation_alias=AliasChoices("notify_on", "notifyOn")
    )
    terminal_followup: Optional[bool] = Field(
        default=None,
        validation_alias=AliasChoices("terminal_followup", "terminalFollowup"),
    )
    notify_lane: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("notify_lane", "notifyLane")
    )
    notify_once: bool = Field(
        default=True, validation_alias=AliasChoices("notify_once", "notifyOnce")
    )
    context_profile: Optional[CarContextProfile] = Field(
        default=None,
        validation_alias=AliasChoices("context_profile", "contextProfile"),
    )
    approval_mode: Optional[str] = Field(
        default=None,
        validation_alias=AliasChoices("approval_mode", "approvalMode"),
    )
    notify_on_explicit: bool = Field(default=False, exclude=True)
    terminal_followup_explicit: bool = Field(default=False, exclude=True)
    notify_lane_explicit: bool = Field(default=False, exclude=True)
    notify_once_explicit: bool = Field(default=False, exclude=True)

    @field_validator("approval_mode")
    @classmethod
    def _normalize_approval_mode(cls, value: Optional[str]) -> Optional[str]:
        if value is None:
            return None
        normalized = normalize_approval_mode(value)
        if normalized is None:
            raise ValueError("approval_mode is invalid")
        return normalized

    @model_validator(mode="before")
    @classmethod
    def _capture_followup_intent(cls, value: Any) -> Any:
        if not isinstance(value, dict):
            return value
        payload = dict(value)
        repo_id = payload.get("repo_id", payload.get("repoId"))
        if repo_id is not None:
            raise ValueError(
                "repo_id is not supported; use resource_kind='repo' with resource_id"
            )
        payload["notify_on_explicit"] = any(
            key in value for key in ("notify_on", "notifyOn")
        )
        payload["terminal_followup_explicit"] = any(
            key in value for key in ("terminal_followup", "terminalFollowup")
        )
        payload["notify_lane_explicit"] = any(
            key in value for key in ("notify_lane", "notifyLane")
        )
        payload["notify_once_explicit"] = any(
            key in value for key in ("notify_once", "notifyOnce")
        )
        return payload


class SessionSettingsRequest(Payload):
    autorunner_model_override: Optional[str] = None
    autorunner_effort_override: Optional[str] = None
    autorunner_approval_policy: Optional[str] = None
    autorunner_sandbox_mode: Optional[str] = None
    autorunner_workspace_write_network: Optional[bool] = None
    runner_stop_after_runs: Optional[int] = None


class GithubIssueRequest(Payload):
    issue: str


class GithubContextRequest(Payload):
    url: str


class GithubPrSyncRequest(Payload):
    draft: bool = True
    title: Optional[str] = None
    body: Optional[str] = None
    mode: Optional[str] = None


# Keep an explicit module-level reference so dead-code heuristics treat these
# request schemas as part of the public route contract surface.
_GITHUB_REQUEST_MODELS = (
    GithubIssueRequest,
    GithubContextRequest,
    GithubPrSyncRequest,
)


class HubPinRepoRequest(Payload):
    pinned: bool = True


class HubDestinationSetRequest(Payload):
    kind: str
    image: Optional[str] = None
    container_name: Optional[str] = Field(
        default=None,
        validation_alias=AliasChoices("container_name", "containerName", "name"),
    )
    workdir: Optional[str] = Field(
        default=None,
        validation_alias=AliasChoices("workdir", "workDir"),
    )
    profile: Optional[str] = None
    env_passthrough: Optional[List[str]] = Field(
        default=None,
        validation_alias=AliasChoices("env_passthrough", "envPassthrough"),
    )
    env: Optional[Dict[str, str]] = Field(
        default=None,
        validation_alias=AliasChoices("env", "explicit_env", "explicitEnv"),
    )
    mounts: Optional[List[Dict[str, Any]]] = None

    @model_validator(mode="before")
    @classmethod
    def _normalize_legacy_env_alias(cls, data: Any) -> Any:
        if not isinstance(data, dict):
            return data
        raw_env = data.get("env")
        if raw_env is None or not isinstance(raw_env, list):
            return data
        if "env_passthrough" in data or "envPassthrough" in data:
            return data
        normalized = dict(data)
        normalized["env_passthrough"] = raw_env
        normalized.pop("env", None)
        return normalized


class SessionStopRequest(Payload):
    session_id: Optional[str] = None
    repo_path: Optional[str] = None


class TemplateRepoSummary(ResponseModel):
    id: str
    url: str
    trusted: bool
    default_ref: str


class TemplateReposResponse(ResponseModel):
    enabled: bool
    repos: List[TemplateRepoSummary]


class TemplateRepoCreateRequest(Payload):
    id: str
    url: str
    trusted: bool = False
    default_ref: str = Field(
        default="main", validation_alias=AliasChoices("default_ref", "defaultRef")
    )


class TemplateRepoUpdateRequest(Payload):
    url: Optional[str] = None
    trusted: Optional[bool] = None
    default_ref: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("default_ref", "defaultRef")
    )


class TemplateFetchRequest(Payload):
    template: str


class TemplateFetchResponse(ResponseModel):
    content: str
    repo_id: str
    path: str
    ref: str
    commit_sha: str
    blob_sha: str
    trusted: bool
    scan_decision: Optional[Dict[str, Any]] = None


class TemplateApplyRequest(Payload):
    template: str
    at: Optional[int] = None
    next_index: bool = Field(
        default=True, validation_alias=AliasChoices("next_index", "nextIndex")
    )
    suffix: Optional[str] = None
    set_agent: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("set_agent", "setAgent")
    )
    include_provenance: bool = Field(
        default=False,
        validation_alias=AliasChoices("include_provenance", "includeProvenance"),
    )


class TemplateApplyResponse(ResponseModel):
    created_path: str
    index: int
    filename: str
    metadata: Dict[str, Any]


class SystemUpdateRequest(Payload):
    target: Optional[str] = None
    force: bool = False


class SystemUpdateTargetOption(ResponseModel):
    value: str
    label: str
    description: Optional[str] = None
    includes_web: bool = False
    restart_notice: Optional[str] = None


class SystemUpdateTargetsResponse(ResponseModel):
    targets: List[SystemUpdateTargetOption]
    default_target: str


class HubJobResponse(ResponseModel):
    job_id: str
    kind: str
    status: str
    created_at: str
    started_at: Optional[str]
    finished_at: Optional[str]
    result: Optional[Dict[str, Any]]
    error: Optional[str]


class SessionSettingsResponse(ResponseModel):
    autorunner_model_override: Optional[str]
    autorunner_effort_override: Optional[str]
    autorunner_approval_policy: Optional[str]
    autorunner_sandbox_mode: Optional[str]
    autorunner_workspace_write_network: Optional[bool]
    runner_stop_after_runs: Optional[int]


class VersionResponse(ResponseModel):
    asset_version: Optional[str]


class RunControlResponse(ResponseModel):
    running: bool
    once: bool


class RunStatusResponse(ResponseModel):
    running: bool


class RunResetResponse(ResponseModel):
    status: str
    message: str


class SessionItemResponse(ResponseModel):
    session_id: str
    repo_path: Optional[str]
    abs_repo_path: Optional[str] = None
    created_at: Optional[str]
    last_seen_at: Optional[str]
    status: Optional[str]
    alive: bool


class SessionsResponse(ResponseModel):
    sessions: List[SessionItemResponse]
    repo_to_session: Dict[str, str]
    abs_repo_to_session: Optional[Dict[str, str]] = None


class SessionStopResponse(ResponseModel):
    status: str
    session_id: str


class AppServerThreadsResponse(ResponseModel):
    file_chat: Optional[str] = None
    file_chat_opencode: Optional[str] = None
    autorunner: Optional[str] = None
    autorunner_opencode: Optional[str] = None
    corruption: Optional[Dict[str, Any]] = None


class AppServerThreadResetResponse(ResponseModel):
    status: str
    key: str
    cleared: bool


class AppServerThreadArchiveResponse(ResponseModel):
    status: str
    thread_id: str
    archived: bool


class AppServerThreadResetAllResponse(ResponseModel):
    status: str
    cleared: bool


class TokenTotalsResponse(ResponseModel):
    input_tokens: int
    cached_input_tokens: int
    output_tokens: int
    reasoning_output_tokens: int
    total_tokens: int


class RepoUsageResponse(ResponseModel):
    mode: str
    repo: str
    codex_home: str
    since: Optional[str]
    until: Optional[str]
    status: str
    events: int
    totals: TokenTotalsResponse
    latest_rate_limits: Optional[Dict[str, Any]]
    source_confidence: Optional[Dict[str, Any]] = None


class UsageSeriesEntryResponse(ResponseModel):
    key: str
    model: Optional[str]
    token_type: Optional[str]
    total: int
    values: List[int]


class UsageSeriesResponse(ResponseModel):
    mode: str
    repo: str
    codex_home: str
    since: Optional[str]
    until: Optional[str]
    status: str
    bucket: str
    segment: str
    buckets: List[str]
    series: List[UsageSeriesEntryResponse]


class SystemHealthResponse(ResponseModel):
    status: str
    mode: str
    base_path: str
    asset_version: Optional[str] = None


class SystemUpdateResponse(ResponseModel):
    status: str
    message: str
    target: str
    requires_confirmation: bool = False


class SystemUpdateStatusResponse(ResponseModel):
    status: str
    message: str


class SystemUpdateCheckResponse(ResponseModel):
    status: str
    update_available: bool
    message: str
    local_commit: Optional[str] = None
    remote_commit: Optional[str] = None


class ReviewStartRequest(Payload):
    agent: Optional[str] = None
    model: Optional[str] = None
    reasoning: Optional[str] = None
    max_wallclock_seconds: Optional[int] = Field(
        default=None,
        validation_alias=AliasChoices("max_wallclock_seconds", "maxWallclockSeconds"),
    )


class ReviewStatusResponse(ResponseModel):
    review: Dict[str, Any]


class ReviewControlResponse(ResponseModel):
    status: str
    detail: Optional[str] = None


# Ticket CRUD schemas


class TicketCreateRequest(Payload):
    agent: str = "codex"
    title: Optional[str] = None
    goal: Optional[str] = None
    body: str = ""


class TicketUpdateRequest(Payload):
    content: str  # Full markdown with frontmatter


class TicketReorderRequest(Payload):
    source_index: int = Field(
        validation_alias=AliasChoices("source_index", "sourceIndex")
    )
    destination_index: int = Field(
        validation_alias=AliasChoices("destination_index", "destinationIndex")
    )
    place_after: bool = Field(
        default=False, validation_alias=AliasChoices("place_after", "placeAfter")
    )


class TicketResponse(ResponseModel):
    path: str
    index: int
    chat_key: Optional[str] = None
    frontmatter: Dict[str, Any]
    body: str


class TicketDeleteResponse(ResponseModel):
    status: str
    index: int
    path: str


class TicketReorderResponse(ResponseModel):
    status: str
    source_index: int
    destination_index: int
    place_after: bool = False
    lint_errors: list[str] = []


class TicketBulkSetAgentRequest(Payload):
    agent: str
    range: Optional[str] = None


class TicketBulkClearModelRequest(Payload):
    range: Optional[str] = None


class TicketBulkUpdateResponse(ResponseModel):
    status: str
    updated: int
    skipped: int
    errors: list[str] = []
    lint_errors: list[str] = []


class PmaManagedThreadMessageRequest(Payload):
    model_config = ConfigDict(extra="forbid", populate_by_name=True)

    message: str
    busy_policy: Optional[Literal["queue", "interrupt", "reject"]] = Field(
        default=None, validation_alias=AliasChoices("busy_policy", "busyPolicy")
    )
    model: Optional[str] = None
    reasoning: Optional[str] = None
    defer_execution: bool = Field(
        default=False,
        validation_alias=AliasChoices("defer_execution", "deferExecution"),
    )
    notify_on: Optional[Literal["terminal"]] = Field(
        default=None, validation_alias=AliasChoices("notify_on", "notifyOn")
    )
    notify_lane: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("notify_lane", "notifyLane")
    )
    notify_once: bool = Field(
        default=True, validation_alias=AliasChoices("notify_once", "notifyOnce")
    )


class PmaManagedThreadCompactRequest(Payload):
    summary: str
    reset_backend: bool = True


class PmaManagedThreadResumeRequest(Payload):
    model_config = ConfigDict(extra="forbid", populate_by_name=True)


class PmaAutomationSubscriptionCreateRequest(Payload):
    model_config = ConfigDict(extra="allow", populate_by_name=True)

    event_types: Optional[List[str]] = Field(
        default=None, validation_alias=AliasChoices("event_types", "eventTypes")
    )
    repo_id: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("repo_id", "repoId")
    )
    run_id: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("run_id", "runId")
    )
    thread_id: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("thread_id", "threadId")
    )
    lane_id: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("lane_id", "laneId")
    )
    from_state: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("from_state", "fromState")
    )
    to_state: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("to_state", "toState")
    )
    reason: Optional[str] = None
    timestamp: Optional[str] = None


class PmaAutomationTimerCreateRequest(Payload):
    model_config = ConfigDict(extra="allow", populate_by_name=True)

    timer_type: Optional[Literal["one_shot", "watchdog"]] = Field(
        default=None, validation_alias=AliasChoices("timer_type", "timerType")
    )
    delay_seconds: Optional[int] = Field(
        default=None, validation_alias=AliasChoices("delay_seconds", "delaySeconds")
    )
    idle_seconds: Optional[int] = Field(
        default=None, validation_alias=AliasChoices("idle_seconds", "idleSeconds")
    )
    due_at: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("due_at", "dueAt")
    )
    subscription_id: Optional[str] = Field(
        default=None,
        validation_alias=AliasChoices("subscription_id", "subscriptionId"),
    )
    timer_id: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("timer_id", "timerId")
    )
    repo_id: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("repo_id", "repoId")
    )
    run_id: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("run_id", "runId")
    )
    thread_id: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("thread_id", "threadId")
    )
    lane_id: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("lane_id", "laneId")
    )
    from_state: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("from_state", "fromState")
    )
    to_state: Optional[str] = Field(
        default=None, validation_alias=AliasChoices("to_state", "toState")
    )
    reason: Optional[str] = None
    timestamp: Optional[str] = None

    @field_validator("due_at")
    @classmethod
    def _validate_due_at(cls, value: Optional[str]) -> Optional[str]:
        if value is None:
            return None
        text = value.strip()
        if not text:
            return None
        try:
            parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
        except Exception as exc:
            raise ValueError("due_at must be a valid ISO-8601 timestamp") from exc
        if parsed.tzinfo is None:
            parsed = parsed.replace(tzinfo=timezone.utc)
        return parsed.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

    @model_validator(mode="after")
    def _validate_timer_fields(self) -> "PmaAutomationTimerCreateRequest":
        if self.delay_seconds is not None and self.delay_seconds < 0:
            raise ValueError("delay_seconds must be >= 0")
        if self.idle_seconds is not None and self.idle_seconds <= 0:
            raise ValueError("idle_seconds must be > 0")
        if self.timer_type == "watchdog" and self.idle_seconds is None:
            raise ValueError("idle_seconds is required for watchdog timers")
        return self


class PmaAutomationTimerTouchRequest(Payload):
    model_config = ConfigDict(extra="allow", populate_by_name=True)

    reason: Optional[str] = None
    timestamp: Optional[str] = None


class PmaAutomationTimerCancelRequest(Payload):
    model_config = ConfigDict(extra="allow", populate_by_name=True)

    reason: Optional[str] = None
    timestamp: Optional[str] = None
