"""Scoring and feedback logic for Superpitch."""

from __future__ import annotations

import json
import os
import pickle
import re
from datetime import datetime

import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

from model_a import predict_ridge, transform_model_a_inputs
from policy import (
    APPROVE_THRESHOLD,
    CONFLICT_THRESHOLD,
    CORRECTION_REASON_OPTIONS,
    LOW_SUPPORT_QUALITY,
    decide_action,
    default_case_context,
    feedback_priority,
    normalize_correction_reason,
    recommend_review,
)
from prepare import (
    FEATURE_NAMES,
    _RE_ELITE_EDU,
    _RE_ELITE_EMPLOYER,
    _RE_PRIOR_EXIT,
    build_tfidf_index,
    embed_texts,
    example_retrieval_weight,
    extract_features,
    is_generic_reason,
    load_examples,
    normalize_example_source,
    pitch_to_text,
)
from superpitch.app_paths import CONTRIB_FILE, FEEDBACK_FILE, LATEST_MODEL_FILE, LEARNED_MODEL_FILE, TRAIN_FILE


def detect_moonshot(text: str, inp: dict) -> tuple:
    t = text.lower()
    n_red = len(inp.get("red_flags", []))

    elite = bool(_RE_ELITE_EMPLOYER.search(t) or _RE_ELITE_EDU.search(t) or _RE_PRIOR_EXIT.search(t))

    pre_rev = bool(re.search(r"\$0\s*revenue|pre-revenue|no\s*revenue", t))
    large_market = bool(re.search(r"(\$\d+[\d,.]*\s*[bt]\b|\d+[\d,.]*\s*(billion|trillion))", t))
    metric_density = len(re.findall(r"\b\d[\d,]*\.?\d*\s*(%|k|m|b|x)?", t)) / max(len(t.split()), 1)

    is_moonshot = elite and pre_rev and large_market and n_red <= 1
    return is_moonshot, round(metric_density, 3)


def load_model():
    if not os.path.exists(LATEST_MODEL_FILE):
        print("No local model found. Run `superpitch-train` first.")
        raise SystemExit(1)
    with open(LATEST_MODEL_FILE, "rb") as f:
        bundle = pickle.load(f)
    upgrade_pending = False
    try:
        import json
        from superpitch.app_paths import CONFIG_FILE
        if CONFIG_FILE.exists():
            upgrade_pending = bool(json.loads(CONFIG_FILE.read_text()).get("retrain_after_upgrade"))
    except Exception:
        pass

    if not upgrade_pending and os.path.exists(LEARNED_MODEL_FILE):
        try:
            with open(LEARNED_MODEL_FILE, "rb") as f:
                learned = pickle.load(f)
            bundle["model_b"] = learned.get("model")
            bundle["model_b_backend"] = learned.get("embedding_backend")
        except Exception:
            pass
    return bundle


def load_training_data():
    best_examples = {}
    for path in [TRAIN_FILE, CONTRIB_FILE, FEEDBACK_FILE]:
        if not os.path.exists(path):
            continue
        for ex in load_examples(path):
            if not ex.get("key_reason"):
                continue
            key = ex.get("input", {}).get("one_liner", ex.get("text", "")[:80]).strip().lower()
            if not key:
                continue
            current = best_examples.get(key)
            if current is None or example_retrieval_weight(ex) > example_retrieval_weight(current):
                best_examples[key] = ex
    return list(best_examples.values())


def build_retrieval_index(examples):
    if not examples:
        return None
    texts = []
    for ex in examples:
        texts.append(pitch_to_text(ex["input"]) if "input" in ex else ex.get("text", ""))
    try:
        return {"kind": "embedding", "vectors": embed_texts(texts)}
    except Exception:
        tfidf_index = build_tfidf_index(texts, min_df=2, max_features=6000)
        if tfidf_index is None:
            return {"kind": "empty"}
        return {"kind": "tfidf", **tfidf_index}


