from __future__ import annotations

from datetime import datetime, timezone
from enum import Enum
from typing import Literal

from pydantic import BaseModel, ConfigDict, Field, model_validator


class ModuleId(str, Enum):
    FINANCE = "finance"
    HEALTH = "health"
    AUTOSHORTS = "autoshorts"
    FAMILY = "family"
    SYSTEM = "system"


class ModuleStatus(str, Enum):
    OK = "ok"
    ATTENTION = "attention"
    DEGRADED = "degraded"
    OFFLINE = "offline"


class Sensitivity(str, Enum):
    PUBLIC = "public"
    INTERNAL = "internal"
    SENSITIVE = "sensitive"
    CRITICAL = "critical"


class DisplayPolicy(str, Enum):
    HIDDEN = "hidden"
    SUMMARY = "summary"
    EXACT = "exact"


class Severity(str, Enum):
    INFO = "info"
    SUCCESS = "success"
    WARNING = "warning"
    ERROR = "error"
    CRITICAL = "critical"


class ActionMode(str, Enum):
    LINK_ONLY = "link_only"
    READ_ONLY = "read_only"
    PREVIEW_REQUIRED = "preview_required"
    CONFIRM_REQUIRED = "confirm_required"
    BLOCKED_IN_MVP = "blocked_in_mvp"


class SourceType(str, Enum):
    API = "api"
    HTTP_API = "http_api"
    LOCAL_PROBE = "local_probe"
    SQLITE_SUMMARY = "sqlite_summary"
    FILESYSTEM_METADATA = "filesystem_metadata"
    STATIC_CONFIG = "static_config"
    MOCK = "mock"
    SAFE_MARKER = "safe_marker"


class ReportKind(str, Enum):
    HTML = "html"
    PDF = "pdf"
    MARKDOWN = "markdown"
    JSON = "json"
    EXTERNAL = "external"


class ApprovalStatus(str, Enum):
    PENDING = "pending"
    BLOCKED = "blocked"
    APPROVED = "approved"
    REJECTED = "rejected"


class StrictBaseModel(BaseModel):
    model_config = ConfigDict(extra="forbid", use_enum_values=True)


class SourceHealth(StrictBaseModel):
    reachable: bool
    latency_ms: int | None = Field(default=None, ge=0)
    source_type: SourceType
    version: str | None = None
    contract_version: str
    last_success_at: datetime | None = None
    last_attempt_at: datetime | None = None
    last_error_redacted: str | None = None
    stale: bool


class ActionDescriptor(StrictBaseModel):
    label: str
    href: str | None
    mode: ActionMode
    sensitivity: Sensitivity
    blocked_reason: str | None = None
    preview_required: bool
    confirm_required: bool
    audit_required: bool

    @model_validator(mode="after")
    def enforce_mvp_action_rules(self) -> "ActionDescriptor":
        if self.mode in {ActionMode.PREVIEW_REQUIRED, ActionMode.CONFIRM_REQUIRED}:
            if not (self.preview_required and self.confirm_required and self.audit_required):
                raise ValueError("preview/confirm actions require preview, confirm and audit flags")
        if self.mode not in {ActionMode.LINK_ONLY, ActionMode.READ_ONLY, ActionMode.BLOCKED_IN_MVP}:
            raise ValueError("MVP responses may only use link_only, read_only or blocked_in_mvp actions")
        return self


class KpiItem(StrictBaseModel):
    key: str
    label: str
    value: str | int | float | bool | None
    unit: str | None = None
    severity: Severity
    sensitivity: Sensitivity
    display_policy: DisplayPolicy
    help_text: str | None = None


class AttentionItem(StrictBaseModel):
    id: str
    module_id: ModuleId
    title: str
    message: str
    severity: Severity
    sensitivity: Sensitivity
    created_at: datetime
    action: ActionDescriptor | None = None
    dedupe_key: str


class ActivityEvent(StrictBaseModel):
    id: str
    module_id: ModuleId
    title: str
    message: str
    severity: Severity
    sensitivity: Sensitivity
    occurred_at: datetime
    source_type: SourceType
    redacted: bool


class ApprovalItem(StrictBaseModel):
    id: str
    module_id: ModuleId
    title: str
    status: ApprovalStatus
    severity: Severity
    sensitivity: Sensitivity
    requested_at: datetime
    preview_action: ActionDescriptor
    confirm_action: ActionDescriptor


class ReportLink(StrictBaseModel):
    id: str
    module_id: ModuleId
    title: str
    kind: ReportKind
    href: str | None
    sensitivity: Sensitivity
    display_policy: DisplayPolicy
    generated_at: datetime | None = None
    blocked_reason: str | None = None


class RedactionPolicy(StrictBaseModel):
    default_display_policy: DisplayPolicy
    allow_exact_finance_values: bool = False
    allow_health_detail_links: bool = False
    forbidden_fields: list[str]
    log_forbidden_fields: list[str]
    demo_mode: bool = True


class DemoInfo(StrictBaseModel):
    mode: Literal["demo_mock", "demo_mixed_readonly", "mixed_readonly_handoff"]
    actions_enabled: bool = False
    modules: list[Literal["finance", "health", "autoshorts", "family", "system"]]
    runtime_access: Literal["disabled", "optional_readonly"]
    legacy_links: dict[Literal["finance", "health", "autoshorts", "family"], Literal["configured", "configured_protected", "not_configured", "blocked", "locked_by_policy"]]

    @model_validator(mode="after")
    def enforce_demo_info_safety(self) -> "DemoInfo":
        if self.actions_enabled:
            raise ValueError("operator demo actions must remain disabled")
        return self


