"""
Fashion Matches — API Route
Manages AI-powered fashion matching (ai_matches) per product.

Endpoints:
  GET  /api/fashion-matches/{code}              → lấy ai_matches
  POST /api/fashion-matches/{code}/update       → lưu manual edits
  POST /api/fashion-matches/{code}/regen        → trigger AI engine 1 SP
  POST /api/fashion-matches/batch               → trigger engine toàn bộ
  GET  /api/fashion-matches/batch/status        → poll batch progress
  GET  /api/fashion-matches/rules/config        → lấy fashion_rules.json
  PUT  /api/fashion-matches/rules/config        → cập nhật rules
  GET  /api/fashion-matches/rules/meta          → structured metadata (colors, styles, occasions)
  POST /api/fashion-matches/color-logic         → kiểm tra synergy 2 màu
  POST /api/fashion-matches/outfit-suggest      → gợi ý outfit đầy đủ từ 1 product code
  POST /api/fashion-matches/score-test          → test pairwise score breakdown
"""

import json
import logging
import os

from fastapi import APIRouter, BackgroundTasks, Query, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel

from common.pool_wrapper import get_pooled_connection_compat

logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/fashion-matches", tags=["Fashion Matches"])

TABLE = "dashboard_canifa.ultra_descriptions"
RULES_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "worker", "fashion_rules.json")


# ─── Pydantic ────────────────────────────────────────────────
class UpdateMatchesRequest(BaseModel):
    ai_matches: dict


class UpdateRulesRequest(BaseModel):
    rules: dict


class OutfitSuggestRequest(BaseModel):
    code: str
    occasion: str | None = None


class ColorLogicRequest(BaseModel):
    src_color: str
    tgt_color: str


# ─── Helpers ─────────────────────────────────────────────────
# NOTE: Cột ai_matches trong ultra_descriptions đã bị deprecated.
# Data phối đồ giờ nằm ở bảng: ai_outfit_product_matches
# GET endpoint dùng compute_dynamic_rule_matches() real-time.


def _run_engine_background(code: str | None = None):
    try:
        from worker.stylist_engine import StylistEngine

        engine = StylistEngine()
        if code:
            engine.run_for_code(code)
        else:
            engine.run_batch()
    except Exception as e:
        logger.error("[FashionMatches] engine error: %s", e)


def _load_rules() -> dict:
    with open(RULES_PATH, encoding="utf-8") as f:
        return json.load(f)


def _detect_color_key(color_str: str, color_keys: dict) -> str | None:
    c = color_str.lower()
    for key, variants in color_keys.items():
        if any(v in c for v in variants):
            return key
    return None


def _color_group_explain(src_key: str | None, tgt_key: str | None, rules: dict) -> dict:
    color_groups = rules.get("color_groups", {})
    group_matrix = rules.get("color_group_matrix", {})
    color_matrix = rules.get("color_matrix", {})
    default_score = rules.get("default_color_score", 12)

    src_group = color_groups.get(src_key) if src_key else None
    tgt_group = color_groups.get(tgt_key) if tgt_key else None

    if src_group and tgt_group:
        score = group_matrix.get(src_group, {}).get(tgt_group, default_score)
        advice = _color_advice(src_group, tgt_group, src_key, tgt_key)
        return {
            "src_key": src_key,
            "src_group": src_group,
            "tgt_key": tgt_key,
            "tgt_group": tgt_group,
            "score": score,
            "max": 30,
            "method": "group_matrix",
            "advice": advice,
        }

    if src_key and tgt_key:
        score = color_matrix.get(src_key, {}).get(tgt_key, default_score)
        return {
            "src_key": src_key,
            "src_group": src_group or "?",
            "tgt_key": tgt_key,
            "tgt_group": tgt_group or "?",
            "score": score,
            "max": 30,
            "method": "color_matrix",
            "advice": f"Phối {src_key} + {tgt_key} (legacy matrix)",
        }

    return {
        "src_key": src_key or "?",
        "src_group": src_group or "?",
        "tgt_key": tgt_key or "?",
        "tgt_group": tgt_group or "?",
        "score": default_score,
        "max": 30,
        "method": "default",
        "advice": "Không xác định được màu — dùng điểm mặc định",
    }