def find_similar(query_vec, index_data, examples, query_text="", top_k=3):
    if index_data is None or index_data["kind"] == "empty":
        return []
    if index_data["kind"] == "embedding":
        if query_vec is None:
            return []
        sims = cosine_similarity(query_vec, index_data["vectors"])[0]
    else:
        if not query_text.strip():
            return []
        query_sparse = index_data["vectorizer"].transform([query_text])
        sims = cosine_similarity(query_sparse, index_data["matrix"])[0]

    support_weights = np.array([example_retrieval_weight(ex) for ex in examples], dtype=np.float32)
    effective = sims * support_weights
    ordered_idx = effective.argsort()[::-1]
    results = []
    for idx in ordered_idx:
        if sims[idx] < 0.2:
            continue
        ex = examples[idx]
        reason = ex.get("key_reason", "").strip()
        if is_generic_reason(reason):
            reason = ex.get("input", {}).get("one_liner", "") or ex.get("text", "")[:160]
        if not reason:
            continue
        results.append({
            "decision": ex["decision"].upper(),
            "score": ex.get("overall_score", ex.get("user_score", "?")),
            "reason": reason,
            "sim": float(sims[idx]),
            "source": normalize_example_source(ex),
            "support_weight": float(support_weights[idx]),
        })
        if len(results) == top_k:
            break
    return results


def describe_feature_signal(name: str, raw_value: float):
    if raw_value == 0:
        return None
    labels = {
        "has_mrr_arr": "revenue metric present",
        "has_customers": "customer traction",
        "has_growth_pct": "growth metric present",
        "has_retention": "retention metric present",
        "pre_revenue": "pre-revenue",
        "elite_employer": "elite company background",
        "elite_edu": "elite academic background",
        "prior_exit": "founder has prior exit",
        "solo_founder": "solo founder",
        "part_time_team": "part-time team risk",
        "regulatory": "regulatory exposure",
        "niche_market": "niche market",
        "commoditized": "commoditized market",
        "is_profitable": "already profitable",
        "weak_moat": "weak moat",
        "point_solution": "point-solution risk",
        "pre_seed": "pre-seed stage",
        "seed": "seed stage",
        "series_a": "Series A stage",
        "text_length": "detailed pitch",
    }
    if name == "mrr_value":
        return f"${raw_value:.0f}k MRR"
    if name == "nrr_value":
        return f"{raw_value:.0f}% NRR"
    if name == "mom_growth_pct":
        return f"{raw_value:.0f}% MoM growth"
    if name == "raise_amount_k":
        return f"raising ${np.expm1(raw_value):.0f}k"
    if name == "rev_per_customer":
        return f"${raw_value:.1f}k revenue per customer"
    if name == "pre_rev_red_flags":
        return "pre-revenue with multiple red flags"
    return labels.get(name)


def summarize_model_signals(bundle, feat, x_n):
    coefs = getattr(bundle["model"], "coef_", None)
    names = bundle.get("feature_names", FEATURE_NAMES)
    if coefs is None:
        return {"positive": [], "negative": []}

    contributions = []
    base_limit = min(len(names), len(coefs), feat.shape[1], x_n.shape[1])
    for i in range(base_limit):
        raw_value = float(feat[0, i])
        label = describe_feature_signal(names[i], raw_value)
        if not label:
            continue
        contrib = float(coefs[i] * x_n[0, i])
        if abs(contrib) < 0.03:
            continue
        contributions.append((contrib, label))

    positive = []
    negative = []
    seen = set()
    for contrib, label in sorted(contributions, key=lambda item: item[0], reverse=True):
        if contrib > 0 and label not in seen:
            positive.append(label)
            seen.add(label)
        if len(positive) == 2:
            break
    for contrib, label in sorted(contributions, key=lambda item: item[0]):
        if contrib < 0 and label not in seen:
            negative.append(label)
            seen.add(label)
        if len(negative) == 2:
            break
    return {"positive": positive, "negative": negative}


