"""
Ultra Description Manager — API Route
Generates AI-powered product descriptions from product images + stock data.

Pipeline:
  Phase 1: Vision (Llama 4 Scout) → raw JSON 20 fields from product image
  Phase 2: Enrichment (GPT-OSS 120B) → rewrite with stylist tone + real stock data
"""

import asyncio
import json
import logging
import re
import uuid
from typing import Optional

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

from common.cache import redis_cache
from common.starrocks_connection import get_db_connection
from common.ultra_desc_db import UltraDescriptionDB, DescFieldConfig

logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/product-desc", tags=["Ultra Description"])

TABLE_NAME = "shared_source.magento_product_dimension_with_text_embedding"

# ═══ Groq Config — Multi-Key Round-Robin ═══
GROQ_API_KEYS = [
    "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",  # Key 1
    "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",  # Key 2
    "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",  # Key 3
    "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",
    "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",
    "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",
]
_groq_key_index = 0  # Round-robin counter

def _next_groq_key() -> str:
    """Get next Groq API key using round-robin rotation."""
    global _groq_key_index
    key = GROQ_API_KEYS[_groq_key_index % len(GROQ_API_KEYS)]
    _groq_key_index += 1
    return key

GROQ_API_KEY = GROQ_API_KEYS[0]  # Backwards compat for imports
VISION_MODEL = "meta-llama/llama-4-scout-17b-16e-instruct"
ENRICH_MODEL = "openai/gpt-oss-120b"

# ═══ Batch Tracking ═══
BATCH_STATE = {
    "is_running": False,
    "total": 0,
    "done": 0,
    "errors": 0,
    "current_code": None
}


# ═══ Prompts ═══
VISION_PROMPT = """Bạn là chuyên gia phân tích sản phẩm thời trang. Nhìn ảnh sản phẩm và trả về JSON với ĐÚNG 28 trường sau.
QUAN TRỌNG: KHÔNG đoán chất liệu nếu không chắc. KHÔNG bỏ trống trường nào. Nếu không chắc, ghi "Không xác định".

{
  "ten_san_pham": "Tên sản phẩm ngắn gọn",
  "tagline": "1 câu slogan hấp dẫn",
  "mo_ta_chinh": "3 câu mô tả: form dáng + thiết kế + tác dụng lên cơ thể",
  "chat_lieu": "Chất liệu vải (VD: Cotton 100%, Polyester pha). Ghi 'Không xác định' nếu không biết",
  "tinh_nang_vai": "Tính năng nổi bật của vải (VD: co giãn 4 chiều, kháng khuẩn, thấm hút)",
  "huong_dan_bao_quan": "Hướng dẫn giặt/bảo quản (VD: Giặt máy ≤30°C, không dùng tẩy)",
  "phong_cach": "1-3 từ khóa phong cách (VD: Smart Casual, Minimalist)",
  "tags": "5 tags phân loại, cách dấu phẩy",
  "do_tuoi": "Khoảng tuổi phù hợp (VD: 20-35)",
  "gioi_tinh": "Nam/Nữ/Unisex",
  "loi_song": "Mô tả lối sống khách hàng mục tiêu",
  "tinh_cach": "Tính cách phù hợp",
  "dip_mac": "4+ dịp mặc cụ thể, ngăn bởi dấu ·",
  "phoi_do": "3 combo phối đồ, ngăn bởi dấu |",
  "nguyen_tac_phoi_do": "Lý do và quy tắc phối đồ (VD: Áo ôm nên phối quần suông để cân đối; Vải lanh hợp không khí biển)",
  "tranh_phoi_cung": "Những item/dòng SP KHÔNG NÊN phối cùng (Negative constraints - VD: Tránh phối nỉ dày mùa hè, tránh quần jogger vì lệch phong cách)",
  "layer": "Gợi ý layering",
  "mua": "Mùa phù hợp",
  "faq_1_q": "Câu hỏi FAQ 1 (về form dáng)",
  "faq_1_a": "Trả lời FAQ 1",
  "faq_2_q": "Câu hỏi FAQ 2 (về size)",
  "faq_2_a": "Trả lời FAQ 2",
  "faq_3_q": "Câu hỏi FAQ 3 (về mix match)",
  "faq_3_a": "Trả lời FAQ 3",
  "hook_quang_cao": "1 câu hook quảng cáo gây tò mò",
  "cross_sell": "2-3 sản phẩm nên mua kèm",
  "ly_do_mua": "3 lý do nên mua, ngăn bởi dấu |",
  "luu_y_size": "Lưu ý chọn size cụ thể"
}

Trả về ĐÚNG JSON, không text thừa."""

CANIFA_PRODUCT_LINES = "Áo phông, Quần soóc, Áo nỉ, Váy liền, Quần nỉ, Tất, Áo len, Bộ mặc nhà, Bộ quần áo, Áo Polo, Chân váy, Áo nỉ có mũ, Áo Sơ mi, Quần jean, Quần dài, Áo khoác chần bông, Quần mặc nhà, Áo Body, Quần Khaki, Áo khoác gió, Áo khoác dáng ngắn, Áo kiểu, Áo len gilet, Áo khoác gilet chần bông, Áo giữ nhiệt, Áo mặc nhà, Quần lót, Pyjama, Áo ba lỗ, Áo khoác nỉ không mũ, Blazer, Cardigan, Khăn, Bộ thể thao, Áo khoác chống nắng, Quần váy, Quần lót đùi, Áo khoác, Khăn mặt, Áo khoác nỉ có mũ, Mũ, Áo khoác lông vũ, Khăn tắm, Áo lót, Quần lót tam giác, Túi xách, Quần leggings mặc nhà, Áo khoác sợi, Quần leggings, Chăn cá nhân, Mũ thể thao, Áo khoác dạ, Quần giữ nhiệt, Găng tay chống nắng, Khăn lau đầu, Áo khoác gilet, Áo hai dây, Khẩu trang, Quần culottes, Quần Body, Áo bra active"

