import re
from typing import Literal

from pydantic import BaseModel, Field, field_validator, model_validator


class StaffUpsertIn(BaseModel):
    user_id: int
    pos_enabled: bool
    store_ids: list[int] = Field(default_factory=list)
    pin: str | None = None   # None = auto-generate 6-digit PIN


class ResetPinIn(BaseModel):
    user_id: int
    pin: str | None = None   # None = auto-generate 6-digit PIN


class StaffOut(BaseModel):
    user_id: int
    pos_enabled: bool
    store_ids: list[int]
    credential_version: int
    pin: str | None = None   # only populated on upsert/reset (not in list)


class ReturnItemIn(BaseModel):
    orderItemId: int | None = None      # 有小票退货必填
    productId: int | None = None        # 无小票退货必填
    variantId: int | None = None
    name: str | None = None
    quantity: str
    unitPriceCents: int | None = None   # 无小票退货的改价；为空则用参考价


class ReturnPlanIn(BaseModel):
    orderNo: str
    items: list[ReturnItemIn] = Field(min_length=1)
    refundMethod: Literal["original", "cash"] = "original"
    refundAmountCents: int | None = Field(default=None, gt=0)
    moneyOnly: bool = False


class ReturnCreateIn(BaseModel):
    idempotencyKey: str = Field(min_length=8, max_length=120)
    sourceType: Literal["receipt", "receiptless"] = "receipt"
    orderNo: str | None = None
    customerId: int | None = None
    items: list[ReturnItemIn] = Field(min_length=1)
    reason: str | None = None
    overrideReason: str | None = None
    stockDisposition: Literal["resellable", "damaged", "inspection"] = "resellable"
    refundMethod: Literal["original", "cash"] = "original"
    refundAmountCents: int | None = Field(default=None, gt=0)
    moneyOnly: bool = False
    operatorUserId: int
    approverUserId: int | None = None
    approvalEvidence: dict | None = None


class ReturnSplitResultIn(BaseModel):
    paymentMethod: Literal["cash", "eftpos", "store_credit"]
    amountCents: int = Field(ge=0)
    status: str                          # approved 之外一律视为资金未确认
    # paymentId 必须保留：可退余额是按每笔原支付累计的，被 Pydantic 剥掉就会算错，
    # 同一笔支付可以被反复退款。
    paymentId: int | None = None
    originalTxnRef: str | None = None
    providerTxnRef: str | None = None


class ReturnSettleIn(BaseModel):
    splits: list[ReturnSplitResultIn] = Field(default_factory=list)


class ProductAgeIn(BaseModel):
    product_id: int = Field(gt=0)
    minimum_age: int | None = Field(default=None, ge=0, le=120)   # None / 0 表示不限制


_COLOR_RE = re.compile(r"^#[0-9a-fA-F]{6}$")
# 收银端的集合选择器是分页的，所以不必限制在一屏之内。
_COLLECTION_MAX_ITEMS = 100


class QuickButtonCollectionItem(BaseModel):
    """集合按钮里的一项：一个商品 + 可选变体，与商品按钮的取值规则完全一致。"""
    productId: int = Field(gt=0)
    variantId: int | None = Field(default=None, gt=0)


class QuickButtonItem(BaseModel):
    type: Literal["category", "product", "collection"]
    # 集合按钮没有单一目标，targetId 必须为空；分类/商品按钮仍然必填（见下面的 model_validator）。
    targetId: int | None = Field(default=None, gt=0)
    # 必须 gt=0：0 会被下游「有没有变体」的真值判断当成 None，从而跳过归属校验直接入库。
    variantId: int | None = Field(default=None, gt=0)
    label: str | None = Field(default=None, max_length=40)   # None 表示回退显示分类/商品的实时名称，非空标签
    color: str = Field(default="#e6f4f1")
    # 只有集合按钮有；分类/商品按钮必须为空。
    items: list[QuickButtonCollectionItem] | None = None

    @field_validator("color")
    @classmethod
    def validate_color(cls, v: str) -> str:
        if not _COLOR_RE.match(v):
            raise ValueError("color must be #RRGGBB")
        return v.lower()

    @model_validator(mode="after")
    def check_shape(self):
        if self.variantId is not None and self.type != "product":
            raise ValueError("variantId only allowed for product buttons")
        if self.type == "collection":
            if self.targetId is not None:
                raise ValueError("collection buttons must not have targetId")
            # 集合没有可回退的分类/商品名，标签是收银员唯一能看到的字。
            if not (self.label or "").strip():
                raise ValueError("collection buttons require a non-empty label")
            if not self.items:
                raise ValueError("collection buttons require at least one item")
            if len(self.items) > _COLLECTION_MAX_ITEMS:
                raise ValueError(f"collection allows at most {_COLLECTION_MAX_ITEMS} items")
            keys = [(i.productId, i.variantId) for i in self.items]
            if len(keys) != len(set(keys)):
                raise ValueError("duplicate item in collection")
        else:
            if self.targetId is None:
                raise ValueError("targetId is required")
            if self.items is not None:
                raise ValueError("items only allowed for collection buttons")
        return self


class QuickButtonsSaveIn(BaseModel):
    store_id: int = Field(gt=0)
    buttons: list[QuickButtonItem] = Field(max_length=16)
