import json
import logging
import re
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional, Union

from langchain_core.runnables import RunnableConfig
from langchain_core.tools import tool
from pydantic import BaseModel, Field

from .tool_module import SearchEngine as LegacySearchEngine

logger = logging.getLogger(__name__)

PROJECT_ROOT = Path(__file__).resolve().parents[3]
if str(PROJECT_ROOT) not in sys.path:
    sys.path.append(str(PROJECT_ROOT))

try:
    from preference.lead_stage_agent.product_search_engine import (
        InferredSearch as ProductInferredSearch,
        LeadSearchInput,
        LiteralSearch as ProductLiteralSearch,
        ProductSearchEngine,
    )
except Exception as exc:  # pragma: no cover - exercised only when optional import is unavailable
    ProductInferredSearch = None
    ProductLiteralSearch = None
    ProductSearchEngine = None
    LeadSearchInput = None
    logger.warning("ProductSearchEngine import failed; legacy SearchEngine fallback will be used: %s", exc)


class LiteralSearch(BaseModel):
    model_config = {"extra": "ignore"}

    raw_text: str = Field(default="", description="Cau search nguyen van tu user.")


class InferredSearch(BaseModel):
    model_config = {"extra": "ignore"}

    product_line_vn: List[str] = Field(default_factory=list, description="Dong san pham.")
    gender_by_product: Optional[str] = Field(default=None, description="Gioi tinh.")
    age_by_product: Optional[str] = Field(default=None, description="Do tuoi.")
    gender_target: Optional[str] = Field(default=None, description="Alias cu cua gender_by_product.")
    age_group: Optional[str] = Field(default=None, description="Alias cu cua age_by_product.")
    master_color: Optional[str] = Field(default=None, description="Mau sac.")
    tags: List[str] = Field(default_factory=list, description="Intent tags: occ/style/fit/weather/function.")
    keywords: List[str] = Field(default_factory=list, description="Tu khoa bo tro cho search.")
    price_min: Optional[int] = Field(default=None, description="Gia thap nhat.")
    price_max: Optional[int] = Field(default=None, description="Gia cao nhat.")
    discount_min: Optional[int] = Field(default=None, description="% giam gia toi thieu.")
    discount_max: Optional[int] = Field(default=None, description="% giam gia toi da.")
    discovery_mode: Optional[str] = Field(default=None, description="new/best_seller.")
    size: Optional[str] = Field(default=None, description="Size can tim.")


class SearchItem(BaseModel):
    """
    Supports both the current dual-lane schema and older flat tool calls.
    Older endpoints still pass fields like query/product_name/product_line_vn directly.
    """

    model_config = {"extra": "ignore"}

    literal: LiteralSearch = Field(default_factory=LiteralSearch)
    inferred: InferredSearch = Field(default_factory=InferredSearch)
    magento_ref_code: Optional[str] = None
    reasoning: Optional[str] = None
    user_insight: Optional[Dict[str, Any]] = None

    # Legacy flat fields.
    query: Optional[str] = None
    raw_text: Optional[str] = None
    description: Optional[str] = None
    product_name: Optional[str] = None
    product_line_vn: Optional[Union[str, List[str]]] = None
    gender_by_product: Optional[str] = None
    age_by_product: Optional[str] = None
    gender_target: Optional[str] = None
    age_group: Optional[str] = None
    master_color: Optional[str] = None
    tags: Optional[Union[str, List[str]]] = None
    keywords: Optional[Union[str, List[str]]] = None
    price_min: Optional[int] = None
    price_max: Optional[int] = None
    discount_min: Optional[int] = None
    discount_max: Optional[int] = None
    discovery_mode: Optional[str] = None
    size: Optional[str] = None


class MultiSearchParams(BaseModel):
    searches: List[SearchItem] = Field(..., description="Danh sach cac yeu cau tim kiem.")
    reasoning: Optional[str] = None
    user_insight: Optional[Dict[str, Any]] = None


_PRODUCT_ENGINE = ProductSearchEngine() if ProductSearchEngine else None


def _coerce_list(value: Any) -> List[str]:
    if value is None:
        return []
    if isinstance(value, list):
        return [str(v).strip() for v in value if str(v).strip()]
    if isinstance(value, tuple):
        return [str(v).strip() for v in value if str(v).strip()]
    text = str(value).strip()
    if not text:
        return []
    if "/" in text:
        return [part.strip() for part in text.split("/") if part.strip()]
    return [text]


