"""
Tag Search Tool — LangChain @tool cho AI gọi.
AI sinh keywords + tags → Tool build SQL → 3 tầng cascade → Trả kết quả.
+ Tự động gọi Canifa Stock API → chỉ trả SP CÒN HÀNG.

Architecture:
  CỐ ĐỊNH (luôn giữ): product_type + color + gender + price
  BIẾN ĐỔI (fallback):
    Tầng 1: CỐ ĐỊNH + keywords  (từ nguyên văn user)
    Tầng 2: CỐ ĐỊNH + tags      (AI suy luận → tags chung)
    Tầng 3: CHỈ CỐ ĐỊNH         (product_type + color + gender)
"""

import json
import logging
import time
from datetime import datetime, timezone

import httpx
from langchain_core.tools import tool
from pydantic import BaseModel, Field

from common.starrocks_connection import get_db_connection
from .product_mapping import SYNONYM_TO_DB, get_related_lines

logger = logging.getLogger(__name__)

# Langfuse client — optional, graceful fallback nếu chưa init
try:
    from langfuse import get_client as _get_langfuse
except ImportError:
    _get_langfuse = None

# ═══════════════════════════════════════════════
# Stock API — Kiểm tra tồn kho Canifa
# ═══════════════════════════════════════════════
CANIFA_STOCK_API = "https://canifa.com/v1/middleware/stock_get_stock_list_parent"
_STOCK_TIMEOUT = 1.5  # seconds — giữ tổng < 200ms


async def _fetch_stock_batch(base_codes: list[str]) -> dict[str, list[dict]]:
    """
    Gọi Canifa Stock API batch → trả dict: product_color_code → [{size, qty, status}].
    Giữ ALL sizes (kể cả hết hàng) để AI biết full thông tin.
    Timeout 1.5s. Nếu lỗi → trả dict rỗng (graceful fallback).
    Returns: (stock_map, elapsed_ms, timed_out)
    """
    if not base_codes:
        return {}, 0.0, False

    sku_string = ",".join(base_codes)
    url = f"{CANIFA_STOCK_API}?skus={sku_string}"
    t0 = time.time()

    try:
        async with httpx.AsyncClient(timeout=_STOCK_TIMEOUT) as client:
            resp = await client.get(url)
            resp.raise_for_status()
            data = resp.json()

        elapsed = round((time.time() - t0) * 1000, 1)

        # Parse: gom ALL sizes theo product_color_code (kể cả hết hàng)
        stock_map: dict[str, list[dict]] = {}
        results = data.get("result", [])
        if isinstance(results, list):
            for item in results:
                sku_full = item.get("sku", "")     # "6TS25S018-SB055-S"
                qty = item.get("qty", 0) or 0
                if not sku_full:
                    continue
                parts = sku_full.rsplit("-", 1)
                if len(parts) == 2:
                    color_code = parts[0]  # "6TS25S018-SB055"
                    size = parts[1]        # "S"
                    status = "còn hàng" if qty > 0 else "hết hàng"
                    stock_map.setdefault(color_code, []).append({
                        "size": size, "qty": qty, "status": status
                    })

        logger.info("📦 Stock API: %d base_codes → %d color_codes | %.0fms", len(base_codes), len(stock_map), elapsed)
        return stock_map, elapsed, False

    except httpx.TimeoutException:
        elapsed = round((time.time() - t0) * 1000, 1)
        logger.warning("⏰ Stock API timeout (%.1fs=%.0fms) — skip stock filter", _STOCK_TIMEOUT, elapsed)
        return {}, elapsed, True
    except Exception as e:
        elapsed = round((time.time() - t0) * 1000, 1)
        logger.warning("⚠️ Stock API error (%.0fms): %s — skip stock filter", elapsed, e)
        return {}, elapsed, False


