"""比价记录上报 / 读取 schemas。

约定同 welfare:字段 snake_case、金额存整数(分)、时间 ISO 8601。

上报请求(ComparisonRecordIn)的字段刻意对齐 pricebot 协议
(docs/main/02_api_protocol.md 的 calibration + done.params.comparison_results),
让客户端把 Phase1 的 calibration 和 done 帧的 params 字段**零翻译**直接映射上来,
server 端负责拆成结构化列(best_*/saved/is_source_best 由 comparison_results 派生)。
"""
from __future__ import annotations

from datetime import datetime

from pydantic import BaseModel, ConfigDict, Field, field_validator


# ===== 上报请求 =====

class ComparisonItemIn(BaseModel):
    """下单菜品(来自 calibration.items)。"""

    name: str
    qty: int = 1
    # specs 仅供比价记录展示(admin-web 详情 / app 记录页都按字符串数组渲染并 join)。
    # pricebot(2026-06-27 嵌套规格统一)起 calibration 的 specs 可能是规格对象
    # [{name, qty, sub_specs}] 而非字符串 → 下面的 before-validator 统一拍平成可读字符串,
    # 兼容新旧两种输入、保持 list[str] 契约不变(下游零改动)。
    # ⚠️ 两个坑都踩过, 必须"拍平"而非别的: ① 直接声明 list[str] 不拍平 → 对象 specs 整条
    # 422 被拒、不入库(同下方 platform_results list→dict 同类事故); ② 仅放宽成裸 list 又会让
    # 下游 join 出 "[object Object]"/对象 toString 的乱码。
    specs: list[str] | None = None

    @field_validator("specs", mode="before")
    @classmethod
    def _flatten_specs(cls, v: object) -> object:
        """pricebot 规格对象 [{name, qty, sub_specs}] → 可读字符串数组; 字符串元素原样保留;
        非 list 原样交还(让 pydantic 照常报类型错)。嵌套规格拼成 '主项(子1,子2)'。"""
        if not isinstance(v, list):
            return v
        out: list[str] = []
        for it in v:
            if isinstance(it, str):
                s = it
            elif isinstance(it, dict):
                name = str(it.get("name") or "").strip()
                subs = [str(x).strip() for x in (it.get("sub_specs") or []) if str(x).strip()]
                s = f"{name}({','.join(subs)})" if name and subs else (name or ",".join(subs))
            else:
                continue
            if s:
                out.append(s)
        return out


class AppliedCouponIn(BaseModel):
    """单笔已用优惠(来自 comparison_results[].applied_coupons)。amount 单位:元、正数。"""

    name: str
    amount: float


class ComparisonResultIn(BaseModel):
    """逐平台对比项(来自 done.params.comparison_results)。price 单位:元。"""

    platform_id: str | None = None
    platform_name: str | None = None
    package: str | None = None
    price: float | None = None
    is_source: bool = False
    rank: int | None = None
    # 该平台本单用红包省的**纯红包优惠额**(元, 不含配送费/代金券/满减)。None=没用/没抠到。
    # 必须显式声明: 落库走 model_dump(), pydantic 默认丢未知字段, 不声明这行会被悄悄吞掉。
    # 各平台抠到红包即带值(2026-06 起源平台 Phase1 意图识别也抠, 当前仅淘宝源)。见 pricebot 侧 比价红包额留痕-实现方案.md。
    coupon_saved: float | None = None
    # 优惠**来源名**(展示用, best-effort): 美团"外卖大额神券"/京东"百亿补贴"/淘宝"平台红包"。
    # None=没抠到 → 前端走通用"红包"。同样必须显式声明否则上报边界被 pydantic 静默丢弃(pricebot#38 引入)。
    coupon_name: str | None = None
    # 多券明细 [{name, amount}](全口径: 平台红包+商家券+满减+配送减免, amount 单位元正数)。
    # 跟 coupon_saved 并存, 是更丰富的明细; 空=没抠到 → 前端回退单券路径。
    # 必须显式声明: 落库走 model_dump(), pydantic 默认丢未知字段, 不声明这行会被悄悄吞掉。
    applied_coupons: list[AppliedCouponIn] = Field(default_factory=list)
    # 逐平台状态(客户端上报前把 done.params 的 comparison_results + platform_results.status 合并写入):
    #   success / below_minimum / store_closed / store_not_found / items_not_found / unsupported / failed。
    # 「比价记录」页据此对无价平台显示对应文案(未满起送价/门店打烊/店家未入驻/比价失败)。
    # 必须显式声明: 落库走 model_dump(), 不声明会被 pydantic 静默丢弃 → 记录页拿不到 status。
    status: str | None = None
    # 门店打烊原因(price 为 None 时带): 与 status="store_closed" 等价的更早信号, 一并落库供前端兜底判打烊。
    store_closed: str | None = None