def _color_advice(src_group: str, tgt_group: str, src_key: str | None, tgt_key: str | None) -> str:
    combo = (src_group, tgt_group)
    if combo == ("neutral", "neutral"):
        return f"✅ Công thức An Toàn: {src_key} + {tgt_key} — luôn hài hòa, phù hợp mọi dịp."
    if combo in [("neutral", "light"), ("light", "neutral")]:
        return f"✅ Công thức Điểm Nhấn: màu sáng ({src_key or tgt_key}) + trung tính ({tgt_key or src_key}) — nổi bật, tươi trẻ."
    if combo in [("neutral", "dark"), ("dark", "neutral")]:
        return f"✅ Công thức Điểm Nhấn: màu nổi ({src_key or tgt_key}) + trung tính ({tgt_key or src_key}) — cá tính, ấn tượng."
    if combo in [("light", "dark"), ("dark", "light")]:
        return f"⚠️ Tương phản mạnh: {src_key} + {tgt_key} — táo bạo, cần phụ kiện trung tính để cân bằng."
    if combo == ("light", "light"):
        return f"⚠️ Cùng tông sáng: {src_key} + {tgt_key} — pastel dịu dàng nhưng dễ bị nhạt, cần phụ kiện tương phản."
    if combo == ("dark", "dark"):
        return (
            f"⚠️ Cùng tông đậm: {src_key} + {tgt_key} — mạnh mẽ, cần chú ý không bị nặng nề. Thêm phụ kiện trung tính."
        )
    return f"Phối {src_key} + {tgt_key}"


def _build_color_strategy(src_key: str | None, src_group: str | None, rules: dict) -> dict:
    group_matrix = rules.get("color_group_matrix", {})
    color_groups_map = rules.get("color_groups", {})

    if not src_group:
        return {"summary": "Không xác định được màu sắc sản phẩm.", "strategies": []}

    if src_group == "neutral":
        summary = f"{src_key} là màu Trung Tính — dễ phối nhất, áp dụng được cả 3 công thức."
        strategies = [
            {
                "name": '🟢 Công thức "An Toàn"',
                "desc": "Phối với màu Neutral khác (Trắng, Đen, Xám, Be, Nâu) — luôn hài hòa.",
                "score": group_matrix.get("neutral", {}).get("neutral", 30),
                "example_colors": _get_colors_by_group("neutral", color_groups_map, exclude=src_key),
            },
            {
                "name": '✨ Công thức "Điểm Nhấn" (sáng)',
                "desc": "Phối với màu Light (Hồng, Vàng, Xanh lam, Tím) — tươi trẻ, nổi bật.",
                "score": group_matrix.get("neutral", {}).get("light", 22),
                "example_colors": _get_colors_by_group("light", color_groups_map),
            },
            {
                "name": '🔥 Công thức "Cá Tính" (đậm)',
                "desc": "Phối với màu Dark (Đỏ, Cam, Xanh Jeans, Xanh navy) — mạnh mẽ, ấn tượng.",
                "score": group_matrix.get("neutral", {}).get("dark", 25),
                "example_colors": _get_colors_by_group("dark", color_groups_map),
            },
        ]
    elif src_group == "light":
        summary = f"{src_key} là màu Sáng/Pastel — phối BOTTOM Neutral để tiết chế, tránh phụ kiện quá sặc sỡ."
        strategies = [
            {
                "name": "✅ Phối với Neutral",
                "desc": f"Màu {src_key} (sáng/pastel) → phối BOTTOM Neutral (Trắng, Đen, Xám, Be) để cân bằng.",
                "score": group_matrix.get("light", {}).get("neutral", 22),
                "example_colors": _get_colors_by_group("neutral", color_groups_map),
            },
            {
                "name": "⚠️ Phụ kiện nên Neutral",
                "desc": "Chọn phụ kiện đen/trắng để không rối mắt. Tuân thủ quy tắc tối đa 3 màu.",
                "score": None,
                "example_colors": ["Đen", "Trắng", "Xám"],
            },
        ]
    else:
        summary = f"{src_key} là màu Đậm/Nổi — ưu tiên BOTTOM Neutral để cân bằng. Phụ kiện cũng nên Neutral."
        strategies = [
            {
                "name": "✅ Phối với Neutral",
                "desc": f"Màu {src_key} (đậm/nổi) → phối BOTTOM Neutral (Trắng, Đen, Xám, Be) để hài hòa.",
                "score": group_matrix.get("dark", {}).get("neutral", 20),
                "example_colors": _get_colors_by_group("neutral", color_groups_map),
            },
            {
                "name": "🎯 Mono-color (cùng tông)",
                "desc": f"Mặc head-to-toe {src_key} — mạnh mẽ, cần phụ kiện kim loại Gold/Silver làm điểm nhấn.",
                "score": group_matrix.get("dark", {}).get("dark", 10),
                "example_colors": _get_colors_by_group("dark", color_groups_map, exclude=src_key),
            },
        ]

    return {"summary": summary, "strategies": strategies}


