"""管理后台 — 打包分箱规则 API

路由前缀：/api/packing-rules
资源：
  GET    /attribute-fields                     — 可用字段路径（供前端搜索建议）
  GET    /attribute-values                     — 某字段的所有现有值
  POST   /carriers/{carrier_id}/ai-generate   — AI 解析文本生成规则草稿
  GET    /carriers/{carrier_id}/rules          — 查询规则列表
  POST   /carriers/{carrier_id}/rules          — 新增规则
  PUT    /carriers/{carrier_id}/rules/{id}     — 修改规则
  DELETE /carriers/{carrier_id}/rules/{id}     — 删除规则
  POST   /carriers/{carrier_id}/pack           — 执行自动分箱
"""
import json
import re
from typing import Any, Optional, List

from fastapi import APIRouter, Depends, HTTPException, status, Query
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.api.deps import get_db, get_admin_user
from app.core.models.carrier_packing_rule import CarrierPackingRule
from app.core.models.product import Product, ProductVariant
from app.core.models.category import Category
from app.core.models.brand import Brand
from app.core.models.tenant_settings import TenantSettings
from app.core.models.user import User
from app.core.ai_utils import call_ai, resolve_ai_extra
from app.core.services.ai_quota import consume_ai_quota
from app.core.services.packing_engine import (
    ShipmentItem,
    auto_pack,
)

router = APIRouter(prefix="/packing-rules", tags=["打包分箱规则"])


# ══════════════════════════════════════════════════════════════
#  Pydantic Schemas
# ══════════════════════════════════════════════════════════════

class SelectorSchema(BaseModel):
    field: str = Field(..., description="商品字段路径，如 'attributes.category' 或 'weight'")
    operator: str = Field(..., description="eq | neq | in | not_in | contains | exists")
    value: Optional[Any] = None


class RuleIn(BaseModel):
    name: str
    rule_type: str = Field(..., description="LIMIT | UNIQUE | EXCLUSIVE | FORBIDDEN | INCOMPATIBLE | MIXED_LIMIT")
    selector: Optional[SelectorSchema] = None
    params: dict[str, Any] = Field(default_factory=dict)
    message: Optional[str] = None
    priority: int = 0
    is_active: int = 1


class RuleOut(BaseModel):
    id: int
    carrier_id: int
    name: str
    rule_type: str
    selector: Optional[dict] = None
    params: dict
    message: Optional[str] = None
    priority: int
    is_active: int
    model_config = {"from_attributes": True}


# ── 分箱请求 / 响应 ──────────────────────────────────────────

class ShipmentItemIn(BaseModel):
    id: str = Field(default="", description="可选，调试用标识")
    sku_id: str
    name: str
    quantity: int = Field(ge=1)
    weight: float = Field(ge=0, description="单件重量 kg")
    value: float = Field(ge=0, description="单件申报价值")
    attributes: dict[str, Any] = Field(default_factory=dict)


class BoxOut(BaseModel):
    id: str
    items: List[dict]
    total_quantity: int
    total_weight: float
    total_value: float


class PackResponse(BaseModel):
    boxes: List[BoxOut]
    forbidden: List[dict]
    box_count: int


# ══════════════════════════════════════════════════════════════
#  字段路径建议（供前端 datalist）
# ══════════════════════════════════════════════════════════════

# ShipmentItem 的固定顶层字段
_STANDARD_FIELDS = ["weight", "value", "name", "sku_id"]


async def _enrich_from_tables(field_key: str, tenant_id: int, db: AsyncSession) -> set[str]:
    """对 category / brand 字段额外从专用表拉取全量 slug，保证新增分类/品牌立刻可用。"""
    extra: set[str] = set()
    if field_key == "category":
        res = await db.execute(
            select(Category.slug, Category.name)
            .where(Category.tenant_id == tenant_id, Category.is_active == 1)
        )
        for slug, name in res:
            if slug:
                extra.add(slug)
            if name:
                extra.add(name)
    elif field_key == "brand":
        res = await db.execute(
            select(Brand.slug, Brand.name)
            .where(Brand.tenant_id == tenant_id, Brand.is_active == 1)
        )
        for slug, name in res:
            if slug:
                extra.add(slug)
            if name:
                extra.add(name)
    return extra