def _clean_legacy_description(description: Optional[str]) -> str:
    if not description:
        return ""
    text = str(description)
    text = re.sub(
        r"\b(product_name|description_text|product_line_vn|material_group|style|fitting|form_sleeve|form_neckline|season)\s*:\s*",
        " ",
        text,
        flags=re.IGNORECASE,
    )
    text = re.sub(r"[.;]+", " ", text)
    return re.sub(r"\s+", " ", text).strip()


def _first_text(*values: Any) -> str:
    for value in values:
        if value is None:
            continue
        text = str(value).strip()
        if text:
            return text
    return ""


def _build_literal_text(item: SearchItem) -> str:
    return _first_text(
        item.literal.raw_text,
        item.raw_text,
        item.query,
        item.product_name,
        _clean_legacy_description(item.description),
    )


def _set_if_present(target: Dict[str, Any], key: str, value: Any) -> None:
    if value is None:
        return
    if isinstance(value, list) and not value:
        return
    if value == "":
        return
    target[key] = value


def _build_inferred_payload(item: SearchItem) -> Dict[str, Any]:
    data = item.inferred.model_dump(exclude_none=True)

    if data.get("gender_target") and not data.get("gender_by_product"):
        data["gender_by_product"] = data["gender_target"]
    if data.get("age_group") and not data.get("age_by_product"):
        data["age_by_product"] = data["age_group"]

    flat_fields = [
        "gender_by_product",
        "age_by_product",
        "gender_target",
        "age_group",
        "master_color",
        "price_min",
        "price_max",
        "discount_min",
        "discount_max",
        "discovery_mode",
        "size",
    ]
    for field_name in flat_fields:
        current = data.get(field_name)
        if current not in (None, "", []):
            continue
        value = getattr(item, field_name, None)
        _set_if_present(data, field_name, value)

    if data.get("gender_target") and not data.get("gender_by_product"):
        data["gender_by_product"] = data["gender_target"]
    if data.get("age_group") and not data.get("age_by_product"):
        data["age_by_product"] = data["age_group"]

    product_lines = data.get("product_line_vn") or item.product_line_vn
    data["product_line_vn"] = _coerce_list(product_lines)
    data["tags"] = _coerce_list(data.get("tags") or item.tags)
    data["keywords"] = _coerce_list(data.get("keywords") or item.keywords)

    return {
        "product_line_vn": data.get("product_line_vn", []),
        "gender_by_product": data.get("gender_by_product"),
        "age_by_product": data.get("age_by_product"),
        "master_color": data.get("master_color"),
        "tags": data.get("tags", []),
        "keywords": data.get("keywords", []),
        "price_min": data.get("price_min"),
        "price_max": data.get("price_max"),
        "discount_min": data.get("discount_min"),
        "discount_max": data.get("discount_max"),
        "discovery_mode": data.get("discovery_mode"),
        "size": data.get("size"),
    }


def _build_lead_input(item: SearchItem) -> Any:
    if not (LeadSearchInput and ProductLiteralSearch and ProductInferredSearch):
        return None
    return LeadSearchInput(
        literal=ProductLiteralSearch(raw_text=_build_literal_text(item)),
        inferred=ProductInferredSearch(**_build_inferred_payload(item)),
        magento_ref_code=item.magento_ref_code,
        reasoning=item.reasoning,
    )


def _augment_product(product: Dict[str, Any]) -> Dict[str, Any]:
    p = dict(product)
    sku = (
        p.get("sku")
        or p.get("magento_ref_code")
        or p.get("internal_ref_code")
        or p.get("code")
        or ""
    )
    name = p.get("name") or p.get("product_name") or p.get("magento_product_name") or ""
    price = p.get("price", p.get("sale_price", p.get("sale_price_vnd", 0)))
    original_price = p.get("original_price", p.get("price_vnd", price))
    image = p.get("image") or p.get("product_image_url_thumbnail") or p.get("thumbnail_image_url") or ""
    url = p.get("url") or p.get("product_web_url") or p.get("magento_url_key") or ""

    p.setdefault("sku", sku)
    p.setdefault("code", sku)
    p.setdefault("magento_ref_code", sku)
    p.setdefault("internal_ref_code", sku)
    p.setdefault("name", name)
    p.setdefault("product_name", name)
    p.setdefault("magento_product_name", name)
    p.setdefault("price", price)
    p.setdefault("sale_price", price)
    p.setdefault("sale_price_vnd", price)
    p.setdefault("price_vnd", original_price)
    p.setdefault("image", image)
    p.setdefault("product_image_url_thumbnail", image)
    p.setdefault("thumbnail_image_url", image)
    p.setdefault("url", url)
    p.setdefault("product_web_url", url)
    p.setdefault("magento_url_key", url)
    return p


