"""Pydantic models for Gremlin Autopilot API."""

from __future__ import annotations

import hashlib
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any

from pydantic import BaseModel, Field, model_validator


class JobStatus(str, Enum):
    """Status of an autopilot job."""

    PENDING = "pending"
    PLAN_READY = "plan_ready"
    EXECUTING = "executing"
    COMPLETED = "completed"
    FAILED = "failed"
    CANCELLED = "cancelled"


class ChatMode(str, Enum):
    """Interaction mode for the unified chat surface."""

    PLANNING = "planning"
    INVESTIGATION = "investigation"


class ActionStatus(str, Enum):
    """Status of a single action within a job."""

    PENDING = "pending"
    PREVIEWING = "previewing"
    APPROVED = "approved"
    EXECUTING = "executing"
    COMPLETED = "completed"
    SKIPPED = "skipped"
    FAILED = "failed"


class LLMContextLevel(str, Enum):
    """Level of data sharing with the LLM."""

    SCHEMA_ONLY = "schema_only"
    WITH_SAMPLES = "with_samples"


class AuditPrivacyMode(str, Enum):
    """Privacy mode for audit logging."""

    FULL = "full"  # All data including samples
    SUMMARY = "summary"  # Row counts, tool names, hashes only
    MINIMAL = "minimal"  # Timestamps + outcome only


# --- Request/Response Models ---


class HealthResponse(BaseModel):
    """Health check response."""

    status: str = "ok"
    version: str = "0.1.0"
    timestamp: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
    daemon_running: bool = False
    last_scan_at: str | None = None
    policy_hash: str | None = None
    next_scheduled_run: str | None = None
    schedule_profile: str | None = None
    pending_count: int = 0
    recent_24h: dict = Field(default_factory=lambda: {"success": 0, "failed": 0, "total": 0})


class ChatRequest(BaseModel):
    """Request to the chat endpoint."""

    message: str
    mode: ChatMode = ChatMode.PLANNING
    sheet_id: str | None = None
    tab_name: str | None = None
    sample_override_token: str | None = None
    org_alias: str | None = None
    conversation_id: str | None = None


class ChatSource(BaseModel):
    """Evidence source surfaced through chat."""

    source_id: str
    source_type: str
    title: str
    summary: str
    captured_at: str | None = None
    metadata: dict[str, Any] = Field(default_factory=dict)


class ChatBudgetUsed(BaseModel):
    """Budget usage metadata for chat requests."""

    live_salesforce_queries: int = 0
    live_salesforce_rows: int = 0
    live_salesforce_time_ms: int = 0
    max_live_salesforce_queries: int = 0
    max_live_salesforce_rows: int = 0
    max_live_salesforce_time_ms: int = 0
    fallback_used: bool = False
    budget_exceeded: bool = False
    notes: list[str] = Field(default_factory=list)


class ChatResponse(BaseModel):
    """Response from the chat endpoint."""

    status: JobStatus
    chat_mode: ChatMode = ChatMode.PLANNING
    message: str | None = None
    job_id: str | None = None
    actions: list["PlanAction"] | None = None
    error: str | None = None
    evidence_status: str | None = None
    confidence: float | None = None
    sources: list[ChatSource] | None = None
    tools_used: list[str] | None = None
    budget_used: ChatBudgetUsed | None = None
    follow_up_suggestions: list[str] | None = None
    conversation_id: str | None = None


class ActionPreview(BaseModel):
    """Preview of a single action in a plan."""

    action_id: str
    tool_name: str
    label: str
    args: dict[str, Any]
    preview_rows: list[dict[str, Any]] | None = None
    rows_affected: int | None = None
    warnings: list[str] = Field(default_factory=list)
    destructive: bool = False
    rollbackable: bool = True
    external_system: str | None = None
    cli_equivalent: str | None = None


class ExecuteRequest(BaseModel):
    """Request to execute a plan."""

    job_id: str
    approved_action_ids: list[str] | None = None  # None = approve all


class ExecuteResponse(BaseModel):
    """Response from execute endpoint."""

    status: JobStatus
    job_id: str
    results: list[ActionResult] | None = None
    message: str | None = None
    error: str | None = None


class ActionResult(BaseModel):
    """Result of executing a single action."""

    tool: str
    status: str
    rows_affected: int = 0
    duration_ms: int | None = None
    snapshot_id: str | None = None
    message: str | None = None


class PlanAction(BaseModel):
    """A single action in a plan."""

    tool: str
    tool_ref: str | None = None
    args: dict[str, Any]
    label: str
    risk_level: int = 0
    destructive: bool = False
    writes_external: bool = False
    rollback_supported: bool = True
    preview_trustworthy: bool = True
    cli_equivalent: str | None = None
    preview_rows: list[dict[str, Any]] | None = None
    rows_affected: int | None = None