@router.get("/attribute-values", response_model=List[str])
async def list_attribute_values(
    field: str,
    db: AsyncSession = Depends(get_db),
    user: User = Depends(get_admin_user),
):
    """返回指定字段路径的所有去重值（仅支持 attributes.xxx 路径）。
    供前端 datalist / 多选标签使用。
    category / brand 字段额外从专用表拉取，确保新增后立即可选。"""
    if not field.startswith("attributes."):
        return []
    key = field[len("attributes."):]
    values: set[str] = set()

    res1 = await db.execute(
        select(Product.extra_attributes)
        .where(Product.tenant_id == user.tenant_id, Product.extra_attributes.is_not(None))
        .limit(1000)
    )
    for (attrs,) in res1:
        if isinstance(attrs, dict) and key in attrs:
            v = attrs[key]
            values.add(str(v) if v is not None else "")

    res2 = await db.execute(
        select(ProductVariant.attributes)
        .join(Product, ProductVariant.product_id == Product.id)
        .where(Product.tenant_id == user.tenant_id)
        .limit(1000)
    )
    for (attrs,) in res2:
        if isinstance(attrs, dict) and key in attrs:
            v = attrs[key]
            values.add(str(v) if v is not None else "")

    # 补充专用表数据（category / brand 新增后立即可见）
    values |= await _enrich_from_tables(key, user.tenant_id, db)

    values.discard("")
    return sorted(values)


@router.get("/attribute-fields", response_model=List[str])
async def list_attribute_fields(
    db: AsyncSession = Depends(get_db),
    user: User = Depends(get_admin_user),
):
    """扫描商品 extra_attributes / variant attributes，返回所有可用字段路径。
    结果 = 标准字段 + attributes.<key> 前缀的动态键名，供规则编辑器搜索建议使用。"""
    attr_keys: set[str] = set()

    # 从 Product.extra_attributes 采样提取键名
    res1 = await db.execute(
        select(Product.extra_attributes)
        .where(
            Product.tenant_id == user.tenant_id,
            Product.extra_attributes.is_not(None),
        )
        .limit(500)
    )
    for (attrs,) in res1:
        if isinstance(attrs, dict):
            attr_keys.update(attrs.keys())

    # 从 ProductVariant.attributes 采样提取键名
    res2 = await db.execute(
        select(ProductVariant.attributes)
        .join(Product, ProductVariant.product_id == Product.id)
        .where(Product.tenant_id == user.tenant_id)
        .limit(500)
    )
    for (attrs,) in res2:
        if isinstance(attrs, dict):
            attr_keys.update(attrs.keys())

    attribute_fields = sorted(f"attributes.{k}" for k in attr_keys)
    return _STANDARD_FIELDS + attribute_fields


# ══════════════════════════════════════════════════════════════
#  AI 生成端点
# ══════════════════════════════════════════════════════════════

_VALID_RULE_TYPES_SET = {"LIMIT", "UNIQUE", "EXCLUSIVE", "FORBIDDEN", "INCOMPATIBLE", "MIXED_LIMIT"}

_AI_SYSTEM_PROMPT = """\
你是一个物流打包规则解析引擎，只输出 JSON，不输出任何解释文字和 markdown。

## 规则 Schema（每条规则必须包含的字段）
{
  "name": "规则名称（简洁中文）",
  "rule_type": "LIMIT|UNIQUE|EXCLUSIVE|INCOMPATIBLE|MIXED_LIMIT|FORBIDDEN",
  "selector": {"field":"字段路径","operator":"eq|neq|in|not_in|contains|exists","value": ...} | null,
  "params": {},
  "message": "可选提示",
  "priority": 整数,
  "is_active": 1
}

## 各规则类型及示例

LIMIT — 限制件数/重量/价值
  整箱重量：{"name":"单箱最大重量","rule_type":"LIMIT","selector":null,"params":{"metric":"weight","max":7},"priority":0}
  品类件数：{"name":"保健品≤8件","rule_type":"LIMIT","selector":{"field":"attributes.category","operator":"eq","value":"healthcare"},"params":{"metric":"quantity","max":8},"priority":10}
  子类件数：{"name":"Manuka蜂蜜≤3件","rule_type":"LIMIT","selector":{"field":"attributes.subcategory","operator":"eq","value":"manuka_honey"},"params":{"metric":"quantity","max":3},"priority":10}
  纯装限制（箱内只有该品牌时才生效）：{"name":"MitoQ纯装≤2","rule_type":"LIMIT","selector":{"field":"attributes.brand","operator":"eq","value":"MitoQ"},"params":{"metric":"quantity","max":2,"appliesWhen":"box_only_has_matching_items"},"priority":15}

EXCLUSIVE — 必须独占一箱，不与其他品类同箱
  {"name":"彩妆独占≤1套","rule_type":"EXCLUSIVE","selector":{"field":"attributes.category","operator":"eq","value":"makeup"},"params":{"maxQuantity":1},"priority":5}

INCOMPATIBLE — 两类商品不能同箱
  {"name":"Manuka不可与非保健品同箱","rule_type":"INCOMPATIBLE","selector":{"field":"attributes.subcategory","operator":"eq","value":"manuka_honey"},"params":{"with":{"field":"attributes.category","operator":"neq","value":"healthcare"}},"priority":10}

MIXED_LIMIT — 混装时取各品类 LIMIT 规则的最小 max 值作为整箱件数上限
  {"name":"混装取最低品类上限","rule_type":"MIXED_LIMIT","selector":null,"params":{"groupBy":"attributes.category","metric":"quantity","strategy":"min_limit"},"priority":20}

## 使用规则
- selector=null 表示作用于整箱所有商品
- "不能混装"且只提到该品类自身 → EXCLUSIVE
- "A不能和B同箱" → INCOMPATIBLE
- "混装时以最低限制为准" → MIXED_LIMIT（只需一条）
- "纯装限制"（只有该品牌时才生效）→ LIMIT + appliesWhen:box_only_has_matching_items
- priority: 全箱限制=0, EXCLUSIVE=5, 品类LIMIT=10, 品牌LIMIT=15, MIXED_LIMIT=20
- 数字类型用 number，不用字符串

## 可用字段路径与现有值（必须优先使用这些值）
{field_values_section}

仅返回 JSON 数组，第一个字符必须是 [，最后一个字符必须是 ]。\
"""


