"""
Quality scoring for Toxo layers.

Provides:
- Expert-weighted scoring as default for TOXO layers
- Dynamic weight switching based on query classifier
- Utility metric for actionable answers (steps, numbers, recommendations)
- Conciseness weight low but non-zero in expert modes
- Domain-agnostic: works with coffee, linkedin, finance, etc.
"""

import os
import re
from typing import Dict, Any, Optional, Tuple
from dataclasses import dataclass

# Query type from classifier
QUERY_TYPES = ("factual", "procedural", "comparison", "vague", "out_of_domain", "technical", "unknown")

# Domain-specific terms for classify_query and analyze_response
DOMAIN_TERMS = {
    "coffee": ["coffee", "pour", "brew", "espresso", "ratio", "tds", "grind", "extraction", "roast", "water", "gram", "temperature", "yield"],
    "linkedin": ["linkedin", "post", "career", "networking", "leadership", "professional", "engagement", "viral", "content", "profile", "connection"],
    "linkedin_content_creation": ["linkedin", "post", "career", "networking", "leadership", "professional", "engagement", "viral", "content", "profile", "connection"],
    "finance": ["finance", "investment", "portfolio", "risk", "return", "stock", "market", "asset", "revenue", "profit"],
    "general": [],
}

# Weight presets: conciseness kept low but non-zero in expert modes
QUALITY_WEIGHTS = {
    "domain_expert": {
        "relevance": 1.0,
        "completeness": 1.0,
        "coherence": 1.0,
        "conciseness": 0.15,
        "domain_fit": 1.2,
        "actionability": 1.0,
    },
    "generalist": {
        "relevance": 1.0,
        "completeness": 1.0,
        "coherence": 1.0,
        "conciseness": 1.0,
        "domain_fit": 0.8,
        "actionability": 0.5,
    },
    "procedural": {
        "relevance": 1.0,
        "completeness": 1.2,
        "coherence": 1.0,
        "conciseness": 0.15,
        "domain_fit": 1.0,
        "actionability": 1.5,
    },
    "procedural_short": {
        "relevance": 1.0,
        "completeness": 1.0,
        "coherence": 1.0,
        "conciseness": 0.5,
        "domain_fit": 1.0,
        "actionability": 1.2,
    },
    "comparison": {
        "relevance": 1.2,
        "completeness": 1.0,
        "coherence": 1.0,
        "conciseness": 0.25,
        "domain_fit": 1.2,
        "actionability": 1.0,
    },
    "comparison_compact": {
        "domain_fit": 1.2,
        "relevance": 0.8,
        "completeness": 0.8,
        "coherence": 0.6,
        "conciseness": 0.8,
        "actionability": 0.4,
    },
    "factual": {
        "relevance": 1.2,
        "completeness": 1.0,
        "coherence": 1.0,
        "conciseness": 0.3,
        "domain_fit": 1.2,
        "actionability": 0.8,
    },
}


def _get_domain_terms(domain: str) -> list:
    """Get domain terms for scoring. Fallback: extract from domain string."""
    if not domain or domain == "general":
        return []
    terms = DOMAIN_TERMS.get(domain.lower())
    if terms:
        return terms
    # Fallback: use domain words
    return [w for w in domain.lower().replace("_", " ").split() if len(w) > 3]


def compute_domain_confidence(query: str, domain: str = "general") -> float:
    """
    Domain confidence 0-1: likelihood query is in-domain.
    For cost-aware routing: if < 0.6 → use Direct; if >= 0.6 → use Layer.
    """
    q = query.strip().lower()
    if not domain or domain == "general":
        return 0.5
    terms = _get_domain_terms(domain)
    if not terms:
        return 0.5
    hits = sum(1 for t in terms if t in q)
    if hits >= 2:
        return 0.9
    if hits >= 1:
        return 0.75
    generic = any(w in q for w in ["what is", "capital", "who is", "where is", "who wrote"])
    if generic and len(q) > 20:
        return 0.3
    return 0.5


def should_use_layer(query: str, domain: str = "general", threshold: float = 0.6) -> Tuple[bool, float]:
    """
    Cost-aware routing: use Layer when domain confidence >= threshold, else Direct.
    Returns: (use_layer, domain_confidence)
    """
    conf = compute_domain_confidence(query, domain)
    return (conf >= threshold, conf)