class PlayDefinition(BaseModel):
    """Definition of a Play (workflow template)."""

    name: str
    description: str
    icon: str = "📋"
    category: str = "general"
    schema_version: str = "1"
    version: str = "1.0.0"
    parameters: list[PlayParameter] = Field(default_factory=list)
    steps: list[PlayStep] = Field(default_factory=list)

    # Credential requirements (all must be present)
    requires_credentials: list[str] = Field(default_factory=list)

    # Declared output artifacts
    outputs: list[str] = Field(default_factory=list)


class PlayParameter(BaseModel):
    """Parameter definition for a Play."""

    name: str
    type: str = "string"  # string, number, boolean, choice
    label: str
    description: str | None = None
    required: bool = False
    default: Any | None = None
    choices: list[str] | None = None  # For choice type parameters
    when: str | None = None  # Optional condition expression


class PlayStep(BaseModel):
    """A single step in a Play."""

    # Tool-based execution (existing pattern)
    tool: str | None = None
    args: dict[str, Any] = Field(default_factory=dict)

    # Command-based execution (new pattern)
    command: str | None = None

    # Shell mode (default False for security - uses shlex.split)
    shell: bool = False

    # Common fields
    label: str
    destructive: bool = False

    # Conditional execution
    when: str | None = None

    # Step identifier (for logging)
    name: str | None = None

    @model_validator(mode="after")
    def validate_execution_method(self) -> "PlayStep":
        """Ensure either tool or command is specified, not both."""
        if self.tool and self.command:
            raise ValueError("Step cannot have both 'tool' and 'command'")
        if not self.tool and not self.command:
            raise ValueError("Step must have either 'tool' or 'command'")
        return self


class PlayListResponse(BaseModel):
    """Response listing available plays."""

    plays: list[PlaySummary]


class PlaySummary(BaseModel):
    """Summary of a play for listing."""

    name: str
    description: str
    icon: str
    category: str


class BlueprintPhase(BaseModel):
    """Definition of a phase within an Architect blueprint."""

    label: str
    steps: list[PlayStep] = Field(default_factory=list)
    outputs: list[str] = Field(default_factory=list)
    approval_required: bool = False
    llm_assisted: bool = False
    prompt_template: str | None = None
    target_org: str | None = None


class BlueprintSafety(BaseModel):
    """Safety guardrails for blueprint proposals."""

    max_custom_fields: int | None = None
    max_validation_rules: int | None = None
    max_custom_properties: int | None = None
    max_workflows: int | None = None
    requires_sandbox_verification: bool = True
    rollback_plan: str | None = None


class BlueprintDefinition(BaseModel):
    """Definition of an Architect blueprint."""

    name: str
    description: str
    parameters: list[PlayParameter] = Field(default_factory=list)
    phases: dict[str, BlueprintPhase] = Field(default_factory=dict)
    requires_credentials: list[str] | dict[str, list[str]] = Field(default_factory=list)
    variants: list[str] = Field(default_factory=list)
    active_variant: str | None = None
    requires_tools: dict[str, list[str]] = Field(default_factory=dict)
    safety: BlueprintSafety | None = None
    verification_checklist: list[str] = Field(default_factory=list)


class ProjectStatus(str, Enum):
    """Status of an Architect project."""

    CREATED = "created"
    DISCOVERING = "discovering"
    DISCOVERED = "discovered"
    PROPOSING = "proposing"
    PROPOSED = "proposed"
    DEPLOYING_SANDBOX = "deploying_sandbox"
    DEPLOYED_SANDBOX = "deployed_sandbox"
    VERIFYING_SANDBOX = "verifying_sandbox"
    VERIFIED_SANDBOX = "verified_sandbox"
    DEPLOYING_PROD = "deploying_prod"
    DEPLOYED_PROD = "deployed_prod"
    FAILED = "failed"
    CANCELLED = "cancelled"


class ProjectPhaseResult(BaseModel):
    """Result of executing a project phase."""

    phase: str
    status: str
    started_at: str | None = None
    completed_at: str | None = None
    step_results: list[StepResult] = Field(default_factory=list)
    artifacts: list[str] = Field(default_factory=list)
    approval_token: str | None = None
    error: str | None = None


