from __future__ import annotations

import logging
import secrets
import string
from datetime import datetime, timezone
from decimal import Decimal
from typing import Any

import bcrypt
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.models.customer import Customer
from app.core.models.customer_address import CustomerAddress
from app.core.models.order import Order, OrderItem
from app.core.models.product import Product, ProductVariant
from app.plugins.xero_import.models import (
    XeroConnection,
    XeroEntityMap,
    XeroSyncLog,
    XeroSyncState,
)

logger = logging.getLogger("uvicorn.error")


def _generate_password(length: int = 10) -> str:
    """生成随机密码：字母 + 数字，至少包含一个大写、一个小写、一个数字。"""
    alphabet = string.ascii_letters + string.digits
    while True:
        pwd = ''.join(secrets.choice(alphabet) for _ in range(length))
        if (any(c.isupper() for c in pwd) and any(c.islower() for c in pwd) and any(c.isdigit() for c in pwd)):
            return pwd


def _hash_pwd(pwd: str) -> str:
    return bcrypt.hashpw(pwd.encode(), bcrypt.gensalt()).decode()


# ── 纯函数：载荷标准化 ────────────────────────────────────────────


def xero_quantity_to_decimal(value) -> Decimal:
    """Xero 库存保留两位小数存储。"""
    if value is None:
        return Decimal("0")
    return max(Decimal("0"), Decimal(str(value)).quantize(Decimal("0.01")))


def _first_phone(phones: list[dict] | None) -> str | None:
    for phone in phones or []:
        if phone.get("PhoneNumber"):
            return phone["PhoneNumber"]
    return None


def normalize_contact(payload: dict) -> dict:
    return {
        "xero_id": payload["ContactID"],
        "email": payload.get("EmailAddress") or f"xero-{payload['ContactID']}@placeholder.local",
        "name": payload.get("Name") or payload.get("EmailAddress") or f"Xero Contact {payload['ContactID']}",
        "phone": _first_phone(payload.get("Phones")),
        "addresses": payload.get("Addresses") or [],
        "updated_at": payload.get("UpdatedDateUTC"),
        "raw": payload,
    }


# Xero 国家名（英文全名）→ 本地 ISO 2 位国家代码，常见国家兜底映射
DEFAULT_COUNTRY_NAME_MAPPING = {
    "new zealand": "NZ",
    "australia": "AU",
    "china": "CN",
    "united states": "US",
    "united states of america": "US",
    "usa": "US",
    "united kingdom": "GB",
    "uk": "GB",
    "canada": "CA",
    "hong kong": "HK",
    "singapore": "SG",
    "japan": "JP",
    "south korea": "KR",
    "korea": "KR",
    "taiwan": "TW",
    "malaysia": "MY",
    "thailand": "TH",
    "philippines": "PH",
    "vietnam": "VN",
    "indonesia": "ID",
    "india": "IN",
    "germany": "DE",
    "france": "FR",
}


def _pick_address(addresses: list[dict]) -> dict | None:
    """优先取 STREET 地址，没有则取 POBOX 地址。"""
    street = next((a for a in addresses if a.get("AddressType") == "STREET"), None)
    if street:
        return street
    return next((a for a in addresses if a.get("AddressType") == "POBOX"), None)


def normalize_address(addr: dict, country_name_mapping: dict[str, str] | None = None) -> dict | None:
    """把 Xero Address 对象转换为本地 CustomerAddress 所需字段。country 无法映射时返回 None。"""
    if not addr:
        return None
    raw_country = (addr.get("Country") or "").strip()
    mapping = {**DEFAULT_COUNTRY_NAME_MAPPING, **{k.lower(): v for k, v in (country_name_mapping or {}).items()}}
    country_code = mapping.get(raw_country.lower())
    if not country_code:
        return None

    street_parts = [
        addr.get("AddressLine1"), addr.get("AddressLine2"),
        addr.get("AddressLine3"), addr.get("AddressLine4"),
    ]
    street = ", ".join(p for p in street_parts if p)
    if not street:
        return None

    return {
        "country": country_code,
        "province": addr.get("Region") or "",
        "city": addr.get("City") or "",
        "street": street,
        "zip_code": addr.get("PostalCode") or "",
    }