def classify_query(query: str, domain: str = "general") -> str:
    """
    Classify query type for dynamic weight selection.
    Returns: factual | procedural | comparison | vague | out_of_domain | technical | unknown
    """
    q = query.strip().lower()
    terms = _get_domain_terms(domain)

    # Out-of-domain: no domain terms and generic question
    if domain and domain != "general" and terms:
        if not any(t in q for t in terms) and len(q) > 15:
            generic = any(w in q for w in ["what is", "capital", "who is", "where is", "who wrote"])
            if generic:
                return "out_of_domain"

    # Procedural: how-to, steps, guide
    procedural = (
        "how to" in q or "how do" in q or "step by step" in q or "step-by-step" in q
        or "guide" in q or "process" in q or "dial in" in q or "walk me" in q
    )
    if procedural:
        return "procedural"

    # Comparison: compare, vs, difference
    comparison = (
        "compare" in q or " vs " in q or " versus " in q or "difference between" in q
        or ("both" in q and "each" in q)
    )
    if comparison:
        return "comparison"

    # Vague: short, "best one", "which"
    if len(q) < 25 and ("best" in q or "which" in q or "what's the best" in q):
        return "vague"

    # Technical: ratio, TDS, %, target, yield, metrics
    technical = any(t in q for t in ["ratio", "tds", "%", "target", "yield", "metric", "extraction", "benchmark"])
    if technical:
        return "technical"

    # Factual: what is, define, ideal
    factual = any(w in q for w in ["what is", "what's the", "what is the", "define", "ideal", "ratio"])
    if factual:
        return "factual"

    return "unknown"


def get_weights_for_query(
    query: str,
    query_type: Optional[str] = None,
    scoring_mode: Optional[str] = None,
    domain: str = "general",
    use_dynamic: bool = True,
) -> Tuple[str, Dict[str, float]]:
    """Get quality weights for a query. Returns (effective_mode, weights_dict)."""
    mode = scoring_mode or os.getenv("TOXO_SCORING_MODE", "domain_expert")
    if not use_dynamic or mode == "generalist":
        weights = QUALITY_WEIGHTS.get(mode, QUALITY_WEIGHTS["domain_expert"]).copy()
        return mode, weights

    qtype = query_type or classify_query(query, domain)
    q_words = len(query.strip().split())
    type_to_preset = {
        "procedural": "procedural",
        "comparison": "comparison_compact",
        "factual": "factual",
        "vague": "domain_expert",
        "out_of_domain": "generalist",
        "technical": "domain_expert",
        "unknown": "domain_expert",
    }
    preset = type_to_preset.get(qtype, "domain_expert")
    if preset == "procedural" and q_words < 8:
        preset = "procedural_short"
    weights = QUALITY_WEIGHTS.get(preset, QUALITY_WEIGHTS["domain_expert"]).copy()
    effective = f"{mode}+{preset}" if preset != mode else mode
    return effective, weights


def compute_actionability(response: str, query: str, domain: str = "general") -> float:
    """How actionable is the response? Scores steps, numbers, recommendations."""
    if not response or len(response) < 20:
        return 0.0
    text = response.lower()
    score = 0.0
    if re.search(r"\b(1\.|2\.|3\.|step\s*\d|first|second|third)\b", text, re.I):
        score += 0.35
    if re.search(r"\d+[:\-%°]|\d+\s*(gram|g|ml|oz|°f|°c|percent|%)", text, re.I):
        score += 0.3
    action = ["use", "start", "adjust", "target", "try", "apply", "create", "write", "post"]
    if sum(1 for a in action if a in text) >= 2:
        score += 0.2
    if any(c in response for c in ["•", "* ", "- ", "**"]):
        score += 0.15
    return min(1.0, score)