class ArchitectProject(BaseModel):
    """State model for an Architect project."""

    project_id: str
    blueprint_name: str
    status: ProjectStatus = ProjectStatus.CREATED
    parameters: dict[str, Any] = Field(default_factory=dict)
    project_dir: str
    phase_results: dict[str, ProjectPhaseResult] = Field(default_factory=dict)
    proposal_hash: str | None = None
    sandbox_org_alias: str
    prod_org_alias: str
    receipt_ids: dict[str, str] = Field(default_factory=dict)
    variant: str | None = None
    prod_only: bool = False

    @model_validator(mode="after")
    def validate_org_aliases(self) -> "ArchitectProject":
        if not isinstance(self.sandbox_org_alias, str) or not self.sandbox_org_alias.strip():
            raise ValueError("sandbox_org_alias must be a non-empty string")
        if not isinstance(self.prod_org_alias, str) or not self.prod_org_alias.strip():
            raise ValueError("prod_org_alias must be a non-empty string")
        if self.variant == "hubspot":
            if (
                self.sandbox_org_alias.strip() == self.prod_org_alias.strip()
                and not self.prod_only
            ):
                raise ValueError("sandbox and prod profiles must differ (or use --prod-only)")
            return self
        if self.sandbox_org_alias.strip() == self.prod_org_alias.strip():
            raise ValueError("sandbox_org_alias must differ from prod_org_alias")
        return self


class RunPlayRequest(BaseModel):
    """Request to run a Play."""

    play_name: str
    parameters: dict[str, Any] = Field(default_factory=dict)


class PairRequest(BaseModel):
    """Request to pair a remote client."""

    code: str


class PairResponse(BaseModel):
    """Response from pairing endpoint."""

    success: bool
    message: str


# --- Context Models ---


@dataclass
class SheetContext:
    """Context about the current sheet for LLM and validation."""

    sheet_id: str
    tab_name: str
    headers: list[str]
    row_count: int
    revision_id: str | None = None
    sample_hash: str | None = None
    column_types: dict[str, str] = field(default_factory=dict)
    sample_rows: list[dict[str, Any]] | None = None

    def compute_context_hash(self) -> str:
        """Compute hash for detecting stale context."""
        payload = f"{self.sheet_id}:{self.tab_name}:{self.revision_id}:{self.sample_hash}"
        return hashlib.sha256(payload.encode()).hexdigest()[:16]


@dataclass
class LLMContext:
    """Context sent to the LLM (schema-only by default)."""

    sheet_id: str
    tab_name: str
    headers: list[str]
    row_count: int
    column_types: dict[str, str] = field(default_factory=dict)
    sample_rows: list[dict[str, Any]] | None = None  # Only if consented


@dataclass
class SampleOverrideToken:
    """Token for temporary sample sharing."""

    request_id: str
    created_at: datetime
    expires_at: datetime
    sample_count: int = 5

    def is_valid(self) -> bool:
        """Check if token is still valid."""
        return datetime.now(timezone.utc) < self.expires_at


@dataclass
class UserSettings:
    """User settings for Autopilot."""

    llm_context_level: LLMContextLevel = LLMContextLevel.SCHEMA_ONLY
    sample_count: int = 5
    consent_given_at: str | None = None
    audit_privacy_mode: AuditPrivacyMode = AuditPrivacyMode.SUMMARY
    audit_retention_days: int = 90
    allowed_paths: list[str] = field(default_factory=list)
    webhook_profiles: list[dict[str, str]] = field(default_factory=list)


# --- Tool Definition ---


@dataclass
class ToolDefinition:
    """Definition of a tool available to the agent."""

    name: str
    category: str
    description: str
    parameters: dict[str, Any]
    cli_equivalent: str | None = None
    destructive: bool = False
    preview_trustworthy: bool = True
    writes_external: bool = False
    rollback_supported: bool = True
    requires_credentials: list[str] = field(default_factory=list)


# --- Play Execution Results ---


class StepResult(BaseModel):
    """Result of executing a single play step."""

    name: str
    status: str  # success, failed, skipped, dry_run
    duration_ms: int
    artifacts: list[str] = Field(default_factory=list)
    error: str | None = None
    log_file: str | None = None


class PlayRunSummary(BaseModel):
    """Summary of a play execution run."""

    play: str
    started_at: str
    completed_at: str
    status: str  # success, failed, partial
    output_dir: str
    failed_step: str | None = None
    parameters: dict[str, Any]
    steps: list[StepResult]
    artifacts: list[str]
    warnings: list[str] = Field(default_factory=list)


# --- Exceptions ---


class AutopilotError(Exception):
    """Base exception for Autopilot errors."""

    pass


class StaleContextError(AutopilotError):
    """Raised when sheet context has changed since plan creation."""

    pass


class ConsentError(AutopilotError):
    """Raised when consent is required but not given."""

    pass


class ValidationError(AutopilotError):
    """Raised when validation fails."""

    pass