class AiGenerateRequest(BaseModel):
    text: str = Field(..., description="快递公司公告原文")


class AiGenerateResponse(BaseModel):
    rules: List[dict]
    warnings: List[str] = []


def _extract_json_array(raw: str) -> list:
    """从 AI 回复中提取 JSON 数组，兼容带 markdown 代码块和截断响应的情况"""
    cleaned = re.sub(r"```(?:json)?\s*", "", raw).replace("```", "").strip()
    start = cleaned.find("[")
    if start == -1:
        raise ValueError("AI 返回内容中未找到 JSON 数组")
    end = cleaned.rfind("]")
    # 先尝试完整解析
    if end != -1 and end > start:
        try:
            return json.loads(cleaned[start:end + 1])
        except json.JSONDecodeError:
            pass
    # 截断容错：逐个提取已完成的顶层 JSON 对象
    body = cleaned[start + 1:].lstrip()
    decoder = json.JSONDecoder()
    results = []
    pos = 0
    while pos < len(body):
        body = body[pos:].lstrip(" \n\t,")
        if not body or body[0] == "]":
            break
        try:
            obj, pos = decoder.raw_decode(body)
            results.append(obj)
        except json.JSONDecodeError:
            break
    if not results:
        raise ValueError("AI 返回内容中未找到 JSON 数组")
    return results


def _validate_rule(r: dict, idx: int) -> list[str]:
    """校验单条规则，返回警告列表"""
    warnings = []
    if not isinstance(r.get("name"), str) or not r["name"]:
        warnings.append(f"规则 #{idx+1}：缺少 name 字段")
    rt = r.get("rule_type")
    if rt not in _VALID_RULE_TYPES_SET:
        warnings.append(f"规则 #{idx+1} '{r.get('name','')}': rule_type '{rt}' 无效")
    if not isinstance(r.get("params"), dict):
        warnings.append(f"规则 #{idx+1} '{r.get('name','')}': params 必须是对象")
    return warnings