async def _enrich_with_stock(products: list[dict]) -> tuple[list[dict], bool, float, bool]:
    """
    Gọi stock API cho batch products → lọc chỉ giữ SP còn hàng + kèm chi tiết tồn.
    Returns: (filtered_products, stock_checked, stock_api_ms, timed_out)
    """
    if not products:
        return [], False, 0.0, False

    # Gom base_codes (deduplicate) — API nhận base code
    base_codes = list({p.get("internal_ref_code", "") for p in products if p.get("internal_ref_code")})
    if not base_codes:
        return products, False, 0.0, False

    stock_map, stock_api_ms, timed_out = await _fetch_stock_batch(base_codes)

    # Nếu stock API fail → trả nguyên, đánh dấu unchecked
    if not stock_map:
        return products, False, stock_api_ms, timed_out

    # Lọc: giữ SP có ÍT NHẤT 1 size còn hàng (qty > 0)
    # SP nào ALL sizes = 0 → bỏ, không tư vấn
    in_stock = []
    dropped = 0
    for p in products:
        color_code = p.get("product_color_code", "")
        if color_code in stock_map:
            sizes_detail = stock_map[color_code]
            total_qty = sum(s["qty"] for s in sizes_detail)
            if total_qty > 0:
                # Có ít nhất 1 size còn hàng → giữ, kèm FULL sizes (cả hết hàng)
                p["_stock_detail"] = sizes_detail
                p["_total_qty"] = total_qty
                in_stock.append(p)
            else:
                dropped += 1  # ALL sizes hết → bỏ
        # Không có trong stock_map = API không trả data → bỏ

    logger.info("📦 Stock filter: %d → %d in-stock, %d dropped | api=%.0fms timeout=%s",
                len(products), len(in_stock), dropped, stock_api_ms, timed_out)
    return in_stock if in_stock else products[:5], True, stock_api_ms, timed_out  # fallback top 5 nếu tất cả hết hàng



TABLE_NAME = "test_db.magento_product_dimension_with_text_embedding"

SELECT_COLUMNS = """
    internal_ref_code,
    magento_ref_code,
    product_color_code,
    product_name,
    master_color,
    product_color_name,
    product_image_url_thumbnail,
    product_web_url,
    sale_price,
    original_price,
    COALESCE(discount_amount, 0) AS discount_amount,
    ROUND(CASE WHEN original_price > 0
        THEN ((original_price - sale_price) / original_price * 100)
        ELSE 0 END, 0) AS discount_percent,
    age_by_product,
    gender_by_product,
    product_line_vn,
    COALESCE(quantity_sold, 0) AS quantity_sold,
    COALESCE(is_new_product, 0) AS is_new_product,
    size_scale,
    description_text
"""


