"""One model call per batch of products, producing validated attributes.

The model's output is never trusted: anything outside the declared schema, or
outside a closed set, is discarded rather than stored. Two of these fields reach
columns a later phase filters on, and one bad value there makes a filter silently
miss products.
"""
import json
import logging

from app.core.config import settings
from app.core.llm_client import client
from app.core.prompts import ATTRIBUTE_EXTRACTION_PROMPT

logger = logging.getLogger(__name__)

BATCH_SIZE = 20
DESCRIPTION_CHARS = 400
MAX_KEY_FEATURES = 5
MAX_ACCESSORY_TARGETS = 4

GENDERS = ("male", "female", "unisex", "kids")
SIZE_SYSTEMS = ("alpha", "numeric", "uk", "eu", "us", "volume")
PRICE_TIERS = ("budget", "mid", "premium")

STRING_FIELDS = ("color", "material", "style", "use_case")
CLOSED_SETS = {"gender": GENDERS, "size_system": SIZE_SYSTEMS,
               "price_tier": PRICE_TIERS}

_SYSTEM = ATTRIBUTE_EXTRACTION_PROMPT.format(
    genders=GENDERS, size_systems=SIZE_SYSTEMS, price_tiers=PRICE_TIERS,
    max_key_features=MAX_KEY_FEATURES,
    max_accessory_targets=MAX_ACCESSORY_TARGETS)


def build_prompt(products: list) -> str:
    payload = [
        {
            "product_key": p["product_key"],
            "title": p.get("name"),
            # Truncated: a single record can be kilobytes of prose that teaches
            # the model nothing about the product's attributes.
            "description": (p.get("description") or "")[:DESCRIPTION_CHARS],
            "brand": p.get("brand"),
            "category": " > ".join(p.get("taxonomy_path") or []),
            "known_attributes": [
                {"key": a["key"], "value": a["value"]}
                for a in (p.get("attributes") or [])
            ],
        }
        for p in products
    ]
    return json.dumps({"products": payload}, default=str)


def validate_one(raw: dict) -> dict:
    out = {}
    if not isinstance(raw, dict):
        return out

    for field in STRING_FIELDS:
        value = raw.get(field)
        if isinstance(value, str) and value.strip():
            out[field] = value.strip().lower()

    for field, allowed in CLOSED_SETS.items():
        value = raw.get(field)
        if isinstance(value, str) and value.strip().lower() in allowed:
            out[field] = value.strip().lower()

    accessory = raw.get("is_accessory")
    if isinstance(accessory, bool):
        out["is_accessory"] = accessory

    features = raw.get("key_features")
    if isinstance(features, list) and all(isinstance(f, str) for f in features):
        cleaned = [f.strip() for f in features if f.strip()]
        if cleaned:
            out["key_features"] = cleaned[:MAX_KEY_FEATURES]

    # Only meaningful on an accessory. Kept off everything else so a false
    # positive cannot widen what a non-accessory is allowed to complement.
    targets = raw.get("accessory_for")
    if out.get("is_accessory") and isinstance(targets, list) and \
            all(isinstance(t, str) for t in targets):
        cleaned = [t.strip().lower() for t in targets if t.strip()]
        if cleaned:
            out["accessory_for"] = cleaned[:MAX_ACCESSORY_TARGETS]

    return out


def _call_llm(prompt: str) -> dict:
    response = client.chat.completions.create(
        model=settings.LLM_MODEL,
        response_format={"type": "json_object"},
        messages=[{"role": "system", "content": _SYSTEM},
                  {"role": "user", "content": prompt}],
    )
    return json.loads(response.choices[0].message.content)


def extract_batch(products: list) -> dict:
    """Returns {product_key: attributes}. An empty dict on any failure, so one
    bad batch degrades to "not extracted" rather than aborting a catalog."""
    if not products:
        return {}

    try:
        raw = _call_llm(build_prompt(products))
    except Exception:
        logger.error("Attribute extraction failed for a batch of %d",
                     len(products), exc_info=True)
        return {}

    if not isinstance(raw, dict) or not isinstance(raw.get("products"), list):
        logger.warning("Attribute extraction returned an unexpected shape")
        return {}

    asked = {p["product_key"] for p in products}
    out = {}
    for item in raw["products"]:
        if not isinstance(item, dict):
            continue
        key = item.get("product_key")
        # A model that invents a product must not create a row for it.
        if key not in asked:
            continue
        validated = validate_one(item)
        if validated:
            out[key] = validated
    return out