@router.post("/carriers/{carrier_id}/ai-generate", response_model=AiGenerateResponse)
async def ai_generate_rules(
    carrier_id: int,
    body: AiGenerateRequest,
    db: AsyncSession = Depends(get_db),
    user: User = Depends(get_admin_user),
):
    """用 AI 解析快递公告文本，返回规则草稿（不写库，供前端预览后人工确认）"""
    # ── 1. 读取租户 AI 配置 ──
    ts_r = await db.execute(
        select(TenantSettings).where(TenantSettings.tenant_id == user.tenant_id)
    )
    s     = ts_r.scalar_one_or_none()
    extra = (s.extra or {}) if s else {}
    if not extra.get("ai_enabled"):
        raise HTTPException(400, "AI 功能未启用，请在系统设置 → AI 配置中开启")
    extra = await resolve_ai_extra(db, user.tenant_id, extra)
    await consume_ai_quota(db, user.tenant_id)

    # ── 2. 收集现有字段路径及每个字段的现有值（Plan B）──
    attr_keys: set[str] = set()
    res1 = await db.execute(
        select(Product.extra_attributes)
        .where(Product.tenant_id == user.tenant_id, Product.extra_attributes.is_not(None))
        .limit(500)
    )
    for (attrs,) in res1:
        if isinstance(attrs, dict):
            attr_keys.update(attrs.keys())

    res2 = await db.execute(
        select(ProductVariant.attributes)
        .join(Product, ProductVariant.product_id == Product.id)
        .where(Product.tenant_id == user.tenant_id)
        .limit(500)
    )
    for (attrs,) in res2:
        if isinstance(attrs, dict):
            attr_keys.update(attrs.keys())

    # 每个字段的现有值（产品属性扫描 + 专用表补充）
    field_values: dict[str, list[str]] = {}
    for key in sorted(attr_keys):
        vals: set[str] = set()
        path = f"attributes.{key}"
        for (attrs,) in (await db.execute(
            select(Product.extra_attributes)
            .where(Product.tenant_id == user.tenant_id, Product.extra_attributes.is_not(None))
            .limit(500)
        )):
            if isinstance(attrs, dict) and key in attrs and attrs[key] is not None:
                vals.add(str(attrs[key]))
        # 对 category / brand 字段额外从专用表补充，确保新增后立即对 AI 可见
        vals |= await _enrich_from_tables(key, user.tenant_id, db)
        field_values[path] = sorted(vals)

    # 即使产品侧没有用到 category / brand 键，也主动注入（避免空商品库时 AI 无从参考）
    for meta_key in ("category", "brand"):
        path = f"attributes.{meta_key}"
        if path not in field_values:
            extra_vals = await _enrich_from_tables(meta_key, user.tenant_id, db)
            if extra_vals:
                field_values[path] = sorted(extra_vals)

    # 拼成 prompt 段落
    if field_values:
        lines = []
        for path, vals in field_values.items():
            sample = ", ".join(f'"{v}"' for v in vals[:20])
            lines.append(f"  {path}: [{sample}]")
        field_values_section = "\n".join(lines)
    else:
        field_values_section = "  （暂无商品数据，请根据公告内容自行推断合理的英文小写值）"

    system_prompt = _AI_SYSTEM_PROMPT.replace("{field_values_section}", field_values_section)

    # ── 3. 调用 AI ──
    raw = await call_ai(
        user_prompt=f"请解析以下快递公司打包要求，生成规则 JSON 数组：\n\n{body.text}",
        extra=extra,
        system_prompt=system_prompt,
        max_tokens=4096,
        timeout=55,
    )

    # ── 4. 解析 + 校验 ──
    try:
        rules_raw = _extract_json_array(raw)
    except Exception as e:
        raise HTTPException(422, f"AI 返回格式无法解析：{e}。原始回复：{raw[:300]}")

    if not isinstance(rules_raw, list):
        raise HTTPException(422, "AI 未返回规则数组")

    warnings: list[str] = []
    valid_rules: list[dict] = []
    for i, r in enumerate(rules_raw):
        if not isinstance(r, dict):
            warnings.append(f"第 {i+1} 条不是对象，已跳过")
            continue
        w = _validate_rule(r, i)
        warnings.extend(w)
        # 注入默认值
        r.setdefault("priority", 0)
        r.setdefault("is_active", 1)
        r.setdefault("message", None)
        r.setdefault("selector", None)
        valid_rules.append(r)

    return AiGenerateResponse(rules=valid_rules, warnings=warnings)


# ══════════════════════════════════════════════════════════════
#  CRUD 端点
# ══════════════════════════════════════════════════════════════

@router.get("/carriers/{carrier_id}/rules", response_model=List[RuleOut])
async def list_rules(
    carrier_id: int,
    db: AsyncSession = Depends(get_db),
    user: User = Depends(get_admin_user),
):
    result = await db.execute(
        select(CarrierPackingRule)
        .where(
            CarrierPackingRule.tenant_id == user.tenant_id,
            CarrierPackingRule.carrier_id == carrier_id,
        )
        .order_by(CarrierPackingRule.priority, CarrierPackingRule.id)
    )
    return result.scalars().all()


@router.post("/carriers/{carrier_id}/rules", response_model=RuleOut, status_code=status.HTTP_201_CREATED)
async def create_rule(
    carrier_id: int,
    body: RuleIn,
    db: AsyncSession = Depends(get_db),
    user: User = Depends(get_admin_user),
):
    _validate_rule_type(body.rule_type)
    rule = CarrierPackingRule(
        tenant_id=user.tenant_id,
        carrier_id=carrier_id,
        name=body.name,
        rule_type=body.rule_type,
        selector=body.selector.model_dump() if body.selector else None,
        params=body.params,
        message=body.message,
        priority=body.priority,
        is_active=body.is_active,
    )
    db.add(rule)
    await db.commit()
    await db.refresh(rule)
    return rule


