"""Shared Pydantic response shapes (spec §50 API)."""
from __future__ import annotations

from datetime import date
from typing import Optional

from pydantic import BaseModel, Field


class MetricOut(BaseModel):
    key: str
    value: Optional[float]
    display: str
    status: str
    applicability: str
    formula_version: str
    note: Optional[str] = None


class ScoreOut(BaseModel):
    quality_score: Optional[float]
    financial_health_score: Optional[float]
    growth_score: Optional[float]
    competitive_advantage_score: Optional[float]
    valuation_score: Optional[float]
    overall_score: Optional[float]
    risk_score: Optional[float]
    recommendation: Optional[str]
    # AUDIT FIX (final master pass, §104.20 -- "API contracts must match implementation").
    # `recommendation_confidence` was declared here and ALWAYS serialised as None, because nothing
    # anywhere computes it (`RecommendationResult` has no confidence field at all). A field that
    # is permanently null is a false contract: a client cannot tell "not available for this
    # security" from "never available for any security". Removed from the response schema.
    # The database column `scores.recommendation_confidence` is deliberately NOT dropped -- this
    # project's migrations are additive-only and dropping a column to tidy an API response is the
    # wrong trade -- it is documented as unused in app/models/derived.py. If a
    # recommendation-level confidence is ever defined, it gets a formula, a test, and only then a
    # schema field again.
    # AUDIT FIX (StockLab overhaul, Part A1): these two were the actual gap docs/AUDIT_GUI.md §4
    # documented -- compute_confidence_score()/compute_data_quality_score() existed and were
    # tested but nothing populated these on a real ScoreOut response. Now sourced from
    # Score.confidence_score/data_quality_score (app/models/derived.py), themselves populated by
    # app/workers/recompute.py.
    confidence_score: Optional[float] = None
    data_quality_score: Optional[float] = None
    weights_used: Optional[dict] = None
    triggers_fired: Optional[list[str]] = None
    # AUDIT FIX (final master pass, §9 "score must be explainable" + §40 data lineage).
    # `scores.subscore_detail` is a JSON column that existed with the comment "per spec §49 audit
    # trail" and was never written and never read. It now carries the full explainability payload
    # and is exposed here, so a client can answer "why is this 88?" without a second request:
    #   missing_weight                  -- fraction of pillar weight that had NO value and was
    #                                      renormalised away. §9 requires this be visible so
    #                                      renormalisation cannot hide missing information.
    #   subscores_included_in_overall   -- which pillars actually contributed
    #   max_missing_weight_allowed      -- the cap beyond which Overall refuses to extrapolate
    #   pillars.<name>.metrics_included / metrics_excluded / peer_group_tier / weight / status
    #   model_version / model_fingerprint / metric_formula_version  (§39)
    subscore_detail: Optional[dict] = None


class ValuationOut(BaseModel):
    wacc: Optional[float]
    bear_fair_value: Optional[float]
    base_fair_value: Optional[float]
    bull_fair_value: Optional[float]
    weighted_fair_value: Optional[float]
    fair_value_confidence: Optional[float]
    price_at_calculation: Optional[float]
    margin_of_safety: Optional[float]
    strong_buy_price: Optional[float]
    buy_price: Optional[float]
    overvalued_price: Optional[float]
    expected_return_5y: Optional[float]
    business_profile: Optional[str]


class CompanySummaryOut(BaseModel):
    security_id: str
    ticker: str
    company_name: str
    country: Optional[str]
    sector: Optional[str]
    industry: Optional[str]
    is_demo: bool = False


class CompanyPageOut(BaseModel):
    company: CompanySummaryOut
    score: Optional[ScoreOut]
    valuation: Optional[ValuationOut]
    why: list[str] = []
    risks: list[str] = []
    metrics: list[MetricOut] = []
    as_of: Optional[date] = None


class ScreenFilter(BaseModel):
    metric: str = Field(max_length=48)
    op: str  # gt|gte|lt|lte|eq|between
    value: float
    value2: Optional[float] = None  # for "between"
    relative: str = "absolute"  # absolute|industry_percentile|historical_percentile|peer_percentile


class ScreenUniverse(BaseModel):
    region: Optional[str] = None
    country: Optional[str] = None
    sector: Optional[str] = None
    market_cap_min: Optional[float] = None
    market_cap_max: Optional[float] = None
    include_demo: bool = False


class ScreenRequest(BaseModel):
    universe: ScreenUniverse = ScreenUniverse()
    logic: str = "AND"
    filters: list[ScreenFilter] = Field(default=[], max_length=20)
    sort_by: str = "overall_score"
    sort_direction: str = "desc"
    # AUDIT FIX (StockLab overhaul, performance audit, docs/AUDIT_PERFORMANCE.md finding #1): no
    # upper bound existed here before this pass -- a client (or an unrate-limited abusive caller,
    # see docs/AUDIT_SECURITY.md finding #3) could request limit=100000 and force
    # app/api/v1/screeners.py::run_screen's per-row Score/Valuation/company_summary lookups to run
    # tens of thousands of times in a single request. le=200 matches the existing cap already used
    # by app/api/v1/rankings.py::get_ranking, which had this constraint from the start.
    limit: int = Field(50, le=200)
    offset: int = Field(0, ge=0)


class ScreenResultRow(BaseModel):
    company: CompanySummaryOut
    overall_score: Optional[float]
    recommendation: Optional[str]
    weighted_fair_value: Optional[float]
    margin_of_safety: Optional[float]
    # AUDIT FIX (StockLab overhaul, Part A1): "Company Page / Dashboard / Screener, where
    # applicable" -- added here since both callers (screeners.py, rankings.py) already have the
    # Score row in hand (no new query), so a low-confidence/low-data-quality row can be flagged in
    # list views too, not just after clicking through to the Company Page.
    confidence_score: Optional[float] = None
    data_quality_score: Optional[float] = None


class ScreenResponse(BaseModel):
    total: int
    results: list[ScreenResultRow]
