from __future__ import annotations

from datetime import UTC, datetime
from decimal import Decimal
from typing import Any, Literal

from pydantic import BaseModel, Field, field_validator, model_validator

from app.schemas.savings import ProfileBaselineSnapshotRead

SavingsBaselineDimension = Literal[
    "employee",
    "department",
    "expense_type",
    "city",
    "project",
    "workflow",
    "supplier",
]
SavingsInsightType = Literal[
    "budget_forecast_variance",
    "repeated_small_expense_pattern",
    "historical_price_deviation",
    "anomaly_driver_attribution",
    "policy_simulation_candidate",
]


class SavingsAnalysisQualityIssue(BaseModel):
    code: str
    message: str
    severity: Literal["info", "warning", "error"] = "warning"
    dimension_type: str | None = None
    dimension_id: str | None = None
    metadata: dict[str, Any] = Field(default_factory=dict)


class SavingsWindowRequest(BaseModel):
    request_id: str = Field(min_length=8, max_length=120)
    window_start: datetime
    window_end: datetime
    as_of: datetime

    @field_validator("request_id", mode="before")
    @classmethod
    def normalize_request_id(cls, value: Any) -> str:
        return str(value or "").strip()

    @field_validator("window_start", "window_end", "as_of")
    @classmethod
    def require_timezone(cls, value: datetime) -> datetime:
        if value.tzinfo is None or value.utcoffset() is None:
            raise ValueError("分析窗口时间必须包含时区。")
        return value.astimezone(UTC)

    @model_validator(mode="after")
    def validate_window(self) -> SavingsWindowRequest:
        if self.window_end < self.window_start:
            raise ValueError("分析窗口结束时间不能早于开始时间。")
        if self.as_of < self.window_end:
            raise ValueError("分析截止时间不能早于窗口结束时间。")
        return self


class SavingsBaselineGenerateRequest(SavingsWindowRequest):
    dimensions: list[SavingsBaselineDimension] = Field(
        default_factory=lambda: [
            "employee",
            "department",
            "expense_type",
            "city",
            "project",
            "workflow",
            "supplier",
        ],
        min_length=1,
        max_length=7,
    )
    minimum_complete_samples: int = Field(default=5, ge=2, le=100)

    @field_validator("dimensions")
    @classmethod
    def deduplicate_dimensions(
        cls,
        value: list[SavingsBaselineDimension],
    ) -> list[SavingsBaselineDimension]:
        return list(dict.fromkeys(value))


class SavingsBaselineGenerationRead(BaseModel):
    request_id: str
    request_fingerprint: str
    tenant_id: str
    data_scope: str
    snapshots: list[ProfileBaselineSnapshotRead] = Field(default_factory=list)
    quality_issues: list[SavingsAnalysisQualityIssue] = Field(default_factory=list)
    source_claim_count: int = 0
    source_item_count: int = 0
    source_workflow_cycle_count: int = 0
    replayed: bool = False
    generated_at: datetime


class SavingsInsightAnalyzeRequest(SavingsWindowRequest):
    small_amount_threshold: Decimal = Field(
        default=Decimal("200.00"),
        gt=0,
        max_digits=16,
        decimal_places=2,
    )
    minimum_repeat_count: int = Field(default=3, ge=3, le=20)
    price_deviation_ratio: Decimal = Field(
        default=Decimal("1.2500"),
        gt=1,
        le=10,
        max_digits=8,
        decimal_places=4,
    )


class SavingsInsightEvidenceRead(BaseModel):
    evidence_role: str
    resource_type: str
    resource_id: str
    content_hash: str
    occurred_at: datetime
    metadata: dict[str, Any] = Field(default_factory=dict)


class SavingsInsightCandidateRead(BaseModel):
    candidate_key: str
    insight_type: SavingsInsightType
    title: str
    description: str
    dimension_json: dict[str, Any] = Field(default_factory=dict)
    evidence: list[SavingsInsightEvidenceRead] = Field(default_factory=list)
    evidence_sufficient_for_signal: bool
    data_quality_status: Literal["complete", "partial", "insufficient"]
    quality_issues: list[SavingsAnalysisQualityIssue] = Field(default_factory=list)
    exposure_amount: Decimal | None = None
    exposure_meaning: str
    currency: str | None = None
    estimated_savings: Decimal | None = None
    monetization_status: Literal[
        "eligible",
        "withheld_no_counterfactual",
        "unavailable",
    ]
    baseline_snapshot_id: str | None = None


class SavingsInsightAnalysisRead(BaseModel):
    request_id: str
    request_fingerprint: str
    tenant_id: str
    data_scope: str
    candidates: list[SavingsInsightCandidateRead] = Field(default_factory=list)
    quality_issues: list[SavingsAnalysisQualityIssue] = Field(default_factory=list)
    source_claim_count: int = 0
    source_item_count: int = 0
    created_opportunity_ids: list[str] = Field(default_factory=list)
    monetized_opportunity_count: int = 0
    generated_at: datetime
