"""POS 快捷按钮配置 — Admin 侧 CRUD，Agent 侧快照读取。

结构性校验（颜色格式、最多 16 个、variantId 只允许出现在商品按钮上）已经在
schemas.QuickButtonItem 里做过，这里只做需要查库的部分：目标是否属于本租户、
变体是否属于它声明的商品、门店是否真实存在。
"""
from __future__ import annotations

from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.plugins.pos_operations.models import PosQuickButtonConfig


async def get_quick_buttons(db: AsyncSession, tenant_id: int, store_id: int) -> dict:
    """没有配置行 = 未配置，POS 回落到旧行为（前 16 个分类）；
    有行但 buttons 为空 = 管理员明确保存了「一个都不要」，POS 不得回落。"""
    cfg = (await db.execute(
        select(PosQuickButtonConfig).where(
            PosQuickButtonConfig.tenant_id == tenant_id,
            PosQuickButtonConfig.store_id == store_id,
        )
    )).scalar_one_or_none()
    if not cfg:
        return {"configured": False, "buttons": [], "buttons_version": 0}
    return {"configured": True, "buttons": cfg.buttons_json, "buttons_version": cfg.buttons_version}


async def _validate_targets(db: AsyncSession, tenant_id: int, buttons: list[dict]) -> None:
    """确认每个 targetId 都属于本租户且可用，且 variantId 确实挂在它声明的商品下。"""
    from app.core.models.category import Category
    from app.core.models.product import Product, ProductVariant

    cat_ids = {b["targetId"] for b in buttons if b["type"] == "category"}
    # 商品按钮和集合里的每一项是同一种东西（一个商品 + 可选变体），拍平成一张 (商品, 变体)
    # 列表后共用下面的批量查询，集合再长也不会多发一次 SQL。
    pairs = [(b["targetId"], b.get("variantId")) for b in buttons if b["type"] == "product"]
    pairs += [(i["productId"], i.get("variantId"))
              for b in buttons if b["type"] == "collection"
              for i in (b.get("items") or [])]
    prod_ids = {p for p, _ in pairs}
    # 用 is not None 而不是真值判断：variantId=0 被真值判断滤掉就等于绕过下面的归属校验。
    # schemas 已经挡了 gt=0，这里再挡一次，因为本函数不保证调用方一定过了 pydantic。
    var_ids = {v for _, v in pairs if v is not None}

    if cat_ids:
        found = set((await db.execute(
            select(Category.id).where(
                Category.tenant_id == tenant_id,
                Category.is_active == 1,
                Category.id.in_(cat_ids),
            )
        )).scalars().all())
        if cat_ids - found:
            raise HTTPException(422, f"categories not found: {sorted(cat_ids - found)}")

    if prod_ids:
        # products 表没有 is_active 列，上下架状态在 status 上（draft/active/archived）。
        found = set((await db.execute(
            select(Product.id).where(
                Product.tenant_id == tenant_id,
                Product.status == "active",
                Product.id.in_(prod_ids),
            )
        )).scalars().all())
        if prod_ids - found:
            raise HTTPException(422, f"products not found: {sorted(prod_ids - found)}")

    if var_ids:
        # ProductVariant 自带 tenant_id，不必 join products 判租户；
        # 商品本身的上架状态已在上面按 targetId 校验过。
        var_product_map = {
            r.id: r.product_id
            for r in (await db.execute(
                select(ProductVariant.id, ProductVariant.product_id).where(
                    ProductVariant.tenant_id == tenant_id,
                    ProductVariant.is_active == 1,
                    ProductVariant.id.in_(var_ids),
                )
            )).all()
        }
        for pid, vid in pairs:
            if vid is None:
                continue
            if vid not in var_product_map:
                raise HTTPException(422, f"variant {vid} not found")
            if var_product_map[vid] != pid:
                raise HTTPException(
                    422, f"variant {vid} does not belong to product {pid}")


async def _validate_store_and_duplicates(
    db: AsyncSession, tenant_id: int, store_id: int, buttons: list[dict],
) -> None:
    # 系统里没有独立的 stores 表，门店身份只存在于 pos_lanes 上（同 services.list_stores）。
    from app.plugins.pos_sync.models import PosLane

    has_lane = (await db.execute(
        select(PosLane.id).where(
            PosLane.tenant_id == tenant_id,
            PosLane.store_id == store_id,
            PosLane.is_active == 1,
        ).limit(1)
    )).scalar_one_or_none()
    if not has_lane:
        raise HTTPException(422, "store has no active POS lane")

    # 同一目标配两个按钮只会让收银员困惑。整商品按钮与它某个变体的按钮算不同目标。
    # 集合按钮不参与：它们的 targetId/variantId 恒为 None，放进来两个不同的集合会被误判成重复；
    # 集合内部的重名在 schemas 里按 (productId, variantId) 单独查过。
    keys = [(b["type"], b["targetId"], b.get("variantId"))
            for b in buttons if b["type"] != "collection"]
    if len(keys) != len(set(keys)):
        raise HTTPException(422, "duplicate quick button target")


async def upsert_quick_buttons(
    db: AsyncSession, tenant_id: int, store_id: int, buttons: list[dict],
) -> dict:
    await _validate_store_and_duplicates(db, tenant_id, store_id, buttons)
    await _validate_targets(db, tenant_id, buttons)

    # 锁住这一行：两个管理员同时保存时，版本号必须逐次推进，不能读到同一个旧值各加一。
    # 同插件的 services.bump_staff_revision 也是这么做的。
    # ponytail: 首次插入还没有行可锁，并发建同一门店会撞唯一键报 500；
    # 真出现了再捕 IntegrityError 重试一次即可。
    cfg = (await db.execute(
        select(PosQuickButtonConfig).where(
            PosQuickButtonConfig.tenant_id == tenant_id,
            PosQuickButtonConfig.store_id == store_id,
        ).with_for_update()
    )).scalar_one_or_none()
    if cfg:
        cfg.buttons_json = buttons
        # Agent 只在版本变大时才换本地缓存，所以每次保存都必须推进。
        cfg.buttons_version = cfg.buttons_version + 1
    else:
        cfg = PosQuickButtonConfig(
            tenant_id=tenant_id, store_id=store_id,
            buttons_json=buttons, buttons_version=1,
        )
        db.add(cfg)
    await db.commit()
    await db.refresh(cfg)
    return {
        "ok": True,
        "configured": True,
        "buttons": cfg.buttons_json,
        "buttons_version": cfg.buttons_version,
    }
