"""
aca/ai_engine/services/attribute_weighter.py

Attribute Weighting
───────────────────
Re-ranks product results so that user-stated priorities (e.g. "durable",
"cheap", "Nike") and learned preferences outrank surface-level attributes
like colour or name alphabetics.

Integration point:
    Call `rank_products(products, enriched_slots, user_context)` inside
    ConversationManager / QueryBuilder after fetching the raw product list,
    before returning results to the chat response.

Input:
    products        — list of product dicts from the store/search layer
    enriched_slots  — output of intent_layerer.enrich_query()
    user_context    — output of feedback_learner.get_user_context_summary()

Output:
    Re-ordered list of product dicts, each with a `_score` field for debugging.
"""

import logging
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)


# ─────────────────────────────────────────────────────────────────────────────
# Attribute keyword extraction from query slots
# ─────────────────────────────────────────────────────────────────────────────

# Maps a slot key → attribute label → how to detect it in the product
SLOT_TO_ATTRIBUTE = {
    "max_price":  "price",
    "min_price":  "price",
    "brand":      "brand",
    "color":      "color",
    "category":   "category",
    "size":       "size",
    "purpose":    "purpose",
    "season":     "season",
    "formality":  "formality",
    "gender":     "gender",
    "material":   "material",
}

# Base weights for attributes when explicitly mentioned in the query
EXPLICIT_MENTION_WEIGHT = 0.6

# Words in user text that signal high-priority attributes
PRIORITY_SIGNALS = {
    "price":      ["cheap", "budget", "affordable", "expensive", "price", "cost", "inexpensive"],
    "brand":      ["brand", "branded", "authentic", "genuine", "original"],
    "durable":    ["durable", "durability", "last", "quality", "sturdy", "tough", "long-lasting"],
    "waterproof": ["waterproof", "water-resistant", "rain", "wet"],
    "lightweight":["lightweight", "light", "weight", "portable"],
    "comfort":    ["comfortable", "comfort", "soft", "cozy", "cushion"],
    "style":      ["stylish", "trendy", "fashion", "design", "aesthetic", "look"],
    "size":       ["size", "fit", "fitting", "fits"],
}


# ─────────────────────────────────────────────────────────────────────────────
# Scoring helpers
# ─────────────────────────────────────────────────────────────────────────────

def _extract_explicit_weights(
    enriched_slots: Dict[str, Any],
    raw_text: str = "",
) -> Dict[str, float]:
    """
    Build a weight dict from: explicit slot mentions + signal words in raw text.
    """
    weights: Dict[str, float] = {}
    text_lower = (raw_text or "").lower()

    # Any slot that was explicitly set gets its attribute boosted
    for slot_key, attr in SLOT_TO_ATTRIBUTE.items():
        if enriched_slots.get(slot_key):
            weights[attr] = max(weights.get(attr, 0.0), EXPLICIT_MENTION_WEIGHT)

    # Priority signal words in the user's text
    for attr, keywords in PRIORITY_SIGNALS.items():
        for kw in keywords:
            if kw in text_lower:
                weights[attr] = min(1.0, weights.get(attr, 0.0) + 0.25)
                break

    # Intent-derived boosts (from intent_layerer)
    for attr, boost in (enriched_slots.get("_attribute_weight_boosts") or {}).items():
        weights[attr] = min(1.0, weights.get(attr, 0.0) + boost)

    return weights