# ═══════════════════════════════════════════════
# Pydantic Schema — AI sẽ fill vào đây
# ═══════════════════════════════════════════════
class TagSearchInput(BaseModel):
    """Schema cho hybrid search: keywords (user) + tags (AI suy luận)."""

    model_config = {"extra": "forbid"}

    # ① KEYWORDS — từ NGUYÊN VĂN của user (ưu tiên cao, search TRƯỚC)
    keywords: list[str] = Field(
        default=[],
        description=(
            "Từ khoá NGUYÊN VĂN từ câu hỏi khách. "
            "VD: khách nói '30/4' → keywords=['30/4']. "
            "Khách nói 'cờ đỏ sao vàng' → keywords=['cờ đỏ sao vàng']. "
            "Khách nói 'cotton ống rộng' → keywords=['cotton', 'ống rộng']. "
            "Tối đa 2. Để [] nếu khách không nhắc gì đặc biệt."
        )
    )

    # ② TAGS — AI suy luận từ 4 TRỤC CỐ ĐỊNH (BẮT BUỘC có prefix!)
    tags: list[str] = Field(
        default=[],
        description=(
            "AI suy luận ý định khách → chọn từ 4 TRỤC CỐ ĐỊNH (BẮT BUỘC có prefix!): "
            "Trục 1 (occ:): occ:di_lam, occ:di_choi, occ:di_tiec, occ:di_hoc, occ:mac_nha, occ:the_thao, occ:di_bien, occ:du_lich, occ:da_ngoai, occ:di_ngu. "
            "Trục 2a (wthr:): wthr:mua_he, wthr:mua_dong, wthr:giao_mua, wthr:troi_mua, wthr:troi_nang. "
            "Trục 2b (func:): func:thoang_mat, func:giu_am, func:tham_hut, func:nhanh_kho, func:chong_uv, func:can_gio. "
            "Trục 3 (style:): style:thanh_lich, style:nang_dong, style:basic, style:ca_tinh, style:de_thuong, style:tre_trung, style:toi_gian, style:smart_casual. "
            "Trục 4 (fit:): fit:oversize, fit:slim, fit:regular, fit:wide_leg, fit:cropped, fit:relaxed. "
            "VD: 'đi tiệc' → tags=['occ:di_tiec']. 'mùa đông' → tags=['wthr:mua_dong', 'func:giu_am']. "
            "KHÔNG tự nghĩ tag mới! PHẢI giữ prefix! Tối đa 3."
        )
    )

    # ═══ CỐ ĐỊNH — Luôn giữ trong mọi tầng ═══
    product_line_vn: list[str] = Field(
        default=[],
        description=(
            "Dòng sản phẩm. CHÍNH XÁC lời user nói: 'áo phông', 'váy liền', 'đồ lót', 'quần jean'... "
            "Tool sẽ tự động chuẩn hoá từ đồng nghĩa (VD: 'áo thun' → 'Áo phông'). "
            "Nếu khách nói chung chung ('đồ mùa đông', 'đồ tập') hoặc không nhắc, để []. "
        ),
    )
    gender_by_product: str | None = Field(
        default=None,
        description="Giới tính. GIÁ TRỊ HỢP LỆ: women, men, boy, girl, unisex, newborn",
    )
    age_by_product: str | None = Field(
        default=None,
        description="Độ tuổi. GIÁ TRỊ HỢP LỆ: adult, kid, others",
    )
    master_color: str | None = Field(
        default=None,
        description=(
            "Màu sắc. Ghi CHÍNH XÁC từ khách nói: 'trắng', 'đen', 'đỏ'... "
            "VD: 'áo cờ đỏ sao vàng' → color='đỏ'."
        ),
    )
    price_min: int | None = Field(default=None, description="Giá thấp nhất (VND)")
    price_max: int | None = Field(default=None, description="Giá cao nhất (VND)")
    discount_min: int | None = Field(
        default=None, 
        description="% giảm giá TỐI THIỂU. VD: 'giảm 50%' → 50. 'hàng sale' → 1."
    )
    discount_max: int | None = Field(
        default=None, 
        description="% giảm giá TỐI ĐA. VD: 'giảm dưới 30%' → 30."
    )
    discovery_mode: str | None = Field(
        default=None,
        description="'new' = hàng mới, 'best_seller' = bán chạy. Chỉ dùng khi khách nói rõ.",
    )
    magento_ref_code: str | None = Field(
        default=None,
        description="Mã SKU cụ thể. Chỉ điền khi khách cung cấp.",
    )
    reasoning: str | None = Field(
        default=None,
        description=(
            "SUY LUẬN: Giải thích ngắn gọn TẠI SAO bạn chọn params này. "
            "VD: '2/9 = Quốc Khánh → áo đỏ đi chơi', 'khách muốn sịp mặc đi biển → giữ nguyên sịp, tag đi biển'. "
            "Viết 1-2 câu bằng tiếng Việt."
        ),
    )