def _get_colors_by_group(group: str, color_groups_map: dict, exclude: str | None = None) -> list:
    return [k for k, g in color_groups_map.items() if g == group and k != exclude]


# ─── Endpoints ───────────────────────────────────────────────


@router.get("/{code}")
def get_fashion_matches(code: str):
    from worker.stylist_engine import StylistEngine

    engine = StylistEngine()
    data = engine.compute_dynamic_rule_matches(code)
    classifications = engine.compute_super_classifications_sql(code)

    # Return empty ai_matches if not found (or gracefully handle) to avoid UI breaking
    if data is None:
        data = {}
    return {"ok": True, "code": code, "ai_matches": data, "classifications": classifications}


# POST /{code}/update — DEPRECATED (cột ai_matches đã bỏ)
# Data phối đồ giờ lưu ở bảng ai_outfit_product_matches
# Dùng outfit_matcher_worker.py để sinh lại matches


@router.post("/{code}/regen")
async def regen_fashion_matches(code: str):
    import asyncio

    await asyncio.to_thread(_run_engine_background, code)
    logger.info("[FashionMatches] Regen finished: %s", code)
    return {"ok": True, "message": f"Đã tính toán phối đồ cho {code}"}


@router.post("/batch-regen")
async def batch_regen_fashion_matches(request: Request):
    data = await request.json()
    codes = data.get("codes", [])
    if not codes:
        return {"ok": True, "message": "None"}

    def _run_multiple():
        from worker.stylist_engine import StylistEngine

        engine = StylistEngine()
        for c in codes:
            try:
                engine.run_for_code(c)
            except:
                pass

    import asyncio

    await asyncio.to_thread(_run_multiple)
    return {"ok": True, "message": f"Đã xong {len(codes)} sp"}


@router.post("/batch")
async def batch_fashion_matches(background_tasks: BackgroundTasks):
    from worker.stylist_engine import BATCH_STATE

    if BATCH_STATE.get("is_running"):
        return JSONResponse({"ok": False, "error": "Batch đang chạy, vui lòng chờ"}, status_code=409)
    background_tasks.add_task(_run_engine_background, None)
    return {"ok": True, "message": "Đã khởi động batch Stylist Engine..."}


@router.get("/batch/status")
async def batch_status():
    from worker.stylist_engine import BATCH_STATE

    state = dict(BATCH_STATE)
    pct = 0
    if state.get("total", 0) > 0:
        pct = round(state["done"] / state["total"] * 100, 1)
    state["progress_pct"] = pct
    return {"ok": True, "state": state}


@router.get("/rules/config")
async def get_rules():
    try:
        rules = _load_rules()
        return {"ok": True, "rules": rules}
    except Exception as e:
        return JSONResponse({"ok": False, "error": str(e)}, status_code=500)