class ComparisonRecordIn(BaseModel):
    # 幂等键:同一用户同一 trace_id 重复上报只保留一条(覆盖)
    trace_id: str = Field(..., min_length=1, description="pricebot 侧 trace_id")
    business_type: str = Field("food", description="food / ecom / coupon")
    device_id: str | None = None

    # 源平台(来自 calibration)
    store_name: str | None = Field(None, description="店铺名(外卖)")
    source_platform_id: str | None = None
    source_platform_name: str | None = None
    source_package: str | None = None
    source_price: float | None = Field(None, description="源平台到手价(元)")

    # 明细
    items: list[ComparisonItemIn] = Field(default_factory=list)
    comparison_results: list[ComparisonResultIn] = Field(default_factory=list)
    # 逐平台结局摘要(含失败平台的细分原因 status: store_not_found/items_not_found/below_minimum/
    # unsupported/...)。来自 done.params.platform_results,客户端透传;落 raw_payload(不单列),
    # admin「卡在哪一步」从这里读。dict{platform_id: {...}} 宽松存(结构由 pricebot 定——是
    # 按平台 id 索引的 dict 而非 list,只作 debug 展示)。
    # ⚠️ 早先误声明成 list → 每条成功记录 422 被拒、记录页只剩失败/终止
    # (2026-06-17 引入 platform_results 上报后复现);改 dict 后修复。
    platform_results: dict = Field(default_factory=dict)
    skipped_dish_count: int | None = None
    skipped_dish_names: list[str] = Field(default_factory=list)
    total_dish_count: int | None = None

    information: str | None = Field(None, description="done 帧文案,留存备查")
    # 不传则服务端按 comparison_results 派生(有非源有效价=success,否则 failed)
    status: str | None = Field(None, description="success / failed,可不传由服务端派生")
    # 最优平台商家/商品深链(客户端从 collectedLinks[best_index] 取);「再次比价」直达用
    best_deeplink: str | None = Field(None, description="最优平台深链,再次比价直达")
    # pricebot done.params.trace_url 原样上报,落库供记录页「复制调试链接」(dir 名含落盘
    # 时分秒前端拼不出,必须由后端透传)。
    trace_url: str | None = Field(None, description="本次比价公网调试链接")
    total_ms: int | None = Field(None, description="整场比价墙钟耗时(ms)")

    # ===== debug 维度(客户端采集上报;旧客户端不带 → None。仅 admin 比价记录页用)=====
    # 必须显式声明,否则 model_dump() 落 raw_payload 时被 pydantic 静默丢弃(同上面 coupon_saved 的坑)。
    device_model: str | None = None
    device_manufacturer: str | None = None
    rom_vendor: str | None = Field(None, description="vivo/OPPO/Xiaomi/HUAWEI/HONOR/samsung")
    rom_name: str | None = Field(None, description="OriginOS/ColorOS/HyperOS/MIUI/HarmonyOS...")
    rom_version: int | None = None
    android_version: str | None = None
    android_sdk: int | None = None
    app_version: str | None = None
    app_version_code: int | None = None
    source_app_version: str | None = Field(None, description="被操控的源平台 App 版本")
    longitude: float | None = None
    latitude: float | None = None
    total_ms: int | None = Field(None, description="整场比价墙钟耗时(ms)")
    step_count: int | None = Field(None, description="agent loop 总步数")


# ===== 读取出参 =====

class ComparisonRecordOut(BaseModel):
    """列表项:结构化概要字段(不含 raw_payload,减小列表 payload)。"""

    model_config = ConfigDict(from_attributes=True)

    id: int
    business_type: str
    trace_id: str
    # 公网调试链接;仅当 user.debug_trace_enabled 时由端点填充,否则端点层置 None(权限闸)。
    trace_url: str | None = None
    source_platform_id: str | None = None
    source_platform_name: str | None = None
    source_package: str | None = None
    source_price_cents: int | None = None
    best_platform_id: str | None = None
    best_platform_name: str | None = None
    best_price_cents: int | None = None
    best_deeplink: str | None = None
    saved_amount_cents: int | None = None
    is_source_best: bool | None = None
    store_name: str | None = None
    total_dish_count: int | None = None
    skipped_dish_count: int | None = None
    status: str
    information: str | None = None
    items: list = []
    comparison_results: list = []
    skipped_dish_names: list = []
    total_ms: int | None = None
    # 「已下单」(店级):该店名在该用户真实下单(source='compare')里出现过即 True。
    # 由 list_records 动态算出挂在 ORM 实例上(非 DB 列),from_attributes 读出;缺省 False。
    ordered: bool = False
    # 「本次比价看广告赚的金币」(feed_ad_reward,按 trace_id 聚合 granted 金币;非 DB 列,
    # list_records/get_record 动态挂在 ORM 实例上,from_attributes 读出)。0=没赚到 → 记录页不显示金币标。
    ad_coins_earned: int = 0
    created_at: datetime


class ComparisonRecordDetailOut(ComparisonRecordOut):
    """详情:在概要之上额外带 raw_payload 全量。"""

    raw_payload: dict | None = None


class ComparisonRecordPage(BaseModel):
    items: list[ComparisonRecordOut]
    next_cursor: int | None = Field(None, description="下一页游标(末条 id);为空表示到底")


class ComparisonRecordCreatedOut(BaseModel):
    id: int = Field(..., description="写入(或已存在)的记录 id")


class CompareStatsOut(BaseModel):
    """「我的」页省钱战绩卡(比价口径)聚合。"""

    compare_count: int = Field(..., description="累计完成比价次数(status='success' 记录数)")
    discovered_saved_cents: int = Field(..., description="累计发现可省额(分;各成功比价 源价−最低价 之和)")


# ===== 比价战绩里程碑(福利页「记录比价战绩」)=====

class MilestoneStateOut(BaseModel):
    milestone: int = Field(..., description="档位序号 1-based(= 解锁所需的成功比价次数)")
    coin: int = Field(..., description="该档应发金币额(产品规则值);当前领取暂不真发, 见 compare-milestone-claim 文档")
    state: str = Field(..., description="claimed(已领) / active(可领) / locked(未解锁)")


class MilestoneStatusOut(BaseModel):
    success_count: int = Field(..., description="累计成功比价次数(解锁进度)")
    claimable_count: int = Field(..., description="当前可领(active)档数")
    milestones: list[MilestoneStateOut]


class MilestoneClaimResultOut(BaseModel):
    milestone: int = Field(..., description="本次领取的档位序号")
    coin_awarded: int = Field(..., description="本次发放金币")
    coin_balance: int = Field(..., description="领奖后金币余额")