# ═══════════════════════════════════════════════
# SQL Builder
# ═══════════════════════════════════════════════
def _build_fixed_clauses(req: TagSearchInput, params: list) -> list[str]:
    """Build các filter CỐ ĐỊNH (luôn giữ trong mọi tầng)."""
    clauses = []

    # Product line (list) — resolve synonym → search cột product_line_vn
    if req.product_line_vn:
        resolved_types = []
        for pt_raw in req.product_line_vn:
            pt = pt_raw.strip()
            if not pt:
                continue
            mapped = SYNONYM_TO_DB.get(pt.lower(), pt)
            related = get_related_lines(mapped)
            resolved_types.extend(related)
        resolved_types = list(dict.fromkeys(resolved_types))
        if resolved_types:
            in_vals = []
            like_clauses = []
            # Broad terms (áo, quần, váy...) → LIKE vì không phải exact product_line
            # VD: khách nói 'quần' → LIKE '%quần%' → match 'Quần jean', 'Quần lót', etc.
            # KHÔNG map 'quần' sang product_line_vn BITMAP vì sẽ 0 kết quả
            BROAD_TERMS = {"áo", "quần", "váy", "đồ", "bộ", "phụ kiện", "giày", "dép", "túi", "balo", "mũ", "nón", "tất"}
            for rt in resolved_types:
                if rt.lower() in BROAD_TERMS:
                    # Broad term: dùng LOWER(product_name) LIKE để rộng hơn
                    # (broad terms sẽ không match chính xác product_line_vn column)
                    like_clauses.append("LOWER(product_name) LIKE %s")
                    params.append(f"%{rt.lower()}%")
                else:
                    # Exact product line → BITMAP index (không LOWER)
                    in_vals.append(rt)
                    params.append(rt)
            sub_clauses = []
            if in_vals:
                placeholders = ", ".join(["%s"] * len(in_vals))
                sub_clauses.append(f"product_line_vn IN ({placeholders})")
            sub_clauses.extend(like_clauses)
            if sub_clauses:
                clauses.append("(" + " OR ".join(sub_clauses) + ")")
            logger.info("🏷️ [MAPPING] product_line_vn %s → resolved %s", req.product_line_vn, resolved_types)

    # Gender — BITMAP: KHÔNG wrap LOWER()
    if req.gender_by_product:
        gender_lower = req.gender_by_product.lower().strip()
        if gender_lower in ("women", "nữ", "female"):
            gender_db = "female"
        elif gender_lower in ("men", "nam", "male"):
            gender_db = "male"
        else:
            gender_db = gender_lower
        if gender_db in ("female", "male"):
            params.append(gender_db)
            params.append("unisex")
            clauses.append("gender_by_product IN (%s, %s)")
        else:
            params.append(gender_db)
            clauses.append("gender_by_product = %s")

    # Age — BITMAP
    if req.age_by_product:
        age_lower = req.age_by_product.lower().strip()
        if age_lower in ("baby", "newborn"):
            params.append("kid")
        else:
            params.append(age_lower)
        clauses.append("age_by_product = %s")

    # Color — sau khi bạn đánh NGRAMBF trên master_color:
    # product_color_name LIKE không LOWER → NGRAMBF hoạt động
    # master_color LIKE không LOWER → NGRAMBF hoạt động
    if req.master_color:
        c = req.master_color.strip()  # giữ nguyên case để NGRAMBF match
        params.append(f"%{c}%")
        params.append(f"%{c}%")
        clauses.append("(master_color LIKE %s OR product_color_name LIKE %s)")

    # Price
    if req.price_min is not None:
        params.append(req.price_min)
        clauses.append(f"sale_price >= %s")
    if req.price_max is not None:
        params.append(req.price_max)
        clauses.append(f"sale_price <= %s")

    # Discount
    if req.discount_min is not None:
        params.append(req.discount_min)
        # discount_percent is usually derived as ((original - sale) / original * 100)
        # Let's assume there is a discount_percent column, but looking at SELECT_COLUMNS, 
        # discount_percent is a calculated field. We should calculate it in the WHERE clause or use original_price > sale_price.
        # Actually in magento_product_dimension_with_text_embedding there probably IS some discount logic, but let's use:
        # WHERE original_price > 0 AND ((original_price - sale_price) / original_price * 100) >= X
        clauses.append(f"(original_price > 0 AND ((original_price - sale_price) / original_price * 100) >= %s)")
    if req.discount_max is not None:
        params.append(req.discount_max)
        clauses.append(f"(original_price > 0 AND ((original_price - sale_price) / original_price * 100) <= %s)")

    # Discovery mode
    if req.discovery_mode:
        mode = req.discovery_mode.lower().strip()
        if mode == "new":
            clauses.append("is_new_product = 1")
        elif mode == "best_seller":
            clauses.append("quantity_sold > 0")

    return clauses


# ═══════════════════════════════════════════════
# Tag → Indexed column mapping (ƯU TIÊN)
# Thay vì LIKE description_text → dùng BITMAP cột riêng
# ═══════════════════════════════════════════════