ENRICHMENT_SYSTEM = """Bạn là Stylist cao cấp của Canifa — chuyên viết mô tả sản phẩm thời trang bằng tiếng Việt.
Phong cách: tự tin, chuyên gia, dùng từ ngữ gợi hình (hack dáng, tôn dáng, thoát dáng, che khuyết điểm).
Nếu có thông tin chất liệu từ Magento, PHẢI sử dụng chính xác. KHÔNG bịa chất liệu.
NẾU có description gốc, trích xuất chất liệu, tính năng vải, hướng dẫn giặt từ đó.
KHÔNG nói về giá cả. Luôn trả về JSON chuẩn 28 trường."""

ENRICHMENT_EXAMPLE = """{
  "ten_san_pham": "Áo Polo Nam Cổ Bẻ Tay Ngắn",
  "tagline": "Lịch lãm không cần cố gắng",
  "mo_ta_chinh": "Form regular fit vừa vặn, tôn dáng mà không gò bó. Thiết kế cổ bẻ tối giản nhưng lịch sự, phù hợp cả office lẫn weekend. Phom vai suông giúp che nhẹ bắp tay, hack dáng thanh thoát hơn.",
  "chat_lieu": "Cotton USA 100%",
  "tinh_nang_vai": "Co giãn nhẹ, thấm hút mồ hôi tốt, giữ form sau nhiều lần giặt",
  "huong_dan_bao_quan": "Giặt máy ≤30°C, lộn trái, không dùng tẩy, phơi trong bóng râm",
  "phong_cach": "Smart Casual, Minimalist",
  "tags": "polo, nam, công sở, smart casual, tay ngắn, cotton usa",
  "do_tuoi": "25-40",
  "gioi_tinh": "Nam",
  "loi_song": "Người đàn ông hiện đại, coi trọng phong cách nhưng không cầu kỳ",
  "tinh_cach": "Tự tin, chỉn chu, thực dụng",
  "dip_mac": "Đi làm hàng ngày · Cafe cuối tuần · Gặp đối tác · Đi chơi cùng gia đình",
  "phoi_do": "Quần Chinos Beige + Giày Loafer Da → Thanh lịch cuối tuần | Quần Jean Slim Xanh Đậm + Sneaker Trắng → Năng động công sở | Quần Short Khaki + Slide Da → Relax ngày hè",
  "nguyen_tac_phoi_do": "Polo form regular có phần cổ bẻ lịch sự, nguyên tắc phối là mix CÙNG ĐỘ FIT (quần dáng slim hoặc straight) ở thân dưới. Quần quá rộng sẽ làm mất đi độ thanh lịch của áo.",
  "tranh_phoi_cung": "Tránh phối cùng quần jogger nỉ quá thể thao hoặc quần đùi chạy bộ vì sẽ lệch pha phong cách hoàn toàn.",
  "layer": "Mặc đơn mùa hè. Layer cùng Áo Khoác Bomber hoặc Blazer mỏng cho mùa thu.",
  "mua": "Xuân Hè",
  "faq_1_q": "Áo này có hack dáng không?",
  "faq_1_a": "Phom regular fit vai suông giúp che nhẹ bắp tay, tạo cảm giác thanh thoát hơn — đặc biệt hợp với anh em có phần trên hơi đầy.",
  "faq_2_q": "Cao 1m70 nặng 65kg nên mặc size gì?",
  "faq_2_a": "Nên chọn size M. Nếu thích mặc hơi rộng thoải mái thì lên L — nhưng M sẽ chuẩn form nhất.",
  "faq_3_q": "Phối áo này với quần gì đẹp nhất?",
  "faq_3_a": "Combo kinh điển: Chinos hoặc Jean slim + Loafer. Muốn casual hơn: Short khaki + Sneaker trắng.",
  "hook_quang_cao": "Chiếc polo mà anh em mặc đi làm rồi đi cafe luôn — không cần thay đồ",
  "cross_sell": "Quần Chinos Slim, Giày Loafer Da, Thắt Lưng Da Reversible",
  "ly_do_mua": "Form hack dáng thanh thoát | Mặc đi làm hay đi chơi đều ổn | Cổ bẻ lịch sự không cần phối nhiều",
  "luu_y_size": "Nếu nặng trên 75kg nên lên 1 size. Form regular nên mặc đúng size sẽ vừa nhất."
}"""


# ═══ Helpers ═══

# Label mapping for rendering JSON → readable text
_FIELD_LABELS = {
    "ten_san_pham": "Tên sản phẩm",
    "tagline": "Tagline",
    "mo_ta_chinh": "Mô tả chính",
    "chat_lieu": "Chất liệu",
    "tinh_nang_vai": "Tính năng vải",
    "huong_dan_bao_quan": "Hướng dẫn bảo quản",
    "phong_cach": "Phong cách",
    "tags": "Tags",
    "do_tuoi": "Độ tuổi",
    "gioi_tinh": "Giới tính",
    "loi_song": "Lối sống",
    "tinh_cach": "Tính cách",
    "dip_mac": "Dịp mặc",
    "phoi_do": "Gợi ý phối đồ",
    "nguyen_tac_phoi_do": "Nguyên tắc phối đồ",
    "tranh_phoi_cung": "Tránh phối cùng",
    "layer": "Gợi ý layering",
    "mua": "Mùa phù hợp",
    "faq_1_q": "FAQ 1 - Câu hỏi",
    "faq_1_a": "FAQ 1 - Trả lời",
    "faq_2_q": "FAQ 2 - Câu hỏi",
    "faq_2_a": "FAQ 2 - Trả lời",
    "faq_3_q": "FAQ 3 - Câu hỏi",
    "faq_3_a": "FAQ 3 - Trả lời",
    "hook_quang_cao": "Hook quảng cáo",
    "cross_sell": "Cross-sell",
    "ly_do_mua": "Lý do nên mua",
    "luu_y_size": "Lưu ý chọn size",
}