@router.put("/rules/config")
async def update_rules(req: UpdateRulesRequest):
    try:
        with open(RULES_PATH, "w", encoding="utf-8") as f:
            json.dump(req.rules, f, ensure_ascii=False, indent=2)
        return {"ok": True, "message": "Rules đã được cập nhật"}
    except Exception as e:
        return JSONResponse({"ok": False, "error": str(e)}, status_code=500)


@router.get("/rules/meta")
async def get_rules_meta():
    try:
        rules = _load_rules()
        color_hex_map = {
            "Trắng": "#F5F5F5",
            "Đen": "#1A1A1A",
            "Be": "#D4B896",
            "Xám": "#9E9E9E",
            "Nâu": "#795548",
            "Vàng": "#FDD835",
            "Hồng": "#F48FB1",
            "Xanh lam": "#64B5F6",
            "Tím": "#BA68C8",
            "Đỏ": "#EF5350",
            "Cam": "#FFA726",
            "Xanh lá": "#66BB6A",
            "Xanh navy": "#1A237E",
            "Xanh Jeans": "#5C6BC0",
            "Xanh than": "#00897B",
        }
        color_meta = []
        for key, variants in rules.get("color_keys", {}).items():
            group = rules.get("color_groups", {}).get(key, "unknown")
            color_meta.append(
                {
                    "key": key,
                    "group": group,
                    "hex": color_hex_map.get(key, "#CCCCCC"),
                    "keywords": variants,
                }
            )
        return {
            "ok": True,
            "meta": {
                "colors": color_meta,
                "color_group_matrix": rules.get("color_group_matrix", {}),
                "styles": list(rules.get("style_compat", {}).keys()),
                "occasions": [{"key": k, "label": v} for k, v in rules.get("occasion_labels", {}).items()],
                "roles": [{"key": k, "label": v} for k, v in rules.get("role_labels", {}).items()],
                "score_weights": rules.get("score_weights", {}),
                "version": rules.get("version", "?"),
            },
        }
    except Exception as e:
        return JSONResponse({"ok": False, "error": str(e)}, status_code=500)


@router.post("/color-logic")
async def color_logic(req: ColorLogicRequest):
    try:
        rules = _load_rules()
        color_keys = rules.get("color_keys", {})
        src_key = _detect_color_key(req.src_color, color_keys)
        tgt_key = _detect_color_key(req.tgt_color, color_keys)
        result = _color_group_explain(src_key, tgt_key, rules)
        return {"ok": True, "result": result}
    except Exception as e:
        return JSONResponse({"ok": False, "error": str(e)}, status_code=500)