# Tags map thẳng vào cột BITMAP riêng biệt
# format: tag → (column_name, value_in_db)
TAG_TO_BITMAP_COL: dict[str, tuple[str, str]] = {
    # Trục 3 — style (cột `style` có BITMAP index)
    # DB values: Street, Basic Update, Feminine, Basic, Athleisure, Cute, Essential, Smart Casual, Utility, Trend, Dynamic, Preppy
    "style:thanh_lich":  ("style", "Feminine"),
    "style:nang_dong":   ("style", "Dynamic"),
    "style:basic":       ("style", "Basic"),
    "style:ca_tinh":     ("style", "Street"),
    "style:de_thuong":   ("style", "Cute"),
    "style:tre_trung":   ("style", "Trend"),
    "style:toi_gian":    ("style", "Essential"),
    "style:smart_casual":("style", "Smart Casual"),
    # Trục 4 — fitting (cột `fitting` có BITMAP index)
    # DB values: Boxy, Skinny, Oversize, Relax, Slimfit, Regular, Slim
    "fit:oversize":  ("fitting", "Oversize"),
    "fit:slim":      ("fitting", "Slim"),
    "fit:regular":   ("fitting", "Regular"),
    "fit:wide_leg":  ("fitting", "Relax"),
    "fit:cropped":   ("fitting", "Boxy"),
    "fit:relaxed":   ("fitting", "Relax"),
    # Trục 2a — season_sale (cột `season_sale` có BITMAP index)
    # DB values thực tế: Year, Winter, Summer
    "wthr:mua_he":   ("season_sale", "Summer"),
    "wthr:mua_dong": ("season_sale", "Winter"),
    # Trục 1 — occ: tags map sang season_sale khi có thể
    # Đi biển/du lịch mùa hè → Summer; thể thao → Athleisure style
    "occ:di_bien":   ("season_sale", "Summer"),  # Biển = mùa hè
    "occ:du_lich":   ("season_sale", "Summer"),  # Du lịch = mùa hè
    "occ:the_thao":  ("style", "Athleisure"),    # Thể thao = Athleisure style
}

# Tags fallback LIKE description_text (không có cột riêng)
# Chỉ dùng khi KHÔNG có BITMAP mapping ở trên.
# QUAN TRỌNG: search FULL PHRASE trong description_text (không tách word)
TAG_TO_SEARCH_TEXT: dict[str, str] = {
    # Trục 1 — Hoàn cảnh & Dịp (chỉ các tag chưa có BITMAP)
    "occ:di_lam":   "đi làm",
    "occ:di_choi":  "đi chơi",
    "occ:di_tiec":  "đi tiệc",
    "occ:di_hoc":   "đi học",
    "occ:mac_nha":  "mặc nhà",
    "occ:da_ngoai": "dã ngoại",
    "occ:di_ngu":   "ngủ",
    # Trục 2a — thời tiết không map được season_sale
    "wthr:giao_mua":  "giao mùa",
    "wthr:troi_mua":  "mưa",
    "wthr:troi_nang": "nắng",
    # Trục 2b — Tính năng (search trong description_text)
    "func:thoang_mat": "thoáng mát",
    "func:giu_am":     "giữ ấm",
    "func:tham_hut":   "thấm hút",
    "func:nhanh_kho":  "nhanh khô",
    "func:chong_uv":   "chống UV",
    "func:can_gio":    "cản gió",
}


def _build_tag_clauses(tags: list[str], params: list) -> str:
    """
    Smart tag routing:
    - Tags có cột BITMAP riêng → dùng BITMAP (= trực tiếp, cực nhanh)
    - Tags còn lại → search description_text FULL PHRASE (không tách từ)
      Lý do: description_text chứa câu mô tả, search FULL PHRASE 'đi biển' sẽ match
      hơn là search từng chữ riêng với AND.
    Returns: OR clause kết hợp cả 2 path.
    """
    if not tags:
        return ""

    bitmap_by_col: dict[str, list[str]] = {}  # col → [values]
    text_terms: list[str] = []                 # full phrases cho LIKE search

    for tag in tags:
        tag_lower = tag.strip().lower()
        if tag_lower in TAG_TO_BITMAP_COL:
            col, val = TAG_TO_BITMAP_COL[tag_lower]
            bitmap_by_col.setdefault(col, []).append(val)
        elif tag_lower in TAG_TO_SEARCH_TEXT:
            text_terms.append(TAG_TO_SEARCH_TEXT[tag_lower])
        else:
            # Unknown tag → text search thẳng
            text_terms.append(tag_lower.replace(":", " ").replace("_", " "))

    all_clauses = []

    # ① BITMAP clauses — cực nhanh, ưu tiên cao nhất
    for col, values in bitmap_by_col.items():
        if len(values) == 1:
            params.append(values[0])
            all_clauses.append(f"{col} = %s")
        else:
            placeholders = ", ".join(["%s"] * len(values))
            params.extend(values)
            all_clauses.append(f"{col} IN ({placeholders})")

    # ② Text fallback — search FULL PHRASE trong description_text
    # KHÔNG tách từ, KHÔNG dùng AND — chỉ search nguyên cụm
    # vì description_text là câu mô tả đủ ngữ cảnh
    if text_terms:
        term_clauses = []
        for term in text_terms:
            term = term.strip()
            if not term:
                continue
            # Search full phrase trong description_text + product_name
            params.append(f"%{term}%")
            params.append(f"%{term}%")
            term_clauses.append(f"(LOWER(description_text) LIKE %s OR product_name LIKE %s)")
        if term_clauses:
            all_clauses.append(f"({' OR '.join(term_clauses)})")

    if all_clauses:
        return f"({' OR '.join(all_clauses)})"
    return ""