# Section grouping for rendered output
_SECTIONS = [
    ("📦 THÔNG TIN CƠ BẢN", ["ten_san_pham", "tagline", "mo_ta_chinh", "phong_cach", "tags"]),
    ("🧵 CHẤT LIỆU & BẢO QUẢN", ["chat_lieu", "tinh_nang_vai", "huong_dan_bao_quan"]),
    ("🎯 ĐỐI TƯỢNG", ["do_tuoi", "gioi_tinh", "loi_song", "tinh_cach"]),
    ("📅 DỊP MẶC & STYLING", ["dip_mac", "phoi_do", "nguyen_tac_phoi_do", "tranh_phoi_cung", "layer", "mua"]),
    ("❓ FAQ", ["faq_1_q", "faq_1_a", "faq_2_q", "faq_2_a", "faq_3_q", "faq_3_a"]),
    ("🛒 HỖ TRỢ BÁN HÀNG", ["hook_quang_cao", "cross_sell", "ly_do_mua", "luu_y_size"]),
]


def _render_description_text(data: dict, product_name: str = "") -> str:
    """Render JSON description_data into a single formatted text block."""
    if not data:
        return ""

    lines = []
    title = data.get("ten_san_pham", product_name or "Sản phẩm")
    lines.append(f"{'=' * 50}")
    lines.append(f"  {title.upper()}")
    lines.append(f"{'=' * 50}")
    lines.append("")

    for section_title, fields in _SECTIONS:
        section_lines = []
        for key in fields:
            val = data.get(key, "")
            if not val:
                continue
            label = _FIELD_LABELS.get(key, key)

            # FAQ: group Q+A together
            if key.endswith("_q"):
                section_lines.append(f"  💬 {val}")
                continue
            elif key.endswith("_a"):
                section_lines.append(f"     → {val}")
                continue

            # Phối đồ: format combos nicely
            if key == "phoi_do" and "|" in str(val):
                section_lines.append(f"  {label}:")
                for combo in str(val).split("|"):
                    section_lines.append(f"    • {combo.strip()}")
                continue

            # Lý do mua: format with bullets
            if key == "ly_do_mua" and "|" in str(val):
                section_lines.append(f"  {label}:")
                for reason in str(val).split("|"):
                    section_lines.append(f"    • {reason.strip()}")
                continue

            # Dịp mặc: keep · separator
            section_lines.append(f"  {label}: {val}")

        if section_lines:
            lines.append(f"── {section_title} ──")
            lines.extend(section_lines)
            lines.append("")

    return "\n".join(lines)

def _extract_json(raw_text: str | list | dict) -> dict | None:
    """Parse JSON safely from LLM output."""
    if not raw_text:
        return None

    # Handle list response (e.g., from Langchain Gemini multimodal fallback)
    if isinstance(raw_text, list):
        text_parts = []
        for part in raw_text:
            if isinstance(part, dict) and "text" in part:
                text_parts.append(part["text"])
            elif isinstance(part, str):
                text_parts.append(part)
        raw_text = "".join(text_parts)

    # If it's a dict somehow, just return it directly or stringify
    if isinstance(raw_text, dict):
        return raw_text

    if not isinstance(raw_text, str):
        raw_text = str(raw_text)

    # Try ```json ... ``` block
    if "```" in raw_text:
        match = re.search(r'```(?:json)?\s*([\s\S]*?)```', raw_text)
        if match:
            try:
                return json.loads(match.group(1).strip())
            except json.JSONDecodeError:
                pass
    # Try { ... } directly
    match = re.search(r'\{[\s\S]*\}', raw_text)
    if match:
        try:
            return json.loads(match.group(0))
        except json.JSONDecodeError:
            pass
    try:
        return json.loads(raw_text.strip())
    except json.JSONDecodeError:
        return None


def _is_groq_model(model_name: str) -> bool:
    """Groq models: openai/gpt-oss-*, meta-llama/*, mixtral/*, etc."""
    m = model_name.lower()
    return any(kw in m for kw in ("gpt-oss", "llama", "mixtral", "gemma", "qwen", "deepseek")) or m.startswith("openai/")