@router.post("/outfit-suggest")
def outfit_suggest(req: OutfitSuggestRequest):
    try:
        rules = _load_rules()
        color_keys = rules.get("color_keys", {})

        from worker.stylist_engine import StylistEngine

        engine = StylistEngine()
        catalog = engine._load_catalog()

        # Find source product in catalog
        src_product = next(
            (p for p in catalog if p.get("code") == req.code or p.get("internal_ref_code") == req.code), None
        )

        if not src_product:
            return JSONResponse(
                {"ok": False, "error": f"Không tìm thấy sản phẩm: {req.code}"},
                status_code=404,
            )

        logger.info("[OutfitSuggest] Computing real-time dynamic matches for %s", req.code)
        ai_matches = engine.compute_dynamic_rule_matches(req.code)

        src_color = src_product.get("color", "")
        src_key = _detect_color_key(src_color, color_keys)
        src_group = rules.get("color_groups", {}).get(src_key) if src_key else None

        desc_data = src_product.get("description_data", {})
        src_name = src_product.get("name", "")
        src_line = src_product.get("product_line", "")
        src_gender = src_product.get("gender", "")
        src_img = src_product.get("image", "")

        src_color = src_color or ""
        src_key = _detect_color_key(src_color, color_keys)
        src_group = rules.get("color_groups", {}).get(src_key) if src_key else None

        occasions = rules.get("occasions", [])
        active_occasions = [req.occasion] if (req.occasion and req.occasion in occasions) else occasions

        outfit_by_occasion = {}
        for occ in active_occasions:
            occ_data = ai_matches.get(occ, {})
            if not occ_data:
                continue
            slots = {}
            for role, items in occ_data.items():
                enriched = []
                for item in items:
                    item_color_key = _detect_color_key(item.get("color", ""), color_keys)
                    color_info = _color_group_explain(src_key, item_color_key, rules)
                    enriched.append(
                        {
                            **item,
                            "color_key": item_color_key,
                            "color_group": rules.get("color_groups", {}).get(item_color_key, "?")
                            if item_color_key
                            else "?",
                            "color_synergy": color_info,
                        }
                    )
                slots[role] = enriched
            if slots:
                outfit_by_occasion[occ] = {
                    "label": rules.get("occasion_labels", {}).get(occ, occ),
                    "slots": slots,
                }

        color_strategy = _build_color_strategy(src_key, src_group, rules)

        # Inject our brand new SQL-based super classification matches
        classifications = engine.compute_super_classifications_sql(req.code)

        return {
            "ok": True,
            "source_product": {
                "code": req.code,
                "name": src_name or "",
                "color": src_color,
                "color_key": src_key,
                "color_group": src_group,
                "product_line": src_line or "",
                "gender": src_gender or "",
                "image": src_img or "",
                "style": desc_data.get("phong_cach", ""),
                "occasion": desc_data.get("dip_mac", ""),
                "material": desc_data.get("vat_lieu", ""),
            },
            "color_strategy": color_strategy,
            "outfit_by_occasion": outfit_by_occasion,
            "classifications": classifications,
        }

    except Exception as e:
        logger.error("[OutfitSuggest] error: %s", e, exc_info=True)
        return JSONResponse({"ok": False, "error": str(e)}, status_code=500)


class ScoreTestRequest(BaseModel):
    source_code: str
    target_code: str


@router.post("/score-test")
def score_test(req: ScoreTestRequest):
    try:
        from worker.stylist_engine import StylistEngine

        engine = StylistEngine()
        catalog = engine._load_catalog()

        source = next((p for p in catalog if p.get("code") == req.source_code), None)
        target = next((p for p in catalog if p.get("code") == req.target_code), None)

        if not source:
            return JSONResponse({"ok": False, "error": f"Không tìm thấy SP nguồn: {req.source_code}"}, status_code=404)
        if not target:
            return JSONResponse({"ok": False, "error": f"Không tìm thấy SP đích: {req.target_code}"}, status_code=404)

        breakdown = engine._score_breakdown(source, target)
        total = sum(v["score"] for v in breakdown.values())
        occ0 = (engine.rules.get("occasions") or ["hang_ngay"])[0]
        reason = engine._build_reason(source, target, occ0, total)

        return {
            "ok": True,
            "result": {
                "source_code": req.source_code,
                "target_code": req.target_code,
                "target_name": target.get("name", ""),
                "roles_str": target.get("product_line", ""),
                "total_score": total,
                "reason": reason,
                "breakdown": breakdown,
            },
        }
    except Exception as e:
        logger.error("[ScoreTest] error: %s", e)
        return JSONResponse({"ok": False, "error": str(e)}, status_code=500)