def summarize_outlier_signals(bundle, feat, x_n):
    names = bundle.get("feature_names", FEATURE_NAMES)
    base_limit = min(bundle.get("base_feature_count", len(names)), len(names), feat.shape[1], x_n.shape[1])
    unusual = []
    for i in range(base_limit):
        z_score = float(x_n[0, i])
        if abs(z_score) < 2.5:
            continue
        raw_value = float(feat[0, i])
        label = describe_feature_signal(names[i], raw_value) or names[i].replace("_", " ")
        unusual.append((abs(z_score), label))

    unusual.sort(reverse=True)
    return {"outlier_count": len(unusual), "unusual_signals": [label for _, label in unusual[:2]]}


def assess_confidence(pred_score, similar, diagnostics, case_context, is_moonshot=False, specificity=0.0):
    margin = abs(pred_score - APPROVE_THRESHOLD)
    predicted_decision = "APPROVE" if pred_score >= APPROVE_THRESHOLD else "REJECT"
    avg_sim = float(np.mean([s["sim"] for s in similar])) if similar else 0.0
    support_quality = float(np.mean([s["support_weight"] for s in similar])) if similar else 0.0
    agreement = None
    if similar:
        agreement = sum(1 for s in similar if s["decision"] == predicted_decision) / len(similar)

    score = 0
    notes = []
    if is_moonshot:
        score -= 3
        notes.append("moonshot")
    if margin >= 1.2:
        score += 1
    elif margin < 0.6:
        score -= 1
        notes.append("close to threshold")
    if specificity >= 0.08:
        score += 1
    elif specificity < 0.04:
        score -= 1
        notes.append("low evidence")
    if diagnostics["outlier_count"] >= 2:
        score -= 1
        notes.append("unusual vs training set")
    if case_context["conflict_flag"] >= CONFLICT_THRESHOLD:
        score -= 1
        notes.append("memory conflict")
    if similar and support_quality < LOW_SUPPORT_QUALITY:
        score -= 1
        notes.append("weak precedents")
    if agreement is not None and avg_sim >= 0.45 and agreement < 0.67:
        score -= 1
        notes.append("precedents disagree")

    if score >= 2:
        level = "high"
    elif score <= -1:
        level = "low"
    else:
        level = "medium"

    extra_flag = level == "low" or diagnostics["outlier_count"] >= 2
    needs_review, _ = recommend_review(pred_score, case_context=case_context, needs_review=extra_flag, is_moonshot=is_moonshot)
    if not notes:
        notes.append("solid call" if level != "low" else "borderline")
    return {"level": level, "needs_review": needs_review, "notes": notes[:2]}


def score_text(bundle, text, inp=None, need_embedding=False):
    if inp is None:
        inp = {}
    is_moonshot, specificity = detect_moonshot(text, inp)
    if bundle.get("use_stacking"):
        feat, _case_row, case_context, x_n, sub = transform_model_a_inputs(bundle, text, inp)
    else:
        feat = extract_features(text, inp).reshape(1, -1)
        x_n = bundle["scaler"].transform(feat)
        case_context = default_case_context()
        sub = {name: 5.0 for name in ["team", "traction", "market", "risk"]}

    pred_score = float(np.clip(predict_ridge(bundle["model"], x_n)[0], 1, 10))

    model_b = bundle.get("model_b")
    if model_b is not None:
        try:
            from prepare import embedding_backend_available, preferred_embedding_backend
            if embedding_backend_available():
                current_backend = preferred_embedding_backend()
                trained_backend = bundle.get("model_b_backend")
                if not trained_backend or current_backend == trained_backend:
                    emb = embed_texts([text]).astype(np.float32)
                    pred_b = float(np.clip(model_b.predict(emb)[0], 1, 10))
                    pred_score = round(0.7 * pred_score + 0.3 * pred_b, 4)
        except Exception:
            pass

    signals = summarize_model_signals(bundle, feat, x_n)
    diagnostics = summarize_outlier_signals(bundle, feat, x_n)
    emb_vec = None
    if need_embedding:
        try:
            emb_vec = embed_texts([text]).astype(np.float32)
        except Exception:
            emb_vec = None
    return emb_vec, pred_score, sub, is_moonshot, specificity, signals, diagnostics, case_context