class ModuleRegistryEntry(StrictBaseModel):
    module_id: ModuleId
    title: str
    enabled: bool
    sensitivity: Sensitivity
    source_type: SourceType
    base_url_env: str | None = None
    legacy_url_env: str | None = None
    runtime_base_env: str | None = None
    contract_version: str
    default_display_policy: DisplayPolicy
    actions_enabled: bool
    notes: list[str] = Field(default_factory=list)

    @model_validator(mode="after")
    def enforce_registry_safety(self) -> "ModuleRegistryEntry":
        if self.actions_enabled:
            raise ValueError("actions_enabled must remain false in MVP")
        if self.module_id in {ModuleId.FINANCE, ModuleId.HEALTH} and self.default_display_policy == DisplayPolicy.EXACT:
            raise ValueError("finance/health registry entries cannot default to exact display")
        return self


class RuntimeConfig(StrictBaseModel):
    jarvis_env: Literal["development", "production"] = "development"
    jarvis_demo_mode: bool = True
    jarvis_public_base_url: str = "https://jarvis.local"
    jarvis_gateway_host: str = "127.0.0.1"
    jarvis_gateway_port: int = Field(default=8080, ge=1, le=65535)
    jarvis_demo_api_port: int = Field(default=8010, ge=1, le=65535)
    jarvis_demo_dashboard_port: int = Field(default=5173, ge=1, le=65535)
    jarvis_operator_demo_profile: Literal["demo_mock", "demo_mixed_readonly", "mixed_readonly_handoff"] = "demo_mock"
    jarvis_dev_cors_origins: list[str] = Field(default_factory=lambda: ["http://127.0.0.1:5174", "http://localhost:5174"])
    finance_adapter_mode: Literal["mock", "live_readonly", "disabled"] = "mock"
    finance_api_base_url: str | None = None
    finance_legacy_dashboard_url: str | None = None
    finance_read_timeout_seconds: float = Field(default=3, ge=0.1, le=30)
    health_adapter_mode: Literal["mock", "local_probe", "safe_marker", "disabled"] = "mock"
    health_runtime_base: str | None = None
    health_safe_status_marker_path: str | None = None
    health_safe_status_max_age_hours: int = Field(default=72, ge=1, le=8760)
    health_legacy_dashboard_url: str | None = None
    autoshorts_adapter_mode: Literal["mock", "disabled", "local_probe", "live_readonly"] = "mock"
    autoshorts_api_base_url: str | None = None
    autoshorts_runtime_base: str | None = None
    autoshorts_legacy_dashboard_url: str | None = None
    family_legacy_dashboard_url: str | None = None
    jarvis_operator_public_origin: str | None = None
    jarvis_allow_tailnet_links: bool = False
    redaction_default_policy: DisplayPolicy = DisplayPolicy.SUMMARY
    allow_exact_finance_values: bool = False
    allow_health_detail_links: bool = False
    log_level: str = "INFO"

    @model_validator(mode="after")
    def enforce_finance_adapter_safety(self) -> "RuntimeConfig":
        if self.allow_exact_finance_values:
            raise ValueError("ALLOW_EXACT_FINANCE_VALUES=1 is forbidden in MVP")
        if self.finance_adapter_mode == "live_readonly" and not self.finance_api_base_url:
            raise ValueError("FINANCE_API_BASE_URL is required for live_readonly mode")
        return self


class ErrorEnvelope(StrictBaseModel):
    code: str
    message: str
    severity: Severity
    module_id: ModuleId | None = None
    retryable: bool
    details_redacted: bool
    occurred_at: datetime


class ModuleSnapshot(StrictBaseModel):
    module_id: ModuleId
    title: str
    status: ModuleStatus
    sensitivity: Sensitivity
    display_policy: DisplayPolicy
    last_success_at: datetime | None
    last_attempt_at: datetime | None
    stale_after_seconds: int = Field(ge=0)
    degraded_reason: str | None = None
    source_health: SourceHealth
    kpis: list[KpiItem] = Field(default_factory=list, max_length=3)
    attention_items: list[AttentionItem] = Field(default_factory=list)
    links: list[ActionDescriptor] = Field(default_factory=list)
    primary_action: ActionDescriptor | None = None
    contract_version: str
    approval_items: list[ApprovalItem] = Field(default_factory=list)
    reports: list[ReportLink] = Field(default_factory=list)

    @model_validator(mode="after")
    def enforce_module_display_policy(self) -> "ModuleSnapshot":
        if self.module_id == ModuleId.HEALTH and self.display_policy == DisplayPolicy.EXACT:
            raise ValueError("health cannot use exact display in MVP")
        if self.module_id == ModuleId.FINANCE and self.display_policy == DisplayPolicy.EXACT:
            raise ValueError("finance cannot use exact display in global overview")
        return self


class OverviewResponse(StrictBaseModel):
    contract_version: str
    generated_at: datetime
    status: ModuleStatus
    modules: list[ModuleSnapshot] = Field(default_factory=list, max_length=5)
    attention_items: list[AttentionItem] = Field(default_factory=list, max_length=5)
    activity: list[ActivityEvent] = Field(default_factory=list)
    approvals: list[ApprovalItem] = Field(default_factory=list)
    reports: list[ReportLink] = Field(default_factory=list)
    redaction_policy: RedactionPolicy
    errors: list[ErrorEnvelope] = Field(default_factory=list)


def utcnow() -> datetime:
    return datetime.now(timezone.utc)