@router.get("/audit/tag-coverage")
def audit_tag_coverage(limit: int = Query(300, ge=20, le=2000), q: str = ""):
    """Audit product tags vs rules coverage for data QA."""
    try:
        from worker.stylist_engine import StylistEngine

        engine = StylistEngine()
        catalog = engine._get_catalog()
        q_lower = (q or "").strip().lower()

        if q_lower:
            catalog = [
                p
                for p in catalog
                if q_lower in (p.get("code") or "").lower()
                or q_lower in (p.get("name") or "").lower()
                or q_lower in (p.get("product_line") or "").lower()
            ]

        sample_catalog = catalog[:limit]

        # Cache rule lookup per anchor category + normalized gender to avoid repeated DB calls.
        rule_cache: dict[tuple[str, str], dict] = {}

        def resolve_rule_status(anchor_cat: str, gender_raw: str) -> dict:
            gender_key = engine._normalize_gender(gender_raw)
            cache_key = (anchor_cat or "", gender_key)
            if cache_key in rule_cache:
                return rule_cache[cache_key]

            db_rules = engine._fetch_rules_with_reason(anchor_cat, gender_raw)
            fallback = engine._get_fallback_mappings(anchor_cat)
            status = {
                "db_rules_count": len(db_rules),
                "fallback_occasion_count": len(fallback),
                "status": "none",
            }
            if status["db_rules_count"] > 0:
                status["status"] = "db"
            elif status["fallback_occasion_count"] > 0:
                status["status"] = "fallback"

            rule_cache[cache_key] = status
            return status

        product_line_stats: dict[str, dict] = {}
        missing_by_sku: list[dict] = []

        for item in sample_catalog:
            code = item.get("code") or ""
            name = item.get("name") or ""
            product_line = item.get("product_line") or ""
            gender = item.get("gender") or ""
            role = engine._get_role(item)
            rule_status = resolve_rule_status(product_line, gender)

            stat = product_line_stats.setdefault(
                product_line,
                {
                    "product_line": product_line,
                    "total_skus": 0,
                    "db_covered": 0,
                    "fallback_covered": 0,
                    "rule_missing": 0,
                    "role_missing": 0,
                },
            )
            stat["total_skus"] += 1
            if rule_status["status"] == "db":
                stat["db_covered"] += 1
            elif rule_status["status"] == "fallback":
                stat["fallback_covered"] += 1
            else:
                stat["rule_missing"] += 1
            if not role:
                stat["role_missing"] += 1

            missing_tags = []
            if not product_line:
                missing_tags.append("product_line")
            if not role:
                missing_tags.append("role_mapping")
            if not (item.get("color") or "").strip():
                missing_tags.append("color")
            if not (item.get("gender") or "").strip():
                missing_tags.append("gender")
            if not item.get("material_tags"):
                missing_tags.append("material_tags")
            if not item.get("occasion_tags"):
                missing_tags.append("occasion_tags")
            if not item.get("season_tags"):
                missing_tags.append("season_tags")
            if rule_status["status"] == "none":
                missing_tags.append("rule_coverage")

            if missing_tags:
                missing_by_sku.append(
                    {
                        "code": code,
                        "name": name,
                        "product_line": product_line,
                        "gender": gender,
                        "rule_status": rule_status["status"],
                        "missing_tags": missing_tags,
                    }
                )

        product_lines = sorted(
            product_line_stats.values(), key=lambda x: (-x["rule_missing"], -x["role_missing"], x["product_line"])
        )
        product_line_unmatched = [p for p in product_lines if p["rule_missing"] > 0 or p["role_missing"] > 0]

        checklist = [
            {
                "key": "product_line_to_role",
                "label": "Bổ sung mapping product_line_to_role cho product_line thiếu role",
                "affected_count": sum(1 for p in product_lines if p["role_missing"] > 0),
            },
            {
                "key": "chatbot_fashion_rules",
                "label": "Bổ sung rule DB cho anchor_category chưa phủ",
                "affected_count": sum(1 for p in product_lines if p["rule_missing"] > 0),
            },
            {
                "key": "description_data.material",
                "label": "Bổ sung vat_lieu để tạo material_tags",
                "affected_count": sum(1 for x in missing_by_sku if "material_tags" in x["missing_tags"]),
            },
            {
                "key": "description_data.occasion",
                "label": "Bổ sung dip_mac để tạo occasion_tags",
                "affected_count": sum(1 for x in missing_by_sku if "occasion_tags" in x["missing_tags"]),
            },
            {
                "key": "description_data.season",
                "label": "Bổ sung mua để tạo season_tags",
                "affected_count": sum(1 for x in missing_by_sku if "season_tags" in x["missing_tags"]),
            },
        ]

        summary = {
            "sample_size": len(sample_catalog),
            "total_after_filter": len(catalog),
            "product_line_count": len(product_lines),
            "product_line_unmatched_count": len(product_line_unmatched),
            "sku_with_missing_tags": len(missing_by_sku),
            "db_rule_covered_skus": sum(
                1
                for x in sample_catalog
                if resolve_rule_status(x.get("product_line") or "", x.get("gender") or "")["status"] == "db"
            ),
            "fallback_rule_skus": sum(
                1
                for x in sample_catalog
                if resolve_rule_status(x.get("product_line") or "", x.get("gender") or "")["status"] == "fallback"
            ),
            "no_rule_skus": sum(
                1
                for x in sample_catalog
                if resolve_rule_status(x.get("product_line") or "", x.get("gender") or "")["status"] == "none"
            ),
        }

        return {
            "ok": True,
            "summary": summary,
            "product_line_unmatched": product_line_unmatched,
            "sku_missing_tags": missing_by_sku,
            "checklist": checklist,
        }
    except Exception as e:
        logger.error("[TagAudit] error: %s", e)
        return JSONResponse({"ok": False, "error": str(e)}, status_code=500)