def _resolve_tag(tag: str) -> str:
    """Legacy helper — giữ cho backward compat."""
    return TAG_TO_SEARCH_TEXT.get(tag.strip().lower(), tag)


def _build_search_clause(search_terms: list[str], params: list, resolve_tags: bool = False) -> str:
    """
    Build OR clause cho keywords (user input).
    Dùng product_name LIKE (NGRAMBF) — KHÔNG LOWER() để dùng được index.
    description_text chỉ search nếu keyword ngắn (< 4 chars, không dùng NGRAMBF được).
    """
    term_clauses = []
    for term in search_terms:
        t = _resolve_tag(term) if resolve_tags else term.strip()
        if not t:
            continue
        words = t.split()
        if not words:
            continue
        word_clauses = []
        for word in words:
            if len(word) >= 4:
                # NGRAMBF gram_num=4: KHÔNG wrap LOWER() → index được dùng
                params.append(f"%{word}%")
                word_clauses.append("product_name LIKE %s")
            else:
                # Short word: NGRAMBF không match được gram < 4 → fallback cả 2 cột
                params.append(f"%{word}%")
                params.append(f"%{word}%")
                word_clauses.append("(product_name LIKE %s OR LOWER(description_text) LIKE %s)")
        if word_clauses:
            term_clauses.append(f"({' AND '.join(word_clauses)})")
    if term_clauses:
        return f"({' OR '.join(term_clauses)})"
    return ""


def _build_full_query(fixed_clauses: list[str], search_clause: str | None) -> str:
    """Combine fixed + search clauses thành SQL hoàn chỉnh."""
    all_clauses = list(fixed_clauses)
    if search_clause:
        all_clauses.append(search_clause)

    where = " AND ".join(all_clauses) if all_clauses else "1=1"
    return f"""
    SELECT {SELECT_COLUMNS}
    FROM {TABLE_NAME}
    WHERE {where}
    ORDER BY quantity_sold DESC NULLS LAST, sale_price ASC
    LIMIT 20
    """


# ═══════════════════════════════════════════════
# 3-Tier Cascading Search
# ═══════════════════════════════════════════════
async def _cascading_search(req: TagSearchInput, db) -> tuple[list, int]:
    """
    3 tầng fallback:
      Tầng 1: CỐ ĐỊNH + keywords  (từ nguyên văn user)
      Tầng 2: CỐ ĐỊNH + tags      (AI suy luận → tags chung)
      Tầng 3: CHỈ CỐ ĐỊNH         (product_type + color + gender)

    Returns: (products, tier_matched)
    """

    # ---- Tầng 1: CỐ ĐỊNH + KEYWORDS ----
    if req.keywords:
        params = []
        fixed = _build_fixed_clauses(req, params)
        search = _build_search_clause(req.keywords, params)  # dùng NGRAMBF product_name
        if search:
            sql = _build_full_query(fixed, search)
            products = await db.execute_query_async(sql, params=tuple(params))
            if products:
                logger.info("🎯 Tầng 1 (keywords→NGRAMBF) match! %d kết quả", len(products))
                return products, 1

    # ---- Tầng 2: CỐ ĐỊNH + TAGS (BITMAP/LIKE hybrid) ----
    if req.tags:
        params = []
        fixed = _build_fixed_clauses(req, params)
        search = _build_tag_clauses(req.tags, params)  # BITMAP-first routing!
        if search:
            sql = _build_full_query(fixed, search)
            products = await db.execute_query_async(sql, params=tuple(params))
            if products:
                logger.info("🎯 Tầng 2 (tags→BITMAP) match! %d kết quả", len(products))
                return products, 2

    # ---- Tầng 3: CHỈ CỐ ĐỊNH ----
    params = []
    fixed = _build_fixed_clauses(req, params)
    sql = _build_full_query(fixed, None)
    products = await db.execute_query_async(sql, params=tuple(params))
    if products:
        logger.info("🎯 Tầng 3 (chỉ cố định) match! %d kết quả", len(products))
        return products, 3
    logger.info("🎯 Tầng 3 (chỉ cố định) match! 0 kết quả")

    # ---- Tầng 4: DROP GENDER, chỉ giữ product_line ----
    # VD: "sịp nam" → gender=men returns 0 → try again without gender
    if req.product_line_vn and req.gender_by_product:
        logger.info("🔄 Tầng 4: thử drop gender '%s' để tìm rộng hơn...", req.gender_by_product)
        saved_gender = req.gender_by_product
        req.gender_by_product = None  # drop gender
        params = []
        fixed = _build_fixed_clauses(req, params)
        sql = _build_full_query(fixed, None)
        products = await db.execute_query_async(sql, params=tuple(params))
        req.gender_by_product = saved_gender  # restore
        if products:
            logger.info("🎯 Tầng 4 (drop gender) match! %d kết quả", len(products))
            return products, 4

    return [], 3