def _score_product(
    product: dict,
    weights: Dict[str, float],
    user_context: Dict[str, Any],
) -> float:
    """
    Compute a relevance score for a single product.
    Higher is better.
    """
    score = 0.0

    preferred_brands = {b.lower() for b in (user_context.get("preferred_brands") or [])}
    preferred_cats   = {c.lower() for c in (user_context.get("preferred_categories") or [])}
    preferred_stores = {s.lower() for s in (user_context.get("preferred_stores") or [])}
    learned_weights  = user_context.get("attribute_weights") or {}

    # ── Brand match ──────────────────────────────────────────────────
    product_brand = (product.get("brand") or "").lower()
    if product_brand and product_brand in preferred_brands:
        score += 0.30
    brand_weight = max(weights.get("brand", 0.0), learned_weights.get("brand", 0.0))
    if product_brand and brand_weight:
        score += brand_weight * 0.20

    # ── Category match ────────────────────────────────────────────────
    product_cat = (product.get("category") or "").lower()
    if product_cat and product_cat in preferred_cats:
        score += 0.15
    cat_weight = max(weights.get("category", 0.0), learned_weights.get("category", 0.0))
    if product_cat and cat_weight:
        score += cat_weight * 0.10

    # ── Store / merchant match ────────────────────────────────────────
    product_store = (product.get("store_slug") or "").lower()
    if product_store and product_store in preferred_stores:
        score += 0.10

    # ── Price alignment ───────────────────────────────────────────────
    price_weight = max(weights.get("price", 0.0), learned_weights.get("price", 0.0))
    if price_weight:
        # Products are already filtered by budget; reward cheaper items
        # when price was flagged as important
        product_price = product.get("price") or 0
        if product_price and product_price > 0:
            # Normalise: prefer lower prices when price-sensitive
            score += price_weight * (1.0 / (1.0 + product_price / 1000))

    # ── Attribute tag matching ────────────────────────────────────────
    # Products can carry an `attributes` list like ["waterproof", "lightweight"]
    product_attrs = {a.lower() for a in (product.get("attributes") or [])}
    product_tags  = {t.lower() for t in (product.get("tags") or [])}
    product_desc  = (product.get("description") or "").lower()
    all_product_signals = product_attrs | product_tags

    for attr, weight in weights.items():
        if attr in all_product_signals or attr in product_desc:
            score += weight * 0.35

    # ── Recent positive interaction bonus ─────────────────────────────
    positive_names = {
        p.get("name", "").lower()
        for p in (user_context.get("recent_positive_interactions") or [])
    }
    negative_names = {
        p.get("name", "").lower()
        for p in (user_context.get("recent_negative_interactions") or [])
    }
    product_name_lower = (product.get("name") or "").lower()

    if product_name_lower in positive_names:
        score += 0.20
    if product_name_lower in negative_names:
        score -= 0.30  # penalise previously dismissed products

    return round(score, 4)


# ─────────────────────────────────────────────────────────────────────────────
# Public API
# ─────────────────────────────────────────────────────────────────────────────

def rank_products(
    products: List[dict],
    enriched_slots: dict,
    user_context: Optional[dict] = None,
    raw_text: str = "",
) -> List[dict]:
    """
    Re-ranks a list of product dicts based on:
      - Explicitly stated user priorities (price, brand, attributes)
      - Intent-inferred attribute boosts from intent_layerer
      - Learned preference signals from feedback_learner

    Returns the sorted list (best match first), each product dict gets a
    `_score` field (float) that can be logged or surfaced for debugging.
    """
    if not products:
        return products

    ctx = user_context or {}
    weights = _extract_explicit_weights(enriched_slots, raw_text)

    if weights:
        logger.info("Attribute weighter using weights: %s", weights)

    scored = []
    for product in products:
        s = _score_product(product, weights, ctx)
        scored.append({**product, "_score": s})

    scored.sort(key=lambda p: p["_score"], reverse=True)
    logger.debug("Top product after re-ranking: %s (score %.4f)", scored[0].get("name"), scored[0]["_score"])

    return scored


def explain_ranking(product: dict, enriched_slots: dict, user_context: Optional[dict] = None) -> str:
    """
    Returns a human-readable explanation of why a product ranked where it did.
    Useful for debugging or surfacing "Why we recommended this" UI.
    """
    ctx = user_context or {}
    weights = _extract_explicit_weights(enriched_slots)
    score = _score_product(product, weights, ctx)
    reasons = []

    preferred_brands = {b.lower() for b in (ctx.get("preferred_brands") or [])}
    brand = (product.get("brand") or "").lower()
    if brand and brand in preferred_brands:
        reasons.append(f"matches your preferred brand ({product['brand']})")

    preferred_cats = {c.lower() for c in (ctx.get("preferred_categories") or [])}
    cat = (product.get("category") or "").lower()
    if cat and cat in preferred_cats:
        reasons.append(f"fits a category you shop often ({product['category']})")

    if weights.get("price", 0) > 0.4:
        reasons.append("priced competitively based on your budget preference")

    product_attrs = {a.lower() for a in (product.get("attributes") or [])}
    matched = [attr for attr in weights if attr in product_attrs]
    if matched:
        reasons.append(f"has attributes you prioritised: {', '.join(matched)}")

    if not reasons:
        reasons.append("good general match for your query")

    return f"Score {score:.2f}: " + "; ".join(reasons) + "."