def _dedup_products(products: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    seen: set[str] = set()
    deduped: List[Dict[str, Any]] = []
    for product in products:
        sku = str(product.get("sku") or product.get("magento_ref_code") or product.get("internal_ref_code") or "")
        if sku and sku in seen:
            continue
        if sku:
            seen.add(sku)
        deduped.append(product)
    return deduped


async def _run_product_search_engine(
    item: SearchItem,
    *,
    reasoning: Optional[str],
    user_insight: Optional[Dict[str, Any]],
) -> Dict[str, Any]:
    if not _PRODUCT_ENGINE:
        raise RuntimeError("ProductSearchEngine is not available")

    req = _build_lead_input(item)
    result = await _PRODUCT_ENGINE.search(
        req,
        reasoning=item.reasoning or reasoning,
        user_insight=item.user_insight or user_insight,
    )
    products = [_augment_product(p) for p in result.get("products", [])]
    result["products"] = products
    result["engine_used"] = "product_search_engine"
    return result


async def _run_legacy_engine(item: SearchItem, *, use_sqlite: bool) -> Dict[str, Any]:
    engine = LegacySearchEngine(use_sqlite=use_sqlite)
    res = await engine.search(
        literal=_build_literal_text(item),
        inferred=_build_inferred_payload(item),
    )
    products = [_augment_product(p) for p in res.get("results", [])]
    return {
        "status": "success" if products else "no_results",
        "count": len(products),
        "tier": res.get("tier", 0),
        "search_mode": res.get("message", "legacy_search"),
        "message": res.get("message", ""),
        "products": products,
        "engine_used": "legacy_search_engine",
    }


@tool(args_schema=MultiSearchParams)
async def data_retrieval_tool(
    searches: List[SearchItem],
    reasoning: Optional[str] = None,
    user_insight: Optional[Dict[str, Any]] = None,
    config: RunnableConfig = None,
) -> str:
    """
    Tim kiem san pham Canifa bang ProductSearchEngine core.
    Wrapper nay giu compatibility voi schema cu va schema dual-lane moi.
    """
    configurable = config.get("configurable", {}) if config else {}
    db_source = configurable.get("db_source", "starrocks")
    use_sqlite = db_source == "sqlite"
    shared_user_insight = user_insight or configurable.get("user_insight")

    per_search_results: List[Dict[str, Any]] = []
    all_products: List[Dict[str, Any]] = []
    errors: List[str] = []

    for item in searches:
        literal_text = _build_literal_text(item)
        logger.info("[Tool] Searching via ProductSearchEngine: %s", literal_text)
        try:
            result = await _run_product_search_engine(
                item,
                reasoning=reasoning,
                user_insight=shared_user_insight,
            )
        except Exception as exc:
            logger.exception("ProductSearchEngine failed; falling back to legacy engine: %s", exc)
            result = await _run_legacy_engine(item, use_sqlite=use_sqlite)

        if result.get("status") == "error":
            errors.append(str(result.get("message", "unknown error")))

        products = [_augment_product(p) for p in result.get("products", [])]
        result["products"] = products
        result["literal_text"] = result.get("literal_text") or literal_text
        per_search_results.append(result)
        all_products.extend(products)

    products = _dedup_products(all_products)
    has_success = any(r.get("status") == "success" for r in per_search_results)
    has_products = bool(products)
    status = "success" if has_success or has_products else ("error" if errors else "no_results")

    payload = {
        "status": status,
        "count": len(products),
        "products": products,
        "results": per_search_results,
    }
    if errors:
        payload["errors"] = errors

    return json.dumps(payload, ensure_ascii=False, default=str)