@router.put("/carriers/{carrier_id}/rules/{rule_id}", response_model=RuleOut)
async def update_rule(
    carrier_id: int,
    rule_id: int,
    body: RuleIn,
    db: AsyncSession = Depends(get_db),
    user: User = Depends(get_admin_user),
):
    rule = await _get_rule(db, user.tenant_id, carrier_id, rule_id)
    _validate_rule_type(body.rule_type)
    rule.name = body.name
    rule.rule_type = body.rule_type
    rule.selector = body.selector.model_dump() if body.selector else None
    rule.params = body.params
    rule.message = body.message
    rule.priority = body.priority
    rule.is_active = body.is_active
    await db.commit()
    await db.refresh(rule)
    return rule


@router.delete("/carriers/{carrier_id}/rules/{rule_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_rule(
    carrier_id: int,
    rule_id: int,
    db: AsyncSession = Depends(get_db),
    user: User = Depends(get_admin_user),
):
    rule = await _get_rule(db, user.tenant_id, carrier_id, rule_id)
    await db.delete(rule)
    await db.commit()


# ══════════════════════════════════════════════════════════════
#  分箱端点
# ══════════════════════════════════════════════════════════════

@router.post("/carriers/{carrier_id}/pack", response_model=PackResponse)
async def pack_items(
    carrier_id: int,
    body: List[ShipmentItemIn],
    db: AsyncSession = Depends(get_db),
    user: User = Depends(get_admin_user),
):
    """执行自动分箱，返回分箱结果和禁运列表"""
    # 加载该渠道全部启用规则
    result = await db.execute(
        select(CarrierPackingRule)
        .where(
            CarrierPackingRule.tenant_id == user.tenant_id,
            CarrierPackingRule.carrier_id == carrier_id,
            CarrierPackingRule.is_active == 1,
        )
        .order_by(CarrierPackingRule.priority, CarrierPackingRule.id)
    )
    db_rules = result.scalars().all()

    # 转成 dict（引擎只依赖 dict，不依赖 ORM 对象）
    rules = [
        {
            "id": r.id,
            "name": r.name,
            "rule_type": r.rule_type,
            "selector": r.selector,
            "params": r.params,
            "message": r.message,
            "priority": r.priority,
            "is_active": r.is_active,
        }
        for r in db_rules
    ]

    # 转换输入商品
    items = [
        ShipmentItem(
            id=i.id or i.sku_id,
            sku_id=i.sku_id,
            name=i.name,
            quantity=i.quantity,
            weight=i.weight,
            value=i.value,
            attributes=i.attributes,
        )
        for i in body
    ]

    pack_result = auto_pack(items, rules)

    boxes_out = [
        BoxOut(
            id=box.id,
            items=[
                {
                    "sku_id": unit.sku_id,
                    "name": unit.name,
                    "weight": unit.weight,
                    "value": unit.value,
                    "attributes": unit.attributes,
                }
                for unit in box.items
            ],
            total_quantity=box.total_quantity,
            total_weight=round(box.total_weight, 4),
            total_value=round(box.total_value, 4),
        )
        for box in pack_result.boxes
    ]

    forbidden_out = [
        {
            "sku_id": f["item"].sku_id,
            "name": f["item"].name,
            "reason": f["reason"],
        }
        for f in pack_result.forbidden
    ]

    return PackResponse(
        boxes=boxes_out,
        forbidden=forbidden_out,
        box_count=len(boxes_out),
    )


# ══════════════════════════════════════════════════════════════
#  内部工具
# ══════════════════════════════════════════════════════════════

_VALID_RULE_TYPES = {"LIMIT", "UNIQUE", "EXCLUSIVE", "FORBIDDEN", "INCOMPATIBLE", "MIXED_LIMIT"}


def _validate_rule_type(rule_type: str):
    if rule_type not in _VALID_RULE_TYPES:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail=f"rule_type 必须为 {_VALID_RULE_TYPES} 之一",
        )


async def _get_rule(
    db: AsyncSession, tenant_id: int, carrier_id: int, rule_id: int
) -> CarrierPackingRule:
    result = await db.execute(
        select(CarrierPackingRule).where(
            CarrierPackingRule.id == rule_id,
            CarrierPackingRule.carrier_id == carrier_id,
            CarrierPackingRule.tenant_id == tenant_id,
        )
    )
    rule = result.scalar_one_or_none()
    if not rule:
        raise HTTPException(status_code=404, detail="规则不存在")
    return rule
