"""AI CV Ranking (RANK module) — scores candidates against a JD via the LLM layer.

Uses apps.llm.service.call_llm with the default configured provider. Produces an
overall score, per-parameter scores, and a human-readable rationale per candidate.
"""
import json
import re

from apps.llm.service import call_llm
from apps.llm.models import LLMProvider
from .models import JobApplication, RankRun, RankScore, RankParameter

# Hardcoded fallback used only if no RankParameter rows exist (RANK-003 makes these editable).
DEFAULT_WEIGHTS = {
    "skills": 40,
    "experience": 25,
    "qualifications": 15,
    "role_fit": 20,
}
_DEFAULT_DESC = {
    "skills": "How well the candidate's skills match the JD must-have/good-to-have skills.",
    "experience": "Relevance and years of experience vs the JD's experience band.",
    "qualifications": "Education/qualifications vs the JD requirement.",
    "role_fit": "Overall fit including current role, location, and domain.",
}


def active_parameters():
    """Return [(key, label, description, default_weight)] from the config, or the fallback."""
    rows = list(RankParameter.objects.filter(is_active=True).order_by("sort_order", "id"))
    if rows:
        return [(r.key, r.label, r.description or r.label, r.weight) for r in rows]
    return [(k, k.title(), _DEFAULT_DESC.get(k, k), w) for k, w in DEFAULT_WEIGHTS.items()]


def _candidate_summary(c) -> str:
    skills = ", ".join(c.skills.values_list("name", flat=True)) or "—"
    return (
        f"Name: {c.first_name} {c.last_name}\n"
        f"Current role: {c.current_role or '—'} at {c.current_company or '—'}\n"
        f"Total experience: {c.total_experience or 0} years\n"
        f"Skills: {skills}\n"
        f"Qualification: {c.highest_qualification or '—'}\n"
        f"Location: {c.current_location or c.city or '—'}\n"
    )


def _jd_summary(job) -> str:
    return (
        f"Title: {job.title}\n"
        f"Experience band: {job.experience_band or '—'}\n"
        f"Must-have skills: {job.must_have_skills or '—'}\n"
        f"Good-to-have skills: {job.good_to_have_skills or '—'}\n"
        f"Qualifications: {job.qualifications or '—'}\n"
        f"Location: {job.location or '—'}\n"
    )


def _build_prompt(job, candidate, params, weights) -> str:
    # params: [(key,label,description,default_weight)] — prompt is built dynamically (RANK-003)
    lines = "\n".join(
        f"- {key} ({label}, weight {weights.get(key, dw)}): {desc}"
        for key, label, desc, dw in params
    )
    keys_json = ", ".join(f'"{key}": <0-100>' for key, *_ in params)
    return (
        "You are an expert technical recruiter. Score how well the CANDIDATE matches the JOB DESCRIPTION.\n"
        "Score EACH parameter below from 0-100, then give a weighted overall (weights are the share of 100):\n"
        f"{lines}\n\n"
        f"=== JOB DESCRIPTION ===\n{_jd_summary(job)}\n"
        f"=== CANDIDATE ===\n{_candidate_summary(candidate)}\n"
        "Return ONLY strict JSON, no markdown, in this exact shape:\n"
        f'{{"overall": <0-100 int>, "parameters": {{{keys_json}}}, "rationale": "<1-2 sentence explanation>"}}'
    )


def _parse_json(text: str) -> dict | None:
    if not text:
        return None
    m = re.search(r"\{.*\}", text, re.DOTALL)
    if not m:
        return None
    try:
        return json.loads(m.group(0))
    except Exception:
        return None


def rank_job_candidates(job, user, top_n: int = 5, weights: dict | None = None) -> dict:
    """Rank all candidates currently in the JD's pipeline. Returns a summary dict.
    Weight precedence (RANK-002): explicit `weights` > job.rank_weights > global default.
    Explicit weights are persisted to the JD for future runs."""
    params = active_parameters()               # RANK-003: dynamic parameter set
    param_keys = [k for k, *_ in params]
    default_w = {k: dw for k, _, _, dw in params}
    if weights:
        # keep only known keys; persist the chosen weights on the JD
        weights = {k: int(weights.get(k, default_w.get(k, 0)) or 0) for k in param_keys}
        job.rank_weights = weights
        job.save(update_fields=["rank_weights", "updated_at"])
    else:
        saved = job.rank_weights or {}
        weights = {k: int(saved.get(k, default_w.get(k, 0)) or 0) for k in param_keys}
    apps_qs = JobApplication.objects.filter(job=job).select_related("candidate")
    if not apps_qs.exists():
        return {"ok": False, "error": "No candidates in this JD pipeline to rank. Assign candidates first."}

    provider = LLMProvider.objects.filter(is_active=True, is_default=True).first() or \
        LLMProvider.objects.filter(is_active=True).first()
    if not provider:
        return {"ok": False, "error": "No active LLM provider configured. Add one under Master Data → AI / LLM Providers."}

    run = RankRun.objects.create(
        job=job, weights=weights, provider_name=provider.name,
        candidate_count=apps_qs.count(), top_n=top_n, run_by=user,
    )

    scored = []
    for app in apps_qs:
        c = app.candidate
        result = call_llm(_build_prompt(job, c, params, weights), provider=provider, purpose="rank", user=user)
        parsed = _parse_json(result.get("text", "")) if result.get("ok") else None
        if parsed:
            overall = int(parsed.get("overall") or 0)
            param_scores = parsed.get("parameters") or {}
            rationale = str(parsed.get("rationale") or "")
        else:
            overall, param_scores, rationale = 0, {}, (result.get("error") or "Could not parse AI response.")
        rs = RankScore.objects.create(
            run=run, application=app, candidate=c,
            overall_score=max(0, min(100, overall)),
            parameter_scores=param_scores, rationale=rationale,
        )
        # keep the pipeline entry's score in sync (used by dashboards/reports)
        app.score = rs.overall_score
        app.save(update_fields=["score", "updated_at"])
        scored.append(rs)

    # flag Top-N (RANK-005)
    scored.sort(key=lambda r: r.overall_score, reverse=True)
    for i, rs in enumerate(scored):
        rs.is_top = i < top_n
        rs.save(update_fields=["is_top"])

    return {"ok": True, "run_id": run.id, "ranked": len(scored)}