def compute_info_density(response: str, query: str, domain: str = "general") -> float:
    """Useful info per token: penalizes verbosity inflation. Returns 0-1."""
    if not response or len(response) < 20:
        return 0.0
    text = response.lower()
    length = max(len(response), 1)
    actionability = compute_actionability(response, query, domain)
    terms = _get_domain_terms(domain)
    domain_hits = sum(1 for t in terms if t in text) if terms else 3
    domain_score = min(1.0, domain_hits * 0.2)
    numeric_count = len(re.findall(r"\d+[:\-%°]|\d+\s*(gram|g|ml|oz|°f|°c|percent|%)", text, re.I))
    numeric_score = min(1.0, numeric_count * 0.25)
    info_sum = actionability + domain_score + numeric_score
    raw_density = info_sum / max(1, length / 150)
    return round(min(1.0, raw_density * 0.6), 2)


def analyze_response(
    query: str,
    response: str,
    source: str = "",
    scoring_mode: Optional[str] = None,
    domain: str = "general",
    use_dynamic_weights: bool = True,
) -> Dict[str, Any]:
    """Analyze response quality with dynamic weight switching."""
    if not response:
        return {"error": "Empty response", "scores": {}, "verdict": "N/A"}

    text = response.strip().lower()
    q_lower = query.lower()
    terms = _get_domain_terms(domain)

    # Relevance
    q_words = set(w for w in q_lower.split() if len(w) > 2)
    resp_words = set(w for w in text.split() if len(w) > 2)
    overlap = len(q_words & resp_words) / max(len(q_words), 1)
    relevance = min(1.0, 0.3 + overlap * 0.7)

    # Completeness
    ends_abruptly = response.rstrip().endswith("…") or "..." in response[-20:]
    has_content = len(response) > 30
    completeness = 0.9 if has_content and not ends_abruptly else (0.5 if has_content else 0.2)

    # Coherence
    has_structure = any(c in response for c in ["•", "*", "-", "1.", "2.", ". ", ":"])
    sentences = len([s for s in response.split(". ") if len(s) > 5])
    coherence = min(1.0, 0.5 + (0.2 if has_structure else 0) + min(0.3, sentences * 0.05))

    # Conciseness
    target_len = 80 + len(query) * 3
    if len(response) <= target_len * 1.5 and has_content:
        conciseness = 1.0
    elif len(response) <= target_len * 3:
        conciseness = 0.8
    elif len(response) > 1500:
        conciseness = 0.4
    else:
        conciseness = 0.6

    # Domain fit
    in_domain_query = any(t in q_lower for t in terms) if terms else True
    domain_hits = sum(1 for t in terms if t in text) if terms else 3
    domain_fit = min(1.0, domain_hits * 0.2) if in_domain_query else 0.7

    actionability = compute_actionability(response, query, domain)

    scores = {
        "relevance": round(relevance, 2),
        "completeness": round(completeness, 2),
        "coherence": round(coherence, 2),
        "conciseness": round(conciseness, 2),
        "domain_fit": round(domain_fit, 2),
        "actionability": round(actionability, 2),
    }

    qtype = classify_query(query, domain)
    effective_mode, weights = get_weights_for_query(query, qtype, scoring_mode, domain, use_dynamic_weights)

    weighted_sum = sum(scores.get(k, 0) * weights.get(k, 1.0) for k in scores)
    weight_total = sum(weights.get(k, 1.0) for k in scores)
    base_overall = weighted_sum / weight_total if weight_total else sum(scores.values()) / len(scores)

    info_density = compute_info_density(response, query, domain)
    overall = 0.9 * base_overall + 0.1 * info_density
    verdict = "strong" if overall >= 0.8 else "good" if overall >= 0.6 else "weak"

    scores["info_density"] = info_density

    return {
        "scores": scores,
        "overall": round(overall, 2),
        "verdict": verdict,
        "scoring_mode": effective_mode,
        "query_type": qtype,
        "notes": _quality_notes(scores, response, query),
    }


def _quality_notes(scores: dict, response: str, query: str) -> list:
    notes = []
    if scores.get("completeness", 1) < 0.7:
        notes.append("Response may be truncated")
    if scores.get("conciseness", 1) < 0.5:
        notes.append("Verbose")
    if scores.get("relevance", 1) < 0.6:
        notes.append("May not fully address query")
    if scores.get("domain_fit", 1) >= 0.8:
        notes.append("Domain-specific content")
    if scores.get("actionability", 0) >= 0.7:
        notes.append("Actionable")
    return notes
