"""平台店铺表(store_mapping)—— 跨平台"同一家店"的 id/名 映射资产层。

每完成一次淘宝比价(在目标淘宝店通过 更多操作→分享→复制链接 拿到分享短链、
HTTP 解析出 shopId 后),pricebot server→server 内部上报落这里一行。**与登录无关、
不依赖客户端鉴权**(比价透传链路当前不鉴权,user_id 客户端带上时一并记)。

与 price_observation 的区别:
- price_observation:平台/门店视角的**价格事实**(某店这单多少钱)。
- store_mapping:平台/门店视角的**身份映射**(同一家物理店在 淘宝/美团/京东 各自的
  店铺 id 与店名)。是未来"我见过这家店→跳过重新搜索/匹配"的源头。两表独立。

「先存下来、用法后说」:列尽量铺全(各平台 id/名 + 地理 + 溯源 + 淘宝/美团原料 URL),
attrs(JSONB)兜底存灵活明细,免得每多记一个字段就迁移 schema。

⚠️ 数据质量:跨平台"同一家店"的连接来自 agent 的 LLM 店铺匹配,匹配错则一行里连错店。
本表是 append-only 原始记录(每比价一行、trace_id 幂等防重试重复),清洗/归一二期再做。
已接通**淘宝**(id_taobao=shopId)、**美团**(meituan_poi_id_str,非稳定主键、单列存)、
**京东**(id_jd=storeId + jd_vender_id,均稳定数字主键;3.cn 短链反查)。
"""
from __future__ import annotations

from datetime import datetime

from sqlalchemy import (
    JSON,
    DateTime,
    Float,
    Integer,
    String,
    Text,
    UniqueConstraint,
    func,
)
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column

from app.db.base import Base

# PG 上用 JSONB,SQLite(本地/测试)退化为通用 JSON(同 price_observation / comparison_record)。
_JSON = JSON().with_variant(JSONB(), "postgresql")