def evaluate_pitch(bundle, text, index_vecs=None, examples=None, inp=None):
    need_embedding = index_vecs is not None and index_vecs["kind"] == "embedding"
    emb_vec, pred_score, sub, is_moonshot, specificity, signals, diagnostics, case_context = score_text(
        bundle,
        text,
        inp=inp,
        need_embedding=need_embedding,
    )
    similar = find_similar(emb_vec, index_vecs, examples or [], text) if index_vecs is not None else []
    confidence = assess_confidence(pred_score, similar, diagnostics, case_context, is_moonshot, specificity)
    final_action, review_reasons = decide_action(
        pred_score,
        case_context=case_context,
        confidence_level=confidence["level"],
        needs_review=confidence["needs_review"],
        is_moonshot=is_moonshot,
    )
    return {
        "pred_score": pred_score,
        "sub": sub,
        "similar": similar,
        "confidence": confidence,
        "signals": signals,
        "diagnostics": diagnostics,
        "final_action": final_action,
        "review_reasons": review_reasons,
        "is_moonshot": is_moonshot,
        "specificity": specificity,
    }


def collect_feedback(text, pred_score, final_action, inp=None, confidence=None, signals=None, diagnostics=None, is_moonshot=False):
    priority = feedback_priority(
        pred_score,
        confidence_level=confidence["level"] if confidence else None,
        needs_review=confidence["needs_review"] if confidence else False,
        is_moonshot=is_moonshot,
    )
    print()
    if priority["level"] == "low":
        print("  Feedback optional. Type 'y' to correct, or press enter to continue: ", end="")
        if input().strip().lower() not in {"y", "yes"}:
            return None
    else:
        print(f"  Feedback requested ({priority['level']})")

    print(f"  Your score 1-10 (model said {pred_score:.1f}): ", end="")
    score_raw = input().strip()
    if not score_raw:
        return None
    try:
        user_score = int(score_raw)
        if not 1 <= user_score <= 10:
            print("  Score must be 1-10. Skipped.")
            return None
    except ValueError:
        print("  Not a number. Skipped.")
        return None

    decision = "approve" if user_score >= APPROVE_THRESHOLD else "reject"
    print(f"  One-line reason (why {decision}?): ", end="")
    reason = input().strip()
    options = "/".join(CORRECTION_REASON_OPTIONS)
    print(f"  Primary correction reason [{options}] (enter for other): ", end="")
    reason_category = normalize_correction_reason(input().strip() or "other")

    feedback = {
        "text": text,
        "input": inp if inp else {"one_liner": text[:200]},
        "user_score": user_score,
        "overall_score": user_score,
        "decision": decision,
        "key_reason": reason if reason else f"User scored {user_score}/10",
        "feedback_reason_category": reason_category,
        "feedback_priority": priority["level"],
        "model_score": round(pred_score, 1),
        "model_action": final_action.lower(),
        "model_confidence": confidence["level"] if confidence else None,
        "model_signals": signals if signals else None,
        "model_unusual_signals": diagnostics["unusual_signals"] if diagnostics else None,
        "timestamp": datetime.now().isoformat(),
        "source": "feedback",
    }

    FEEDBACK_FILE.parent.mkdir(parents=True, exist_ok=True)
    with open(FEEDBACK_FILE, "a") as f:
        f.write(json.dumps(feedback) + "\n")
    print("  Feedback saved.")
    return feedback