def normalize_item(payload: dict) -> dict:
    sales = payload.get("SalesDetails") or {}
    price = Decimal(str(sales.get("UnitPrice") or 0)).quantize(Decimal("0.01"))
    is_tracked = bool(payload.get("IsTrackedAsInventory"))
    return {
        "xero_id": payload["ItemID"],
        "sku": payload.get("Code") or payload["ItemID"],
        "name": payload.get("Name") or payload.get("Code") or f"Xero Item {payload['ItemID']}",
        "description": payload.get("Description"),
        "base_price": str(price),
        "is_sold": bool(payload.get("IsSold", True)),
        "is_tracked": is_tracked,
        "stock_qty": xero_quantity_to_decimal(payload.get("QuantityOnHand")) if is_tracked else None,
        "sales_account_code": sales.get("AccountCode"),
        "sales_tax_type": sales.get("TaxType"),
        "updated_at": payload.get("UpdatedDateUTC"),
        "raw": payload,
    }


def choose_item_target(variant_id: int | None, product_id: int | None) -> tuple[str | None, int | None]:
    if variant_id is not None:
        return "variant", variant_id
    if product_id is not None:
        return "product", product_id
    return None, None


# ── 导入服务 ──────────────────────────────────────────────────────


class XeroImportService:
    def __init__(self, db: AsyncSession, tenant_id: int, client: Any, sync_options: dict | None = None):
        self.db = db
        self.tenant_id = tenant_id
        self.client = client
        self._pending_emails: list[dict] = []
        self.sync_price = (sync_options or {}).get("sync_price", False)
        self.auto_delist_zero_stock = (sync_options or {}).get("auto_delist_zero_stock", True)
        self.tax_type_mapping: dict[str, int] = (sync_options or {}).get("tax_type_mapping", {})
        self.country_name_mapping: dict[str, str] = (sync_options or {}).get("country_name_mapping", {})
        self.sync_address_overwrite: bool = (sync_options or {}).get("sync_address_overwrite", False)

    async def _get_active_connection(self) -> XeroConnection | None:
        result = await self.db.execute(
            select(XeroConnection).where(
                XeroConnection.tenant_id == self.tenant_id,
                XeroConnection.is_active == 1,
            ).limit(1)
        )
        return result.scalar_one_or_none()

    async def _ensure_token(self, conn: XeroConnection) -> str:
        if conn.expires_at.replace(tzinfo=timezone.utc) < datetime.now(timezone.utc):
            token_data = await self.client.refresh(conn.refresh_token)
            conn.access_token = token_data["access_token"]
            conn.refresh_token = token_data["refresh_token"]
            conn.expires_at = self.client.expires_at_from_token(token_data)
            await self.db.flush()
        return conn.access_token

    async def _get_sync_state(self, connection_id: int, resource: str) -> XeroSyncState | None:
        result = await self.db.execute(
            select(XeroSyncState).where(
                XeroSyncState.tenant_id == self.tenant_id,
                XeroSyncState.connection_id == connection_id,
                XeroSyncState.resource == resource,
            )
        )
        return result.scalar_one_or_none()

    async def _write_log(self, connection_id: int | None, resource: str, mode: str,
                         status: str, created: int, updated: int, skipped: int,
                         failed: int, message: str | None = None, detail: dict | None = None):
        log = XeroSyncLog(
            tenant_id=self.tenant_id,
            connection_id=connection_id,
            resource=resource,
            mode=mode,
            status=status,
            created_count=created,
            updated_count=updated,
            skipped_count=skipped,
            failed_count=failed,
            message=message,
            detail=detail,
        )
        self.db.add(log)
        await self.db.flush()

    async def _send_welcome_emails(self):
        """同步完成后，逐条发送 Xero 客户欢迎邮件（带初始密码）。"""
        try:
            from app.core.models.tenant_settings import TenantSettings
            from app.services.email import build_smtp_config, send_notification

            ts_r = await self.db.execute(
                select(TenantSettings).where(TenantSettings.tenant_id == self.tenant_id)
            )
            ts = ts_r.scalar_one_or_none()
            smtp_config = build_smtp_config(ts)
            store_name = ts.store_name if ts else "SME Store"

            sent = 0
            for item in self._pending_emails:
                try:
                    from app.tasks.email_tasks import send_notification_task
                    send_notification_task.delay(
                        tenant_id=self.tenant_id,
                        template_key="xero_welcome",
                        to_email=item["email"],
                        variables={
                            "name": item["name"],
                            "email": item["email"],
                            "password": item["password"],
                            "store_name": store_name,
                        },
                        smtp_config=smtp_config,
                    )
                    sent += 1
                except Exception as e:
                    logger.warning("[xero_import] 发送欢迎邮件失败 %s: %s", item["email"], e)
            logger.info("[xero_import] 欢迎邮件发送完成: %d/%d", sent, len(self._pending_emails))
        except Exception as e:
            logger.error("[xero_import] 批量发送欢迎邮件异常: %s", e)
        finally:
            self._pending_emails.clear()

    async def _sync_customer_address(self, customer_id: int, data: dict, overwrite: bool) -> None:
        """根据 Xero Contact 的地址写入/更新本地默认收货地址。"""
        addr = _pick_address(data.get("addresses") or [])
        if not addr:
            return
        normalized = normalize_address(addr, self.country_name_mapping)
        if not normalized:
            return

        result = await self.db.execute(
            select(CustomerAddress).where(
                CustomerAddress.tenant_id == self.tenant_id,
                CustomerAddress.customer_id == customer_id,
                CustomerAddress.is_default == 1,
            ).limit(1)
        )
        existing = result.scalar_one_or_none()

        addr_name = (data["name"] or "")[:50]
        addr_phone = (data["phone"] or "")[:20]

        if existing:
            if not overwrite:
                return
            existing.country = normalized["country"]
            existing.province = normalized["province"][:100]
            existing.city = normalized["city"][:100]
            existing.street = normalized["street"][:200]
            existing.zip_code = normalized["zip_code"][:20]
            existing.name = addr_name
            existing.phone = addr_phone or existing.phone
        else:
            self.db.add(CustomerAddress(
                tenant_id=self.tenant_id,
                customer_id=customer_id,
                name=addr_name,
                phone=addr_phone,
                country=normalized["country"],
                province=normalized["province"][:100],
                city=normalized["city"][:100],
                street=normalized["street"][:200],
                zip_code=normalized["zip_code"][:20],
                is_default=1,
            ))

    # ── 客户同步 ──────────────────────────────────────────────────

    async def sync_contacts(self, mode: str = "manual") -> dict:
        conn = await self._get_active_connection()
        if not conn:
            return {"resource": "contacts", "created": 0, "updated": 0, "skipped": 0, "failed": 0, "error": "无活跃连接"}

        access_token = await self._ensure_token(conn)
        conn_id = conn.id
        xero_tenant_id = conn.xero_tenant_id
        state = await self._get_sync_state(conn_id, "contacts")

        try:
            # 始终全量拉取：Xero 的 UpdatedDateUTC 不会对所有字段变更都更新（同地址变更类似），
            # 增量同步会漏掉这些客户
            raw_contacts = await self.client.contacts(access_token, xero_tenant_id, None)
        except Exception as e:
            await self._write_log(conn_id, "contacts", mode, "failed", 0, 0, 0, 0, str(e)[:500])
            await self.db.commit()
            return {"resource": "contacts", "created": 0, "updated": 0, "skipped": 0, "failed": 0, "error": str(e)}

        created = updated = skipped = failed = 0
        # 每个客户独立 commit 后 conn/state 等对象属性会过期，循环内统一使用 conn_id 这个纯变量，
        # 避免在循环中重新触发对已过期 ORM 对象的隐式刷新（会引发 greenlet_spawn 异常）

        for raw in raw_contacts:
            try:
                if not raw.get("IsCustomer"):
                    skipped += 1
                    continue

                data = normalize_contact(raw)

                # 查找已有映射（按 xero_id）
                map_result = await self.db.execute(
                    select(XeroEntityMap).where(
                        XeroEntityMap.tenant_id == self.tenant_id,
                        XeroEntityMap.connection_id == conn_id,
                        XeroEntityMap.entity_type == "customer",
                        XeroEntityMap.xero_id == data["xero_id"],
                    )
                )
                existing_map = map_result.scalar_one_or_none()

                if existing_map:
                    # 更新已有客户
                    cust_result = await self.db.execute(
                        select(Customer).where(Customer.id == existing_map.local_id)
                    )
                    customer = cust_result.scalar_one_or_none()
                    if not customer:
                        skipped += 1
                        continue
                    customer.name = data["name"]
                    customer.phone = data["phone"]
                    # 邮箱从占位符更新为真实邮箱时，同步邮箱并生成登录密码
                    new_email = data["email"]
                    old_is_placeholder = (customer.email or "").endswith("@placeholder.local")
                    new_is_real = not new_email.endswith("@placeholder.local")
                    if old_is_placeholder and new_is_real:
                        customer.email = new_email
                        raw_password = _generate_password()
                        customer.set_attribute("password_hash", _hash_pwd(raw_password))
                        self._pending_emails.append({
                            "email": new_email,
                            "name": data["name"],
                            "password": raw_password,
                        })
                    elif new_is_real and customer.email != new_email:
                        customer.email = new_email
                    customer.set_attribute("xero", {"contact_id": data["xero_id"], "raw": data["raw"]})
                    existing_map.xero_updated_at = datetime.now(timezone.utc)
                    await self._sync_customer_address(customer.id, data, self.sync_address_overwrite)
                    updated += 1
                else:
                    # 按 email 查找本地客户
                    cust_result = await self.db.execute(
                        select(Customer).where(
                            Customer.tenant_id == self.tenant_id,
                            Customer.email == data["email"],
                        )
                    )
                    customer = cust_result.scalar_one_or_none()

                    if customer:
                        # 检查该客户是否已被另一个 xero contact 映射
                        local_map_result = await self.db.execute(
                            select(XeroEntityMap).where(
                                XeroEntityMap.tenant_id == self.tenant_id,
                                XeroEntityMap.connection_id == conn_id,
                                XeroEntityMap.entity_type == "customer",
                                XeroEntityMap.local_id == customer.id,
                            )
                        )
                        local_map = local_map_result.scalar_one_or_none()
                        if local_map:
                            # 已有映射，跳过（同一客户不能映射到多个 xero contact）
                            skipped += 1
                            continue
                        customer.name = data["name"]
                        customer.phone = data["phone"]
                        customer.set_attribute("xero", {"contact_id": data["xero_id"], "raw": data["raw"]})
                        await self._sync_customer_address(customer.id, data, self.sync_address_overwrite)
                        updated += 1
                    else:
                        # 判断是否有真实邮箱（非占位符）
                        has_real_email = not data["email"].endswith("@placeholder.local")
                        raw_password = _generate_password() if has_real_email else None
                        extra = {"xero": {"contact_id": data["xero_id"], "raw": data["raw"]}}
                        if raw_password:
                            extra["password_hash"] = _hash_pwd(raw_password)

                        customer = Customer(
                            tenant_id=self.tenant_id,
                            email=data["email"],
                            name=data["name"],
                            phone=data["phone"],
                            extra_data=extra,
                        )
                        self.db.add(customer)
                        await self.db.flush()
                        await self._sync_customer_address(customer.id, data, overwrite=True)
                        created += 1

                        # 发送欢迎邮件（有真实邮箱时）
                        if has_real_email and raw_password:
                            self._pending_emails.append({
                                "email": data["email"],
                                "name": data["name"],
                                "password": raw_password,
                            })

                    entity_map = XeroEntityMap(
                        tenant_id=self.tenant_id,
                        connection_id=conn_id,
                        entity_type="customer",
                        xero_id=data["xero_id"],
                        local_id=customer.id,
                        xero_updated_at=datetime.now(timezone.utc),
                    )
                    self.db.add(entity_map)
                # 每个客户独立提交：避免单个客户出错时 rollback 影响本次循环中其它已处理的客户，
                # 也避免 rollback 后 session 状态损坏导致后续客户全部失败
                await self.db.commit()
            except Exception as e:
                logger.warning("Xero contact sync failed for %s: %s", raw.get("ContactID"), e)
                await self.db.rollback()
                failed += 1

        # 更新同步状态（state 在循环中多次 commit 后可能已过期，重新查询一次）
        state = await self._get_sync_state(conn_id, "contacts")
        if not state:
            state = XeroSyncState(
                tenant_id=self.tenant_id,
                connection_id=conn_id,
                resource="contacts",
            )
            self.db.add(state)
        state.last_success_at = datetime.now(timezone.utc)

        await self._write_log(conn_id, "contacts", mode, "success", created, updated, skipped, failed)
        await self.db.commit()

        # 发送欢迎邮件（在 commit 之后，不阻塞同步流程）
        if self._pending_emails:
            await self._send_welcome_emails()

        return {"resource": "contacts", "created": created, "updated": updated, "skipped": skipped, "failed": failed}

    # ── 商品/库存同步 ─────────────────────────────────────────────

    async def sync_items(self, mode: str = "manual") -> dict:
        conn = await self._get_active_connection()
        if not conn:
            logger.warning("[xero_import] sync_items: 无活跃连接")
            return {"resource": "items", "created": 0, "updated": 0, "skipped": 0, "failed": 0, "error": "无活跃连接"}

        conn_id = conn.id
        xero_tenant_id = conn.xero_tenant_id
        logger.info("[xero_import] sync_items: 开始，connection_id=%d, mode=%s", conn_id, mode)
        access_token = await self._ensure_token(conn)
        state = await self._get_sync_state(conn_id, "items")

        try:
            raw_items = await self.client.items(access_token, xero_tenant_id, None)
            logger.info("[xero_import] sync_items: 从 Xero 获取到 %d 条 items", len(raw_items))
        except Exception as e:
            logger.error("[xero_import] sync_items: Xero API 调用失败: %s", e)
            await self._write_log(conn_id, "items", mode, "failed", 0, 0, 0, 0, str(e)[:500])
            await self.db.commit()
            return {"resource": "items", "created": 0, "updated": 0, "skipped": 0, "failed": 0, "error": str(e)}

        created = updated = skipped = failed = 0
        # 每个商品独立 commit，避免单条出错 rollback 拖累前面已处理的商品，详见 sync_contacts 的同类修复

        for idx, raw in enumerate(raw_items):
            try:
                logger.info("[xero_import] sync_items: 处理第 %d/%d 条, ItemID=%s, Code=%s",
                            idx + 1, len(raw_items), raw.get("ItemID", "?"), raw.get("Code", "?"))
                item = normalize_item(raw)

                # 查找已有映射
                map_result = await self.db.execute(
                    select(XeroEntityMap).where(
                        XeroEntityMap.tenant_id == self.tenant_id,
                        XeroEntityMap.connection_id == conn_id,
                        XeroEntityMap.entity_type.in_(["item_variant", "item_product"]),
                        XeroEntityMap.xero_id == item["xero_id"],
                    )
                )
                existing_map = map_result.scalar_one_or_none()

                if existing_map:
                    ok = await self._update_existing_item(existing_map, item)
                    if ok:
                        await self.db.commit()
                        updated += 1
                        continue
                    # 孤儿映射已删除，继续走下面的创建/匹配逻辑
                    await self.db.commit()
                    logger.info("[xero_import] 重新创建 item: xero_id=%s", item["xero_id"])

                # SKU 匹配：先查 variant，再查 product
                variant_result = await self.db.execute(
                    select(ProductVariant).where(
                        ProductVariant.tenant_id == self.tenant_id,
                        ProductVariant.sku == item["sku"],
                    ).limit(1)
                )
                variant = variant_result.scalar_one_or_none()

                product_result = await self.db.execute(
                    select(Product).where(
                        Product.tenant_id == self.tenant_id,
                        Product.sku == item["sku"],
                    ).limit(1)
                )
                product = product_result.scalar_one_or_none()

                entity_type, local_id = choose_item_target(
                    variant.id if variant else None,
                    product.id if product else None,
                )

                if entity_type == "variant":
                    self._apply_item_to_variant(variant, item)
                    local_id = variant.id
                    entity_type = "item_variant"
                    updated += 1
                elif entity_type == "product":
                    self._apply_item_to_product(product, item)
                    local_id = product.id
                    entity_type = "item_product"
                    updated += 1
                else:
                    # 新建 Product
                    new_stock = item["stock_qty"] if item["is_tracked"] and item["stock_qty"] is not None else 0
                    # 判断状态：库存为0且开启自动下架 → draft
                    if self.auto_delist_zero_stock and new_stock == 0:
                        new_status = "draft"
                    else:
                        new_status = "active" if item["is_sold"] else "draft"
                    product = Product(
                        tenant_id=self.tenant_id,
                        name=item["name"],
                        sku=item["sku"],
                        slug=item["sku"].lower().replace(" ", "-"),
                        description=item["description"],
                        base_price=Decimal(item["base_price"]) if self.sync_price else Decimal("0"),
                        stock_qty=new_stock,
                        status=new_status,
                        tax_class_id=self._resolve_tax_class_id(item),
                        extra_attributes={"xero": {"item_id": item["xero_id"], "raw": item["raw"]}},
                    )
                    self.db.add(product)
                    await self.db.flush()
                    local_id = product.id
                    entity_type = "item_product"
                    created += 1

                # 检查 local_id 是否已被映射
                local_map_result = await self.db.execute(
                    select(XeroEntityMap).where(
                        XeroEntityMap.tenant_id == self.tenant_id,
                        XeroEntityMap.connection_id == conn_id,
                        XeroEntityMap.entity_type == entity_type,
                        XeroEntityMap.local_id == local_id,
                    )
                )
                local_map = local_map_result.scalar_one_or_none()
                if not local_map:
                    entity_map = XeroEntityMap(
                        tenant_id=self.tenant_id,
                        connection_id=conn_id,
                        entity_type=entity_type,
                        xero_id=item["xero_id"],
                        local_id=local_id,
                        xero_updated_at=datetime.now(timezone.utc),
                    )
                    self.db.add(entity_map)
                # 每个商品独立 commit：理由同 sync_contacts 的修复，避免单条出错的 rollback
                # 拖累前面已处理的商品，也避免 session 状态损坏导致后续商品全部失败
                await self.db.commit()
            except Exception as e:
                logger.error("[xero_import] sync_items: 第 %d 条处理失败, ItemID=%s: %s",
                             idx + 1, raw.get("ItemID", "?"), e, exc_info=True)
                await self.db.rollback()
                failed += 1

        # 更新同步状态（state 在循环中多次 commit 后可能已过期，重新查询一次）
        state = await self._get_sync_state(conn_id, "items")
        if not state:
            state = XeroSyncState(
                tenant_id=self.tenant_id,
                connection_id=conn_id,
                resource="items",
            )
            self.db.add(state)
        state.last_success_at = datetime.now(timezone.utc)

        logger.info("[xero_import] sync_items: 完成! created=%d, updated=%d, skipped=%d, failed=%d, total_from_xero=%d",
                    created, updated, skipped, failed, len(raw_items))
        await self._write_log(conn_id, "items", mode, "success", created, updated, skipped, failed)
        await self.db.commit()
        return {"resource": "items", "created": created, "updated": updated, "skipped": skipped, "failed": failed}

    async def _update_existing_item(self, entity_map: XeroEntityMap, item: dict) -> bool:
        """更新已映射的本地对象。返回 True 表示成功，False 表示本地对象已不存在（孤儿映射）。"""
        if entity_map.entity_type == "item_variant":
            result = await self.db.execute(select(ProductVariant).where(ProductVariant.id == entity_map.local_id))
            target = result.scalar_one_or_none()
            if target:
                self._apply_item_to_variant(target, item)
            else:
                logger.warning("[xero_import] 孤儿映射: entity_map id=%d -> variant id=%d 不存在，删除映射",
                               entity_map.id, entity_map.local_id)
                await self.db.delete(entity_map)
                return False
        else:
            result = await self.db.execute(select(Product).where(Product.id == entity_map.local_id))
            target = result.scalar_one_or_none()
            if target:
                self._apply_item_to_product(target, item)
            else:
                logger.warning("[xero_import] 孤儿映射: entity_map id=%d -> product id=%d 不存在，删除映射",
                               entity_map.id, entity_map.local_id)
                await self.db.delete(entity_map)
                return False
        entity_map.xero_updated_at = datetime.now(timezone.utc)
        return True

    def _resolve_tax_class_id(self, item: dict) -> int | None:
        tax_type = item.get("sales_tax_type")
        if not tax_type or not self.tax_type_mapping:
            return None
        mapped = self.tax_type_mapping.get(tax_type)
        if mapped is not None:
            return int(mapped)
        return None

    def _apply_item_to_product(self, product: Product, item: dict):
        product.name = item["name"]
        product.description = item["description"]
        if self.sync_price:
            product.base_price = Decimal(item["base_price"])
        if item["is_tracked"] and item["stock_qty"] is not None:
            product.stock_qty = item["stock_qty"]
            if self.auto_delist_zero_stock and item["stock_qty"] == 0:
                product.status = "draft"
            elif product.status == "draft" and item["stock_qty"] > 0:
                product.status = "active"
        tax_class_id = self._resolve_tax_class_id(item)
        if tax_class_id is not None:
            product.tax_class_id = tax_class_id
        product.set_attribute("xero", {"item_id": item["xero_id"], "raw": item["raw"]})

    def _apply_item_to_variant(self, variant: ProductVariant, item: dict):
        if item["is_tracked"] and item["stock_qty"] is not None:
            variant.stock_qty = item["stock_qty"]

    # ── 订单推送到 Xero ─────────────────────────────────────────────

    async def _reverse_tax_mapping(self) -> dict[int, str]:
        """反转 tax_type_mapping: local_tax_class_id → Xero TaxType code."""
        return {int(v): k for k, v in self.tax_type_mapping.items()} if self.tax_type_mapping else {}

    def _build_invoice_payload(self, order: Order, items: list[OrderItem],
                               contact_xero_id: str, reverse_tax: dict[int, str],
                               product_map: dict[int, Product],
                               currency_code: str | None = None) -> dict:
        line_items = []
        for oi in items:
            snap = oi.product_snapshot or {}
            description = snap.get("name", f"Product #{oi.product_id}")
            if snap.get("variant_label"):
                description += f" - {snap['variant_label']}"

            line = {
                "Description": description,
                "Quantity": str(oi.quantity),
                "UnitAmount": str(oi.unit_price),
                "AccountCode": "200",
            }

            # 税率映射：从商品的 tax_class_id 查 Xero TaxType
            product = product_map.get(oi.product_id)
            if product and product.tax_class_id and product.tax_class_id in reverse_tax:
                line["TaxType"] = reverse_tax[product.tax_class_id]

            # 关联 Xero Item（如果有映射）
            if product:
                item_code = product.sku
                if item_code:
                    line["ItemCode"] = item_code

            line_items.append(line)

        # 运费作为单独行项
        if order.shipping_total and float(order.shipping_total) > 0:
            shipping_line = {
                "Description": "Shipping",
                "Quantity": "1",
                "UnitAmount": str(order.shipping_total),
                "AccountCode": "200",
            }
            line_items.append(shipping_line)

        # 折扣作为负数行
        if order.discount_total and float(order.discount_total) > 0:
            line_items.append({
                "Description": "Discount",
                "Quantity": "1",
                "UnitAmount": str(-abs(float(order.discount_total))),
                "AccountCode": "200",
            })

        payload = {
            "Type": "ACCREC",
            "Status": "DRAFT",
            "Contact": {"ContactID": contact_xero_id},
            "Date": order.created_at.strftime("%Y-%m-%d") if order.created_at else None,
            "Reference": order.order_no,
            "CurrencyCode": currency_code,
            "LineItems": line_items,
            "LineAmountTypes": "Inclusive",
        }
        return {k: v for k, v in payload.items() if v is not None}

    async def push_order_to_xero(self, order_id: int) -> dict:
        """推送单个订单到 Xero，创建 Draft Invoice。"""
        conn = await self._get_active_connection()
        if not conn:
            return {"success": False, "error": "无活跃 Xero 连接"}

        access_token = await self._ensure_token(conn)

        # 查订单
        order_result = await self.db.execute(
            select(Order).where(Order.id == order_id, Order.tenant_id == self.tenant_id)
        )
        order = order_result.scalar_one_or_none()
        if not order:
            return {"success": False, "error": f"订单 {order_id} 不存在"}

        # 检查是否已推送
        existing_map = await self.db.execute(
            select(XeroEntityMap).where(
                XeroEntityMap.tenant_id == self.tenant_id,
                XeroEntityMap.connection_id == conn.id,
                XeroEntityMap.entity_type == "invoice",
                XeroEntityMap.local_id == order_id,
            )
        )
        if existing_map.scalar_one_or_none():
            return {"success": False, "error": f"订单 {order.order_no} 已推送到 Xero"}

        # 查订单明细
        items_result = await self.db.execute(
            select(OrderItem).where(OrderItem.order_id == order_id)
        )
        order_items = items_result.scalars().all()
        if not order_items:
            return {"success": False, "error": "订单无明细"}

        # 查客户的 Xero ContactID
        cust_map_result = await self.db.execute(
            select(XeroEntityMap).where(
                XeroEntityMap.tenant_id == self.tenant_id,
                XeroEntityMap.connection_id == conn.id,
                XeroEntityMap.entity_type == "customer",
                XeroEntityMap.local_id == order.customer_id,
            )
        )
        cust_map = cust_map_result.scalar_one_or_none()
        if not cust_map:
            # 客户不在 Xero 映射中，尝试用客户信息创建
            return {"success": False, "error": "该订单客户未关联 Xero Contact，请先同步客户"}

        # 加载商品信息（用于 tax mapping 和 ItemCode）
        product_ids = [oi.product_id for oi in order_items if oi.product_id]
        product_map: dict[int, Product] = {}
        if product_ids:
            products_result = await self.db.execute(
                select(Product).where(Product.id.in_(product_ids))
            )
            for p in products_result.scalars().all():
                product_map[p.id] = p

        reverse_tax = await self._reverse_tax_mapping()

        # 查询租户默认货币
        from app.core.models.currency import Currency
        currency_result = await self.db.execute(
            select(Currency.code).where(
                Currency.tenant_id == self.tenant_id,
                Currency.is_default == 1,
            ).limit(1)
        )
        default_currency = currency_result.scalar_one_or_none()

        payload = self._build_invoice_payload(order, order_items, cust_map.xero_id, reverse_tax, product_map, default_currency)

        try:
            xero_invoice = await self.client.create_invoice(access_token, conn.xero_tenant_id, payload)
            xero_invoice_id = xero_invoice.get("InvoiceID", "")
            xero_invoice_number = xero_invoice.get("InvoiceNumber", "")

            # 保存映射
            entity_map = XeroEntityMap(
                tenant_id=self.tenant_id,
                connection_id=conn.id,
                entity_type="invoice",
                xero_id=xero_invoice_id,
                local_id=order_id,
                xero_updated_at=datetime.now(timezone.utc),
                extra_data={"invoice_number": xero_invoice_number, "order_no": order.order_no},
            )
            self.db.add(entity_map)

            # 在订单 extra_attributes 中记录
            order.set_attribute("xero_invoice", {
                "invoice_id": xero_invoice_id,
                "invoice_number": xero_invoice_number,
                "pushed_at": datetime.now(timezone.utc).isoformat(),
            })

            await self.db.flush()
            logger.info("[xero_import] 订单 %s 推送成功: InvoiceID=%s, Number=%s",
                        order.order_no, xero_invoice_id, xero_invoice_number)
            return {"success": True, "invoice_id": xero_invoice_id, "invoice_number": xero_invoice_number}
        except Exception as e:
            logger.error("[xero_import] 推送订单 %s 失败: %s", order.order_no, e, exc_info=True)
            return {"success": False, "error": str(e)[:500]}

    async def push_pending_orders(self, trigger_status: str, mode: str = "auto") -> dict:
        """批量推送指定状态的未推送订单到 Xero。"""
        conn = await self._get_active_connection()
        if not conn:
            return {"resource": "invoices", "created": 0, "updated": 0, "skipped": 0, "failed": 0, "error": "无活跃连接"}

        # 查找符合状态且未推送的订单
        already_pushed = select(XeroEntityMap.local_id).where(
            XeroEntityMap.tenant_id == self.tenant_id,
            XeroEntityMap.connection_id == conn.id,
            XeroEntityMap.entity_type == "invoice",
        ).scalar_subquery()

        orders_result = await self.db.execute(
            select(Order).where(
                Order.tenant_id == self.tenant_id,
                Order.status == trigger_status,
                Order.id.not_in(already_pushed),
            ).order_by(Order.created_at).limit(50)
        )
        orders = orders_result.scalars().all()

        if not orders:
            return {"resource": "invoices", "created": 0, "updated": 0, "skipped": 0, "failed": 0}

        created = skipped = failed = 0
        errors: list[str] = []

        for order in orders:
            try:
                result = await self.push_order_to_xero(order.id)
                if result.get("success"):
                    created += 1
                else:
                    error_msg = result.get("error", "unknown")
                    if "已推送" in error_msg:
                        skipped += 1
                    else:
                        failed += 1
                        errors.append(f"{order.order_no}: {error_msg}")
                await self.db.flush()
            except Exception as e:
                logger.error("[xero_import] push order %s failed: %s", order.order_no, e, exc_info=True)
                failed += 1
                errors.append(f"{order.order_no}: {str(e)[:200]}")

        msg = "; ".join(errors[:5]) if errors else None
        await self._write_log(conn.id, "invoices", mode, "success" if not failed else "partial",
                              created, 0, skipped, failed, msg)
        await self.db.commit()
        logger.info("[xero_import] push_pending_orders: created=%d, skipped=%d, failed=%d", created, skipped, failed)
        return {"resource": "invoices", "created": created, "updated": 0, "skipped": skipped, "failed": failed}