class StoreMapping(Base):
    __tablename__ = "store_mapping"
    __table_args__ = (
        # 一次比价(trace)只记一条:pricebot 重试 / 客户端 replay 重复上报时幂等去重。
        # 一次淘宝比价 = 一个目标淘宝店 → 一行映射(源 + 各平台身份压在同一行)。
        UniqueConstraint("trace_id", name="uq_store_mapping_trace"),
    )

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)

    # ===== 跨平台店铺身份(同一家物理店在各平台的 id/名;按比价角色稀疏填充)=====
    # id_taobao = 分享短链解析出的 shopId(淘宝当目标、走完取 id 流程才有);
    # name_taobao = 店铺页 a11y content_desc "店铺标题：xxx" 剥前缀。
    id_taobao: Mapped[str | None] = mapped_column(String(64), index=True, nullable=True)
    name_taobao: Mapped[str | None] = mapped_column(String(128), nullable=True)
    # 美团:无同款 share→稳定id 机制(poi_id_str 每次变,见下方 meituan_poi_id_str),id_meituan
    # 留给将来 CPS API 的稳定 poi_id;name-only 时仅 name_meituan(源平台店名来自 intent/agent 匹配名)。
    id_meituan: Mapped[str | None] = mapped_column(String(64), index=True, nullable=True)
    name_meituan: Mapped[str | None] = mapped_column(String(128), nullable=True)
    # 京东:已接通 share→id(3.cn 短链反查)。id_jd = storeId(门店稳定数字主键,同 taobao shopId→
    # id_taobao);venderId(deeplink 还需)单列存 jd_vender_id。name_jd = 店铺页店名。
    id_jd: Mapped[str | None] = mapped_column(String(64), index=True, nullable=True)
    name_jd: Mapped[str | None] = mapped_column(String(128), nullable=True)

    # ===== 地理(同名店异地区分 / 地理分桶匹配的主要燃料)=====
    city: Mapped[str | None] = mapped_column(String(64), nullable=True)
    geohash: Mapped[str | None] = mapped_column(String(16), index=True, nullable=True)
    lng: Mapped[float | None] = mapped_column(Float, nullable=True)
    lat: Mapped[float | None] = mapped_column(Float, nullable=True)
    # 淘宝门店地址(店铺页 a11y 抓到才有;比经纬度更利于人工/LLM 匹配)
    taobao_address: Mapped[str | None] = mapped_column(String(256), nullable=True)

    # ===== 溯源 / 用户画像 =====
    # 源平台(发起比价那家:meituan / taobao_flash / jd_waimai ...)
    source_platform: Mapped[str | None] = mapped_column(String(32), index=True, nullable=True)
    business_type: Mapped[str] = mapped_column(String(16), nullable=False, default="food")
    # pricebot 侧 trace_id:回指原始 trace(溯源)+ 幂等去重键(uq_store_mapping_trace)
    trace_id: Mapped[str] = mapped_column(String(64), nullable=False)
    source_device_id: Mapped[str | None] = mapped_column(String(64), index=True, nullable=True)
    # user_id 当前比价链路不鉴权拿不到,客户端带上时才有;先可空。
    source_user_id: Mapped[int | None] = mapped_column(Integer, index=True, nullable=True)

    # ===== 淘宝原料(可复跳 / 可重解析 / 调试;URL 可能很长 → Text)=====
    taobao_share_url: Mapped[str | None] = mapped_column(String(256), nullable=True)   # m.tb.cn 短链
    taobao_resolved_url: Mapped[str | None] = mapped_column(Text, nullable=True)        # 解析出的目标 URL(含 shopId)
    taobao_deeplink: Mapped[str | None] = mapped_column(Text, nullable=True)            # 拼好的 et-store/search deeplink
    # 淘宝 deeplink 失效标记:比价撞"页面出错了"降级页时被置(pricebot server→server invalidate),
    # NULL=有效。lookup 反查过滤掉非 NULL 的淘宝候选,不再返回坏 deeplink(重搜会写新行覆盖)。
    taobao_deeplink_invalid_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)

    # ===== 美团原料(同淘宝;dpurl.cn 短链 → 302 反查 poi_id_str → imeituan:// deeplink)=====
    # ⚠️ poi_id_str 每次分享重新加密、非稳定主键(调研文档 §八), 故单列存"可复跳的一次性票据",
    # 不进 id_meituan —— 后者留给将来 CPS API 拿到的稳定数字 poi_id。
    meituan_poi_id_str: Mapped[str | None] = mapped_column(String(64), index=True, nullable=True)
    meituan_share_url: Mapped[str | None] = mapped_column(String(256), nullable=True)   # dpurl.cn 短链
    meituan_resolved_url: Mapped[str | None] = mapped_column(Text, nullable=True)        # 302 落地 menu URL(含 poi_id_str)
    meituan_deeplink: Mapped[str | None] = mapped_column(Text, nullable=True)            # 拼好的 imeituan:// 店内搜索 deeplink

    # ===== 京东原料(秒送;3.cn 短链 → 跟随重定向反查 venderId+storeId → openapp.jdmobile:// deeplink)=====
    # storeId 进 id_jd(稳定店主键);venderId 单列存(deeplink 模板 venderId+storeId 都要,且 venderId
    # 是商家维度、可跨门店,与门店 storeId 分开记)。其余三列与 taobao_*/meituan_* 平行。
    jd_vender_id: Mapped[str | None] = mapped_column(String(64), index=True, nullable=True)
    jd_share_url: Mapped[str | None] = mapped_column(String(256), nullable=True)         # 3.cn 短链
    jd_resolved_url: Mapped[str | None] = mapped_column(Text, nullable=True)             # 反查出的目标 openapp.jdmobile:// deeplink
    jd_deeplink: Mapped[str | None] = mapped_column(Text, nullable=True)                 # 拼好的 pages/search 店内搜索 deeplink
    # 京东 deeplink 失效标记(同 taobao_deeplink_invalid_at):撞"当前门店超出配送范围"页时被置,NULL=有效。
    jd_deeplink_invalid_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)

    # 灵活字段兜底(免得加字段就迁移)
    attrs: Mapped[dict | None] = mapped_column(_JSON, nullable=True)

    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), server_default=func.now(), index=True, nullable=False
    )

    def __repr__(self) -> str:  # pragma: no cover
        return (
            f"<StoreMapping id={self.id} taobao=({self.id_taobao!r},{self.name_taobao!r}) "
            f"source={self.source_platform} trace_id={self.trace_id}>"
        )