async def _call_ai_with_fallback(messages: list, model: str, max_tokens: int = 3000, temperature: float = 0.7, is_vision: bool = False) -> str:
    """
    Smart Groq-only pipeline with multi-key round-robin rotation.
    3 API keys = 3x throughput. Each model tries all keys before giving up.
    """

    # ── Groq httpx (fast, free, multi-key) ──
    if _is_groq_model(model):
        if is_vision:
            groq_chain = [model]
        else:
            groq_chain = [model]
            for backup in ["openai/gpt-oss-120b", "openai/gpt-oss-20b", "qwen/qwen3-32b"]:
                if backup != model and backup not in groq_chain:
                    groq_chain.append(backup)

        while True:
            for groq_model in groq_chain:
                # Try each key (round-robin) — if one key is rate-limited, try next key instantly
                for key_attempt in range(len(GROQ_API_KEYS)):
                    api_key = _next_groq_key()
                    key_tag = f"K{(_groq_key_index - 1) % len(GROQ_API_KEYS) + 1}"
                    try:
                        async with httpx.AsyncClient(timeout=30) as client:
                            resp = await client.post(
                                "https://api.groq.com/openai/v1/chat/completions",
                                headers={
                                    "Authorization": f"Bearer {api_key}",
                                    "Content-Type": "application/json",
                                },
                                json={
                                    "model": groq_model,
                                    "max_tokens": max_tokens,
                                    "temperature": temperature,
                                    "messages": messages,
                                },
                            )

                            if resp.status_code == 200:
                                result = resp.json()
                                if "error" not in result:
                                    logger.info(f"🏆 Groq [{key_tag}] {groq_model}")
                                    return result["choices"][0]["message"]["content"]
                                logger.warning(f"⚠️ Groq [{key_tag}] {groq_model} API Error: {result['error']}")
                                break  # Go to next model
                            elif resp.status_code == 429:
                                logger.warning(f"⚠️ Groq [{key_tag}] {groq_model} Rate limit → rotating key...")
                                continue  # Try next key instantly!
                            elif resp.status_code >= 500:
                                logger.warning(f"⚠️ Groq [{key_tag}] {groq_model} Server Error {resp.status_code}: {resp.text[:100]}...")
                                continue
                            else:
                                logger.warning(f"⚠️ Groq [{key_tag}] {groq_model} HTTP {resp.status_code}: {resp.text[:200]}")
                                break # Other errors like 400 Bad request -> go to next model

                    except Exception as e:
                        logger.warning(f"⚠️ Groq [{key_tag}] {groq_model} Exception: {e}")
                        break
                else:
                    # All keys exhausted for this model — wait a bit then try next model
                    logger.warning(f"⚠️ All {len(GROQ_API_KEYS)} keys exhausted for {groq_model} → waiting 4s...")
                    await asyncio.sleep(4)
                    continue

            logger.warning("🔄 Toàn bộ model và key đã bị giới hạn, tiếp tục vòng mới sau 5s...")
            await asyncio.sleep(5)


async def _fetch_image_base64(image_url: str) -> str:
    """Download image and convert to base64."""
    import base64
    async with httpx.AsyncClient(timeout=15) as client:
        resp = await client.get(image_url, headers={"User-Agent": "Mozilla/5.0"})
        resp.raise_for_status()
        return base64.b64encode(resp.content).decode("utf-8")