def _format_products(products: list, stock_checked: bool = False) -> list[dict]:
    """Format output cho AI đọc. Kèm stock info nếu có."""
    formatted = []
    for p in products[:15]:
        sale = float(p.get("sale_price") or 0)
        orig = float(p.get("original_price") or 0)
        has_discount = sale < orig and orig > 0
        disc_pct = int(p.get("discount_percent") or 0)
        item = {
            "sku": p.get("magento_ref_code", ""),
            "name": p.get("product_name", ""),
            "price": int(sale),
            "original_price": int(orig),
            "discount": f"-{disc_pct}%" if has_discount else None,
            "color": p.get("master_color", ""),
            "gender": p.get("gender_by_product", ""),
            "product_line": p.get("product_line_vn", ""),
            "image": p.get("product_image_url_thumbnail", ""),
            "url": p.get("product_web_url", ""),
            "sizes": p.get("size_scale", ""),
        }
        # Stock info nếu đã check
        if stock_checked:
            stock_detail = p.get("_stock_detail", [])
            total_qty = p.get("_total_qty", 0)
            item["in_stock"] = total_qty > 0
            item["total_qty"] = total_qty
            item["stock"] = stock_detail  # [{"size": "S", "qty": 2}, {"size": "M", "qty": 1}]
        formatted.append(item)
    return formatted


# ═══════════════════════════════════════════════
# SKU Lookup (đặc biệt, bypass cascade)
# ═══════════════════════════════════════════════
def _build_sku_query(code: str) -> tuple[str, list]:
    """Build query tìm theo mã SKU.
    code đã được .upper() → KHÔNG wrap UPPER() trong SQL để dùng được PRIMARY KEY index.
    """
    code = code.strip().upper()
    sql = f"""
    SELECT {SELECT_COLUMNS}
    FROM {TABLE_NAME}
    WHERE (magento_ref_code = %s
           OR internal_ref_code = %s
           OR magento_ref_code LIKE %s)
    ORDER BY quantity_sold DESC NULLS LAST
    LIMIT 20
    """
    return sql, [code, code, f"{code}%"]