# --- Rules Framework HTML View ---
@router.get("/rules/view")
def rules_view():
    from collections import defaultdict

    from fastapi.responses import HTMLResponse

    rows = []
    conn = None
    try:
        conn = get_pooled_connection_compat()
        cur = conn.cursor()
        cur.execute("""
            SELECT gender_target, anchor_category, occasion_tag, match_role, target_category, ai_reason
            FROM dashboard_canifa.chatbot_fashion_rules
            WHERE gender_target != 'all'
            ORDER BY CASE gender_target
                WHEN 'nu' THEN 1 WHEN 'nam' THEN 2 WHEN 'unisex' THEN 3
                WHEN 'be_gai' THEN 4 WHEN 'be_trai' THEN 5 ELSE 6
            END, occasion_tag, match_role, anchor_category
        """)
        rows = cur.fetchall()
        cur.close()
    except Exception as e:
        logger.error("[RulesView] DB error: %s", e)
    finally:
        if conn:
            conn.close()
    grouped = defaultdict(lambda: defaultdict(list))
    for gender, anchor, occ, role, target, reason in rows:
        grouped[gender][occ].append({"anchor": anchor, "role": role, "target": target, "reason": reason or ""})
    GENDER_META = {
        "nu": {
            "label": "NGA Nu",
            "color": "#ec4899",
            "bg": "#fdf2f8",
            "group": "Neutral+Light+Dark",
            "colors": "Trang, Hong, Be, Tim, Xanh lam, Den",
        },
        "nam": {
            "label": "NAM Nam",
            "color": "#3b82f6",
            "bg": "#eff6ff",
            "group": "Neutral (+5 boost)",
            "colors": "Be, Xam, Den, Trang, Nau, Xanh than",
        },
        "unisex": {
            "label": "UNI Unisex",
            "color": "#8b5cf6",
            "bg": "#f5f3ff",
            "group": "Neutral+Dark",
            "colors": "Den, Trang, Xam, Do, Xanh navy, Xanh Jeans",
        },
        "be_gai": {
            "label": "BG Be Gai",
            "color": "#f43f5e",
            "bg": "#fff1f2",
            "group": "Light Pastel (+5 boost)",
            "colors": "Hong, Tim, Vang nhat, Xanh lam, Trang",
        },
        "be_trai": {
            "label": "BT Be Trai",
            "color": "#f59e0b",
            "bg": "#fffbeb",
            "group": "Dark (+5 boost)",
            "colors": "Vang, Cam, Xanh Jeans, Xanh navy, Do",
        },
    }
    OCC_LABELS = {
        "hang_ngay": "Hang ngay",
        "di_lam": "Di lam",
        "di_choi": "Di choi",
        "du_lich": "Du lich",
        "the_thao": "The thao",
        "mac_nha": "Mac nha",
    }
    ROLE_ICONS = {"top": "TOP", "bottom": "BTM", "outerwear": "OTR", "accessory": "ACC"}
    sections = ""
    for gk, meta in GENDER_META.items():
        occ_data = grouped.get(gk, {})
        if not occ_data:
            continue
        rows_html = ""
        for occ, rlist in occ_data.items():
            items = "".join(
                f'<span class="ri" title="{r["reason"]}">{r["anchor"]} &rarr; {r["target"]} <em>({ROLE_ICONS.get(r["role"], r["role"])})</em></span>'
                for r in rlist
            )
            rows_html += f'<tr><td class="oc"><b>{OCC_LABELS.get(occ, occ)}</b></td><td class="rc">{items}</td></tr>'
        sections += f'<div class="gs" style="--c:{meta["color"]};--bg:{meta["bg"]}"><div class="gh"><span class="gtitle">{meta["label"]}</span><span class="gchip">{meta["group"]}</span><span class="gcol">{meta["colors"]}</span></div><table class="rt"><thead><tr><th>Dip mac</th><th>TOP Anchor &rarr; TARGET (role) theo product_line DB</th></tr></thead><tbody>{rows_html}</tbody></table></div>'
    html = f"""<!DOCTYPE html><html lang="vi"><head><meta charset="UTF-8"><title>Framework Phoi Do</title><style>
*{{box-sizing:border-box;margin:0;padding:0}}body{{font-family:-apple-system,sans-serif;background:#f8fafc;color:#1e293b;padding:20px}}
.ph{{max-width:1000px;margin:0 auto 20px;padding-bottom:16px;border-bottom:2px solid #e2e8f0}}
.ptitle{{font-size:20px;font-weight:800}}.psub{{font-size:12px;color:#64748b;margin-top:3px}}
.chips{{display:flex;gap:8px;margin-top:8px}}.chip{{background:#f1f5f9;border:1px solid #e2e8f0;border-radius:20px;padding:3px 11px;font-size:12px;font-weight:600;color:#475569}}
.gs{{max-width:1000px;margin:0 auto 16px;border-radius:12px;overflow:hidden;border:1.5px solid var(--c);box-shadow:0 2px 8px rgba(0,0,0,.06)}}
.gh{{background:var(--bg);border-bottom:1.5px solid var(--c);padding:10px 16px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}}
.gtitle{{font-size:15px;font-weight:800;color:var(--c)}}.gchip{{background:var(--c);color:#fff;border-radius:20px;padding:2px 8px;font-size:11px;font-weight:700}}.gcol{{font-size:11px;color:#64748b}}
.rt{{width:100%;border-collapse:collapse;background:#fff}}
.rt th{{text-align:left;padding:8px 14px;font-size:10px;text-transform:uppercase;color:#94a3b8;background:#f8fafc;border-bottom:1px solid #e2e8f0}}
.rt tr:not(:last-child) td{{border-bottom:1px solid #f8fafc}}.rt td{{padding:7px 14px;vertical-align:top}}
.oc{{width:110px;font-size:12px;font-weight:700;color:#334155}}.rc{{display:flex;flex-wrap:wrap;gap:5px}}
.ri{{display:inline-flex;align-items:center;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:7px;padding:3px 9px;font-size:12px;font-weight:600;cursor:help;transition:.15s}}
.ri:hover{{background:var(--bg);border-color:var(--c)}}.ri em{{font-size:10px;color:#94a3b8;font-style:normal;margin-left:4px}}
</style></head><body>
<div class="ph"><div class="ptitle">📐 Framework Phối Đồ — Canifa AI Stylist v1.0</div>
<div class="psub">Lưới logic Giới tính × Dịp → nguồn: <code>chatbot_fashion_rules.gender_target</code></div>
<div class="chips"><span class="chip">{len(rows)} rules</span><span class="chip">{len(GENDER_META)} demographic</span></div></div>
{sections}</body></html>"""
    return HTMLResponse(content=html)