# ═══ 1. OVERVIEW — Stats on descriptions ═══
@router.get("/overview", summary="Overview stats: total products, how many have descriptions")
async def overview():
    """Return count of products with/without ultra descriptions (from Postgres)."""
    db = get_db_connection()
    try:
        # Total products from StarRocks
        rows = await db.execute_query_async(f"""
            SELECT COUNT(DISTINCT internal_ref_code) AS total
            FROM {TABLE_NAME}
        """)
        total = rows[0]["total"] if rows else 0

        # Saved descriptions from Postgres
        pg_stats = UltraDescriptionDB.get_stats()
        has_desc = pg_stats.get("total", 0)

        return {
            "status": "success",
            "total": total,
            "has_desc": has_desc,
            "approved": pg_stats.get("approved", 0),
            "pending": pg_stats.get("pending", 0),
            "missing": total - has_desc,
            "progress": round(has_desc / total * 100, 1) if total > 0 else 0,
            "db_stats": {
                "enriched": pg_stats.get("enriched", 0),
                "raw_only": pg_stats.get("raw_only", 0),
                "last_updated": str(pg_stats.get("last_updated", "")) if pg_stats.get("last_updated") else None,
            },
        }
    except Exception as e:
        logger.error(f"Overview error: {e}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})


@router.get("/batch-status", summary="Get progress of current batch generation")
async def batch_status():
    """Return the progress of the background batch task."""
    return BATCH_STATE

# ═══ 2. LIST — Products with description status ═══
@router.get("/list", summary="List products with description status")
async def product_desc_list(
    status: Optional[str] = Query(None, description="Filter: 'missing' or 'has_desc'"),
    search: Optional[str] = Query(None, description="Search by name or code"),
    product_line: Optional[str] = Query(None, description="Filter by product line"),
    limit: int = Query(30, ge=1, le=200),
    page: int = Query(1, ge=1),
):
    """List products with their description status."""
    db = get_db_connection()
    offset = (page - 1) * limit
    clauses = []
    params = []

    if search:
        clauses.append("(LOWER(product_name) LIKE %s OR internal_ref_code LIKE %s)")
        params.extend([f"%{search.lower()}%", f"%{search.upper()}%"])
    if product_line:
        clauses.append("LOWER(product_line_vn) LIKE %s")
        params.append(f"%{product_line.lower()}%")

    where = " AND ".join(clauses) if clauses else "1=1"

    try:
        # Get codes with their approval status from Postgres
        code_status_map = UltraDescriptionDB.get_all_codes_with_status()

        # Count
        count_rows = await db.execute_query_async(
            f"SELECT COUNT(DISTINCT internal_ref_code) AS total FROM {TABLE_NAME} WHERE {where}",
            params=tuple(params) if params else None,
        )
        total = count_rows[0]["total"] if count_rows else 0

        products = await db.execute_query_async(
            f"""
            SELECT
                internal_ref_code,
                ANY_VALUE(product_name) AS product_name,
                ANY_VALUE(product_image_url_thumbnail) AS product_image_url_thumbnail,
                ANY_VALUE(product_line_vn) AS product_line_vn,
                ANY_VALUE(gender_by_product) AS gender_by_product,
                MIN(sale_price) AS sale_price,
                MAX(original_price) AS original_price,
                MAX(quantity_sold) AS quantity_sold,
                COUNT(DISTINCT product_color_code) AS color_count,
                GROUP_CONCAT(DISTINCT master_color) AS colors
            FROM {TABLE_NAME}
            WHERE {where}
            GROUP BY internal_ref_code
            ORDER BY quantity_sold DESC
            LIMIT %s OFFSET %s
            """,
            params=tuple(params + [limit, offset]),
        )

        # Mark desc_status: -1 = missing, 0 = pending, 1 = approved
        for p in products:
            code = p.get("internal_ref_code")
            if code in code_status_map:
                p["has_desc"] = 1
                p["desc_status"] = code_status_map[code]  # 0 or 1
            else:
                p["has_desc"] = 0
                p["desc_status"] = -1  # no description

        # Post-filter by status
        if status == "missing":
            products = [p for p in products if p["desc_status"] == -1]
        elif status == "has_desc":
            products = [p for p in products if p["desc_status"] >= 0]
        elif status == "pending":
            products = [p for p in products if p["desc_status"] == 0]
        elif status == "approved":
            products = [p for p in products if p["desc_status"] == 1]

        return {
            "status": "success",
            "total": total,
            "limit": limit,
            "page": page,
            "products": products,
        }
    except Exception as e:
        logger.error(f"Product desc list error: {e}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})


# ═══ 3. GENERATE — Generate description for a single product ═══
class GenerateRequest(BaseModel):
    internal_ref_code: str


@router.post("/generate", summary="Generate ultra description for a product by code")
async def generate_description(req: GenerateRequest):
    """
    Full pipeline:
    1. Fetch product data + image from StarRocks
    2. Phase 1: Vision → raw JSON from image
    3. Phase 2: Enrichment → ultra description using real stock data
    """
    db = get_db_connection()

    try:
        # ── Fetch product data ──
        rows = await db.execute_query_async(
            f"""
            SELECT
                internal_ref_code,
                ANY_VALUE(product_name) AS product_name,
                ANY_VALUE(product_image_url_thumbnail) AS image_url,
                MIN(sale_price) AS sale_price,
                MAX(original_price) AS original_price,
                ANY_VALUE(product_line_vn) AS product_line,
                ANY_VALUE(gender_by_product) AS gender,
                ANY_VALUE(age_by_product) AS age,
                MAX(quantity_sold) AS quantity_sold,
                ANY_VALUE(size_scale) AS size_scale,
                COUNT(DISTINCT product_color_code) AS color_count,
                GROUP_CONCAT(DISTINCT master_color) AS colors,
                ANY_VALUE(description_text) AS description_text
            FROM {TABLE_NAME}
            WHERE internal_ref_code = %s
            GROUP BY internal_ref_code
            """,
            params=(req.internal_ref_code,),
        )

        if not rows:
            return JSONResponse(status_code=404, content={"status": "error", "message": "Không tìm thấy sản phẩm"})

        product = rows[0]
        image_url = product.get("image_url", "")

        if not image_url:
            return JSONResponse(status_code=400, content={"status": "error", "message": "Sản phẩm không có ảnh"})

        # ── Phase 1: Vision ──
        logger.info(f"📸 Phase 1: Vision analyzing {req.internal_ref_code}...")
        image_base64 = await _fetch_image_base64(image_url)

        # ── Fetch Active Fields ──
        active_fields = DescFieldConfig.get_active_fields()
        num_fields = len(active_fields)
        schema_lines = []
        instruction_lines = []
        for f in active_fields:
            schema_lines.append(f'  "{f["field_key"]}": "{f["field_label"]}"')
            if f["field_instruction"]:
                instruction_lines.append(f'- {f["field_key"]}: {f["field_instruction"]}')

        schema_json = "{\n" + ",\n".join(schema_lines) + "\n}"
        instruction_text = "\n".join(instruction_lines)

        dynamic_vision_prompt = f"""Bạn là chuyên gia phân tích sản phẩm thời trang. Nhìn ảnh sản phẩm và trả về JSON với ĐÚNG {num_fields} trường sau.
QUAN TRỌNG: KHÔNG đoán chất liệu nếu không chắc. KHÔNG bỏ trống trường nào. Nếu không chắc, ghi "Không xác định".

{schema_json}

Trả về ĐÚNG JSON, không text thừa."""

        vision_messages = [
            {"role": "system", "content": dynamic_vision_prompt},
            {
                "role": "user",
                "content": [
                    {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"}},
                    {"type": "text", "text": "Phân tích kỹ sản phẩm thời trang trong ảnh. Trả về JSON 20 trường. KHÔNG đoán chất liệu."},
                ],
            },
        ]

        raw_output = await _call_ai_with_fallback(vision_messages, model=VISION_MODEL, temperature=0.2, is_vision=True)
        raw_json = _extract_json(raw_output)

        if not raw_json:
            return JSONResponse(status_code=500, content={
                "status": "error", "message": "Phase 1 thất bại: không parse được JSON từ ảnh",
                "raw": raw_output[:500],
            })

        logger.info(f"✅ Phase 1 OK: {len(raw_json)} fields")

        # ── Phase 2: Enrichment with real data ──
        logger.info(f"✨ Phase 2: Enrichment with stock data...")

        magento_desc = product.get('description_text', '') or ''

        stock_context = f"""
THÔNG TIN THẬT TỪ DATABASE (ưu tiên dùng data này):
- Tên sản phẩm: {product.get('product_name', '')}
- Giá gốc: {product.get('original_price', '')}đ | Giá sale: {product.get('sale_price', '')}đ
- Giới tính: {product.get('gender', '')}
- Độ tuổi: {product.get('age', '')}
- Dòng sản phẩm: {product.get('product_line', '')}
- Bảng size: {product.get('size_scale', '')}
- Số màu: {product.get('color_count', '')} ({product.get('colors', '')})
- Lượt bán: {product.get('quantity_sold', '')}
{f'- MÔ TẢ GỐC TỪ MAGENTO (trích xuất chất liệu, tính năng vải, hướng dẫn giặt từ đây): {magento_desc[:500]}' if magento_desc else '- Mô tả gốc: Không có'}

DANH SÁCH DÒNG SẢN PHẨM CỦA CANIFA (dùng cho cross_sell và phoi_do):
{CANIFA_PRODUCT_LINES}
"""

        user_content = f"""Đây là JSON thô từ AI Vision. Nhiệm vụ: VIẾT LẠI TOÀN BỘ {num_fields} trường với văn phong Stylist đỉnh cao.

QUY TẮC:
- KHÔNG copy-paste. Mỗi trường phải được VIẾT LẠI hoàn toàn.
{instruction_text}
- CẤM nói về giá cả.

{stock_context}

VÍ DỤ OUTPUT (học theo format):
{ENRICHMENT_EXAMPLE}

JSON THÔ CẦN VIẾT LẠI:
{json.dumps(raw_json, ensure_ascii=False, indent=2)}

Trả về ĐÚNG JSON, không text rác."""

        enrich_messages = [
            {"role": "system", "content": ENRICHMENT_SYSTEM},
            {"role": "user", "content": user_content},
        ]

        enriched_output = await _call_ai_with_fallback(enrich_messages, model=ENRICH_MODEL, max_tokens=3000, temperature=0.7)
        enriched_json = _extract_json(enriched_output)

        if enriched_json:
            logger.info(f"✅ Phase 2 OK: {len(enriched_json)} fields enriched")
            final_data = enriched_json
            final_phase = "enriched"
        else:
            logger.warning("⚠️ Phase 2 failed JSON parse, saving raw text as metadata fallback")
            final_data = raw_json.copy() if raw_json else {}
            final_data["_ai_raw_metadata"] = {
                "error": "JSON_PARSE_ERROR",
                "content": enriched_output
            }
            final_phase = "raw_fallback"

        # ── Save to PostgreSQL ──
        try:
            # Note: UltraDescriptionDB.save() already has ON CONFLICT DO UPDATE SET status = 0
            # which automatically resets the approval status back to "Chờ duyệt" (0).
            UltraDescriptionDB.save(
                internal_ref_code=req.internal_ref_code,
                product_name=product.get("product_name"),
                product_image_url=image_url,
                product_line=product.get("product_line"),
                description_data=final_data,
                phase=final_phase,
                clean_description=_render_description_text(final_data, product.get("product_name", "")),
            )
            logger.info(f"💾 Saved to PostgreSQL: {req.internal_ref_code}")
        except Exception as save_err:
            logger.error(f"⚠️ Save to DB failed (still returning data): {save_err}")

        result = {
            "status": "success",
            "phase": final_phase,
            "product": product,
            "data": final_data,
            "phase1_raw": raw_json,
            "saved": True,
        }
        if final_phase == "raw":
            result["phase2_raw_text"] = enriched_output[:2000]
            result["warning"] = "Phase 2 trả về text không phải JSON. Hiển thị data thô Phase 1."
        return result

    except httpx.TimeoutException:
        return JSONResponse(status_code=504, content={"status": "error", "message": "Groq API timeout"})
    except Exception as e:
        logger.error(f"Generate error: {e}", exc_info=True)
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})


# ═══ 4. PRODUCT LINE FILTERS ═══
@router.get("/filters", summary="Available product lines for filtering")
async def desc_filters():
    db = get_db_connection()
    try:
        lines = await db.execute_query_async(f"""
            SELECT product_line_vn, COUNT(DISTINCT internal_ref_code) AS cnt
            FROM {TABLE_NAME}
            WHERE product_line_vn IS NOT NULL AND product_line_vn != ''
            GROUP BY product_line_vn
            ORDER BY cnt DESC
        """)
        return {"status": "success", "product_lines": lines}
    except Exception as e:
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})