# ═══════════════════════════════════════════════
# Tool Entry Point
# ═══════════════════════════════════════════════
@tool(args_schema=TagSearchInput)
async def tag_search_tool(
    keywords: list[str] | None = None,
    tags: list[str] | None = None,
    product_line_vn: list[str] | None = None,
    gender_by_product: str | None = None,
    age_by_product: str | None = None,
    master_color: str | None = None,
    price_min: int | None = None,
    price_max: int | None = None,
    discount_min: int | None = None,
    discount_max: int | None = None,
    discovery_mode: str | None = None,
    magento_ref_code: str | None = None,
    reasoning: str | None = None,
) -> str:
    """
    Tìm kiếm sản phẩm CANIFA + tự động check tồn kho.
    Chỉ trả về sản phẩm CÒN HÀNG (nếu stock API available).
    """
    tool_start = time.time()
    now_utc = datetime.now(timezone.utc)

    req = TagSearchInput(
        keywords=keywords or [],
        tags=tags or [],
        product_line_vn=product_line_vn or [],
        gender_by_product=gender_by_product,
        age_by_product=age_by_product,
        master_color=master_color,
        price_min=price_min,
        price_max=price_max,
        discount_min=discount_min,
        discount_max=discount_max,
        discovery_mode=discovery_mode,
        magento_ref_code=magento_ref_code,
    )

    db = get_db_connection()

    # ── Langfuse: get current observation context (set by controller) ──
    lf = None
    try:
        lf = _get_langfuse() if _get_langfuse else None
    except Exception:
        pass

    try:
        # ═══ [1] DB SEARCH ═══
        db_start = time.time()
        db_span = None

        if lf:
            try:
                db_span = lf.start_span(
                    name="tag-search-db",
                    start_time=datetime.now(timezone.utc),
                    input={
                        "keywords": keywords,
                        "tags": tags,
                        "product_line": product_line_vn,
                        "gender": gender_by_product,
                        "color": master_color,
                        "sku": magento_ref_code,
                    },
                    metadata={"tier": "cascade", "table": "magento_product_dimension"},
                )
            except Exception as _e:
                logger.debug("Langfuse span init failed: %s", _e)

        if req.magento_ref_code:
            sql, params = _build_sku_query(req.magento_ref_code)
            products = await db.execute_query_async(sql, params=tuple(params))
            tier = 0
        else:
            products, tier = await _cascading_search(req, db)

        db_ms = round((time.time() - db_start) * 1000, 1)

        if db_span:
            try:
                db_span.end(
                    end_time=datetime.now(timezone.utc),
                    output={"count": len(products), "tier": tier, "elapsed_ms": db_ms},
                    metadata={"db_ms": db_ms, "tier": tier},
                )
            except Exception:
                pass

        logger.info("📊 [DB] tier=%d found=%d in %.0fms", tier, len(products), db_ms)

        # ═══ [2] STOCK API ═══
        stock_span = None
        if lf and products:
            try:
                stock_span = lf.start_span(
                    name="tag-search-stock-api",
                    start_time=datetime.now(timezone.utc),
                    input={"product_count": len(products),
                           "base_codes": [p.get("internal_ref_code") for p in products[:5]]},
                    metadata={"api": CANIFA_STOCK_API, "timeout_s": _STOCK_TIMEOUT},
                )
            except Exception as _e:
                logger.debug("Langfuse stock span init failed: %s", _e)

        products, stock_checked, stock_ms, stock_timed_out = await _enrich_with_stock(products)

        if stock_span:
            try:
                stock_span.end(
                    end_time=datetime.now(timezone.utc),
                    output={
                        "in_stock_count": len(products),
                        "stock_checked": stock_checked,
                        "timed_out": stock_timed_out,
                        "elapsed_ms": stock_ms,
                    },
                    metadata={"stock_ms": stock_ms, "timed_out": stock_timed_out},
                    level="WARNING" if stock_timed_out else "DEFAULT",
                )
            except Exception:
                pass

        total_ms = round((time.time() - tool_start) * 1000, 1)

        logger.info(
            "🏷️ [SEARCH+STOCK] keywords=%s tags=%s type=%s color=%s gender=%s | "
            "tier=%d results=%d db=%.0fms stock=%.0fms total=%.0fms checked=%s timeout=%s",
            keywords, tags, product_line_vn, master_color, gender_by_product,
            tier, len(products), db_ms, stock_ms, total_ms, stock_checked, stock_timed_out,
        )

        formatted = _format_products(products, stock_checked=stock_checked)

        return json.dumps({
            "status": "success",
            "count": len(formatted),
            "tier": tier,
            "stock_checked": stock_checked,
            "stock_timed_out": stock_timed_out,
            "elapsed_ms": total_ms,
            "timing": {
                "db_ms": db_ms,
                "stock_api_ms": stock_ms,
                "total_ms": total_ms,
            },
            "reasoning": reasoning or "",
            "keywords_used": keywords or [],
            "tags_used": tags or [],
            "products": formatted,
        }, ensure_ascii=False, default=str)

    except Exception as e:
        logger.error("❌ Search tool error: %s", e, exc_info=True)
        return json.dumps({"status": "error", "message": str(e)})