# ═══ 5. GET SAVED — Retrieve saved description from Postgres ═══
@router.get("/saved/{internal_ref_code}", summary="Get saved ultra description")
async def get_saved_desc(internal_ref_code: str):
    """Retrieve a previously saved ultra description from Postgres."""
    try:
        row = UltraDescriptionDB.get_by_code(internal_ref_code)
        if not row:
            return JSONResponse(status_code=404, content={
                "status": "not_found",
                "message": f"Chưa có ultra description cho {internal_ref_code}",
            })
        # Convert datetime to string for JSON serialization
        result = {}
        for k, v in row.items():
            if hasattr(v, 'isoformat'):
                result[k] = v.isoformat()
            else:
                result[k] = v

        # Render formatted text from description_data
        desc_data = result.get("description_data", {})
        if isinstance(desc_data, str):
            import json as _json
            try:
                desc_data = _json.loads(desc_data)
            except Exception:
                desc_data = {}
        result["rendered_text"] = _render_description_text(desc_data, result.get("product_name", ""))

        return {"status": "success", "description": result}
    except Exception as e:
        logger.error(f"Get saved desc error: {e}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})

class UpdateDescRequest(BaseModel):
    description_data: dict

@router.post("/saved/{internal_ref_code}/update", summary="Manually update saved description fields")
async def update_saved_desc(internal_ref_code: str, req: UpdateDescRequest):
    """Update description_data manually from UI and re-render clean_description."""
    try:
        row = UltraDescriptionDB.get_by_code(internal_ref_code)
        if not row:
            return JSONResponse(status_code=404, content={"status": "error", "message": "Not found"})

        # Render clean formatting
        clean_desc = _render_description_text(req.description_data, row.get("product_name", ""))

        # Update DB
        updated = UltraDescriptionDB.update_data(internal_ref_code, req.description_data, clean_desc)
        if updated:
            return {"status": "success", "message": "Đã cập nhật nội dung", "clean_description": clean_desc}
        return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to update DB"})
    except Exception as e:
        logger.error(f"Update desc error: {e}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})


class UpdateTextRequest(BaseModel):
    clean_description: str

@router.post("/saved/{internal_ref_code}/update-text", summary="Manually update rendered text from popup")
async def update_saved_desc_text(internal_ref_code: str, req: UpdateTextRequest):
    """Save the manually edited HTML/text from the popup back to the database."""
    try:
        updated = UltraDescriptionDB.update_clean_text(internal_ref_code, req.clean_description)
        if updated:
            return {"status": "success", "message": "Đã lưu nội dung text"}
        return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to update DB"})
    except Exception as e:
        logger.error(f"Update text error: {e}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})


        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})


class RewriteFieldRequest(BaseModel):
    field_key: str

@router.post("/saved/{internal_ref_code}/rewrite-field", summary="Rewrite a specific field using AI")
async def rewrite_field_ai(internal_ref_code: str, req: RewriteFieldRequest):
    """Use AI to rewrite a single specific field in the description JSON."""
    try:
        from llm.api_factory import get_llm
        row = UltraDescriptionDB.get_by_code(internal_ref_code)
        if not row:
            return JSONResponse(status_code=404, content={"status": "error", "message": "Not found"})

        desc_data = row.get("description_data", {})
        if isinstance(desc_data, str):
            import json
            desc_data = json.loads(desc_data)

        old_value = desc_data.get(req.field_key, "")

        # Get instructions for this field
        fields = DescFieldConfig.get_active_fields()
        field_instr = ""
        for f in fields:
            if f["field_key"] == req.field_key:
                field_instr = f["field_instruction"]
                break

        prompt = f"""
Bạn là một chuyên gia sáng tạo nội dung thời trang của Canifa.
Nhiệm vụ của bạn là VIẾT LẠI MỘT trường thông tin duy nhất cho chuẩn, hay và tự nhiên hơn.

- Trường cần viết lại: {req.field_key}
- Định hướng (NẾU CÓ): {field_instr}

[Nội dung cũ của trường]:
{old_value}

Yêu cầu:
- Viết lại nội dung cho hấp dẫn hơn, đúng văn phong chuyên nghiệp.
- KHÔNG tạo ra thêm các định dạng markdown gạch đầu dòng nếu nội dung cũ không có.
- CHỈ TRẢ VỀ NỘI DUNG MỚI. KHÔNG GIẢI THÍCH, KHÔNG CHAT.
"""
        llm = get_llm(temperature=0.7)
        resp = await llm.ainvoke(prompt)
        new_text = resp.content.strip()
        new_text = new_text.replace("```json", "").replace("```html", "").replace("```", "").strip()

        # update JSON
        desc_data[req.field_key] = new_text

        # Update clean desc
        clean_desc = _render_description_text(desc_data, row.get("product_name", ""))

        updated = UltraDescriptionDB.update_data(internal_ref_code, desc_data, clean_desc)
        if updated:
            return {"status": "success", "new_value": new_text, "clean_description": clean_desc}
        return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to update DB"})
    except Exception as e:
        logger.error(f"Rewrite error: {e}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})


# ═══ 6. APPROVE / REJECT ═══
class StatusRequest(BaseModel):
    internal_ref_code: str
    status: int  # 0 = pending, 1 = approved


@router.post("/approve", summary="Approve or reject a description")
async def approve_desc(req: StatusRequest):
    """Set status: 0 = chờ duyệt, 1 = đã duyệt. Khi duyệt → tự render clean_description."""
    if req.status not in (0, 1):
        return JSONResponse(status_code=400, content={"status": "error", "message": "Status phải là 0 hoặc 1"})
    try:
        clean_desc = ""
        if req.status == 1:
            # Render clean_description khi duyệt
            row = UltraDescriptionDB.get_by_code(req.internal_ref_code)
            if row:
                desc_data = row.get("description_data", {})
                if isinstance(desc_data, str):
                    try:
                        desc_data = json.loads(desc_data)
                    except Exception:
                        desc_data = {}
                clean_desc = _render_description_text(desc_data, row.get("product_name", ""))

        updated = UltraDescriptionDB.set_status(req.internal_ref_code, req.status, clean_description=clean_desc)
        if updated:
            label = "Đã duyệt" if req.status == 1 else "Hủy duyệt"
            return {"status": "success", "message": f"{label}: {req.internal_ref_code}"}
        return JSONResponse(status_code=404, content={
            "status": "not_found",
            "message": f"Không tìm thấy description cho {req.internal_ref_code}",
        })
    except Exception as e:
        logger.error(f"Approve error: {e}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})


@router.post("/approve-all", summary="Bulk approve ALL pending descriptions")
async def approve_all_desc():
    """Set all status=0 to status=1 in one shot."""
    try:
        count = UltraDescriptionDB.approve_all_pending(render_fn=_render_description_text)
        return {"status": "success", "message": f"✅ Đã duyệt toàn bộ {count} mô tả!", "approved_count": count}
    except Exception as e:
        logger.error(f"Approve all error: {e}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})

# ═══ 7. DELETE ═══
@router.delete("/saved/{internal_ref_code}", summary="Delete saved ultra description")
async def delete_saved_desc(internal_ref_code: str):
    """Delete a saved ultra description from Postgres."""
    try:
        deleted = UltraDescriptionDB.delete_by_code(internal_ref_code)
        if deleted:
            return {"status": "success", "message": f"Đã xóa description cho {internal_ref_code}"}
        return JSONResponse(status_code=404, content={
            "status": "not_found",
            "message": f"Không tìm thấy description cho {internal_ref_code}",
        })
    except Exception as e:
        logger.error(f"Delete desc error: {e}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})


# ═══ 8. BATCH GENERATE (REDIS QUEUE) ═══
class BatchGenerateRequest(BaseModel):
    internal_ref_codes: list[str]

@router.post("/batch-generate", summary="Trigger batch generation via Redis Worker")
async def batch_generate_descriptions(req: BatchGenerateRequest):
    """Start background generation for multiple products via Redis queue."""
    if not req.internal_ref_codes:
        return JSONResponse(status_code=400, content={"status": "error", "message": "List products is empty"})

    # Capping at 50 products per request for safety
    codes = req.internal_ref_codes[:50]

    redis = redis_cache.get_client()
    if not redis:
        return JSONResponse(status_code=500, content={"status": "error", "message": "Redis cache is offline, cannot run batch"})

    job_id = str(uuid.uuid4())
    job_payload = {
        "job_id": job_id,
        "codes": codes
    }

    try:
        await redis.lpush("desc:queue", json.dumps(job_payload))
    except Exception as e:
        logger.error(f"Redis error: {e}")
        return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to enqueue job in Redis"})

    return {
        "status": "success",
        "message": f"Đưa {len(codes)} sản phẩm vào hàng đợi xử lý Background Worker.",
        "job_id": job_id,
        "queued_codes": codes
    }

# ═══ 9. BATCH GENERATE ALL (REDIS QUEUE) ═══
@router.post("/batch-generate-all", summary="Trigger batch generation for ALL missing products up to 500 items")
async def batch_generate_all():
    try:
        # 1. Fetch all existing codes from Postgres
        code_status_map = UltraDescriptionDB.get_all_codes_with_status()

        # 2. Fetch all unique product codes from StarRocks
        db = get_db_connection()
        query = f"SELECT DISTINCT internal_ref_code FROM {TABLE_NAME}"
        rows = await db.execute_query_async(query)

        all_codes = [r["internal_ref_code"] for r in rows if r.get("internal_ref_code")]

        # 3. Filter to find missing codes
        missing_codes = [code for code in all_codes if code not in code_status_map]

        # 4. Limit to 500
        codes = missing_codes[:500]

        if not codes:
            return {"status": "success", "message": "Tuyệt vời! Không có sản phẩm nào thiếu AI Description."}

        redis = redis_cache.get_client()
        if not redis:
            return JSONResponse(status_code=500, content={"status": "error", "message": "Redis cache is offline"})

        job_id = str(uuid.uuid4())
        job_payload = {
            "job_id": job_id,
            "codes": codes
        }
        await redis.lpush("desc:queue", json.dumps(job_payload))

        return {
            "status": "success",
            "message": f"Tìm thấy {len(codes)} sản phẩm thiếu. Đã đưa vào queue cho Background Worker.",
            "job_id": job_id,
            "queued_codes": codes
        }
    except Exception as e:
        logger.error(f"Batch All Error: {e}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})


@router.get("/batch-status", summary="Get status of latest batch generation job")
@router.get("/batch-status/{job_id}", summary="Get status of a specific batch generation job")
async def get_batch_status(job_id: Optional[str] = None):
    """Retrieve progress status from Redis."""
    try:
        redis = redis_cache.get_client()
        if not redis:
            return JSONResponse(status_code=500, content={"status": "error", "message": "Redis is offline"})

        if not job_id:
            job_id = await redis.get("desc:progress:latest_job")
            if not job_id:
                return {
                    "status": "success",
                    "progress": None
                }
            if isinstance(job_id, bytes):
                job_id = job_id.decode('utf-8')

        progress_key = f"desc:progress:{job_id}"
        data = await redis.hgetall(progress_key)

        if not data:
            return {
                "status": "success",
                "message": "Job not found or already expired",
                "progress": None
            }

        # Since decode_responses=True, data is already string
        decoded_data = data
        return {
            "status": "success",
            "job_id": job_id,
            "progress": {
                "is_running": decoded_data.get("is_running") == "true",
                "total": int(decoded_data.get("total", 0)),
                "done": int(decoded_data.get("done", 0)),
                "errors": int(decoded_data.get("errors", 0)),
                "current_code": decoded_data.get("current_code", "")
            }
        }
    except Exception as e:
        logger.error(f"Batch Status Error: {e}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})

# ═══ 10. DYNAMIC FIELDS CRUD ═══

class FieldCreateRequest(BaseModel):
    field_key: str
    field_label: str
    field_instruction: str = ""
    sort_order: int = 99

class FieldUpdateRequest(BaseModel):
    field_label: str | None = None
    field_instruction: str | None = None
    is_active: bool | None = None
    sort_order: int | None = None


@router.get("/fields", summary="Get all dynamic fields")
async def get_fields():
    """Get all dynamic fields (active and inactive) for admin UI."""
    try:
        fields = DescFieldConfig.get_all_fields()
        return {"status": "success", "data": fields}
    except Exception as e:
        logger.error(f"Get fields error: {e}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})


@router.post("/fields", summary="Add a new dynamic field")
async def add_field(req: FieldCreateRequest):
    """Add a new custom field to the description schema."""
    try:
        success = DescFieldConfig.add_field(
            req.field_key, req.field_label, req.field_instruction, req.sort_order
        )
        if success:
            return {"status": "success", "message": f"Đã thêm trường {req.field_key}"}
        return JSONResponse(status_code=400, content={"status": "error", "message": "Field key may already exist"})
    except Exception as e:
        logger.error(f"Add field error: {e}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})


@router.put("/fields/{field_key}", summary="Update a dynamic field")
async def update_field(field_key: str, req: FieldUpdateRequest):
    """Update a custom field (label, instruction, status, order)."""
    try:
        success = DescFieldConfig.update_field(
            field_key, req.field_label, req.field_instruction, req.is_active, req.sort_order
        )
        if success:
            return {"status": "success", "message": f"Đã cập nhật trường {field_key}"}
        return JSONResponse(status_code=404, content={"status": "error", "message": f"Không tìm thấy trường {field_key}"})
    except Exception as e:
        logger.error(f"Update field error: {e}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})


@router.delete("/fields/{field_key}", summary="Delete a dynamic field")
async def delete_field(field_key: str):
    """Completely remove a field from the schema."""
    try:
        success = DescFieldConfig.delete_field(field_key)
        if success:
            return {"status": "success", "message": f"Đã xóa trường {field_key}"}
        return JSONResponse(status_code=404, content={"status": "error", "message": f"Không tìm thấy trường {field_key}"})
    except Exception as e:
        logger.error(f"Delete field error: {e}")
        return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
