from __future__ import annotations

import json
from typing import Any

from django.core.exceptions import ValidationError
from django.db import transaction
from django.urls import reverse
from django.utils.html import strip_tags

from apps.core.audit.rep_helpers import action_create, action_update, audit_rep
from apps.alumni.careers.models import CVProfile, CVSection, JobApplication, JobListing


def _record_career_indicator(*, indicator_code: str, source_event: str, source_object_id) -> None:
    """Feed the job-board funnel into MEL as an automated data point.

    Replaces the old REP ``signal → IntegrationOutbox → drain`` path now that the
    REP LMS is gone. Fired on commit so a MEL hiccup never rolls back the
    job-board write; ``record_automated_point`` itself no-ops when the M&EL
    officer hasn't configured the indicator yet.
    """

    def _emit() -> None:
        try:
            from apps.mel.indicators.services import record_automated_point

            record_automated_point(
                indicator_code=indicator_code,
                source_module="careers",
                source_event=source_event,
                source_object_id=str(source_object_id),
            )
        except Exception:  # pragma: no cover - defensive; never break careers
            import logging as _logging

            _logging.getLogger(__name__).exception(
                "careers.mel_indicator emit failed: %s", indicator_code
            )

    transaction.on_commit(_emit)


MAX_SECTION_ITEMS = 20
# Long narrative fields (plain text; line breaks preserved in JSON)
LEN_SHORT = 255
LEN_LINE = 512
LEN_BLOCK = 8000
LEN_LONG = 12000

SECTION_SORT_ORDER = {
    "education": 0,
    "experience": 1,
    "skills": 2,
    "certifications": 3,
    "publications": 4,
    "languages": 5,
    "references": 6,
}


def get_or_create_cv(user) -> CVProfile:
    cv, created = CVProfile.objects.get_or_create(user=user)
    if created:
        # FRREP-TS005 — pre-populate a brand-new CV from the learner's REP
        # profile so they start from their own data, not a blank form. Only
        # ever runs on first creation; never overwrites an existing profile
        # or saved sections. Fully defensive — a prefill failure must never
        # block CV editing.
        try:
            prefill_cv_from_profile(cv, user)
        except Exception:  # pragma: no cover - defensive; prefill is best-effort
            import logging as _logging

            _logging.getLogger(__name__).exception(
                "rep.career: CV prefill failed for user=%s", getattr(user, "pk", None)
            )
    return cv


def _split_interest_tags(value: Any) -> list[str]:
    """Split a comma- (or newline-) separated interests string into clean tags."""
    if not value:
        return []
    text = str(value).replace("\n", ",")
    seen: set[str] = set()
    out: list[str] = []
    for raw in text.split(","):
        tag = raw.strip()
        if not tag:
            continue
        key = tag.lower()
        if key in seen:
            continue
        seen.add(key)
        out.append(tag[:LEN_LINE])
        if len(out) >= MAX_SECTION_ITEMS:
            break
    return out


def _education_rows_from_qualifications(qualifications: Any) -> list[dict]:
    """Map the loose ``profile.qualifications`` JSON into education section rows.

    ``qualifications`` may be a list of dicts (``{institution, qualification,
    year}``), a list of plain strings, or blank/None. Anything unexpected is
    skipped rather than raised.
    """
    if not isinstance(qualifications, list):
        return []
    rows: list[dict] = []
    for item in qualifications:
        if isinstance(item, dict):
            degree = _clean_str(
                item.get("qualification") or item.get("degree") or item.get("name"),
                LEN_SHORT,
            )
            institution = _clean_str(item.get("institution") or item.get("school"), LEN_SHORT)
            year = _clean_str(item.get("year"), 32)
            details = _clean_str(item.get("details"), LEN_BLOCK)
            row = {
                "degree": degree,
                "institution": institution,
                "year": year,
                "details": details,
            }
        elif isinstance(item, str):
            deg = _clean_str(item, LEN_SHORT)
            if not deg:
                continue
            row = {"degree": deg, "institution": "", "year": "", "details": ""}
        else:
            continue
        if any(v for v in row.values() if v):
            rows.append(row)
        if len(rows) >= MAX_SECTION_ITEMS:
            break
    return rows


def _experience_rows_from_profile(experience: Any) -> list[dict]:
    """Map the loose ``profile.experience`` JSON (see UserProfile.experience)
    into CV "experience" section rows (``{role, org, years, description}`` —
    see normalize_section_data's "experience" branch above).
    """
    if not isinstance(experience, list):
        return []
    rows: list[dict] = []
    for item in experience:
        if not isinstance(item, dict):
            continue
        start_year = item.get("start_year")
        end_year = item.get("end_year")
        if item.get("is_current"):
            years = f"{start_year}–Present" if start_year else "Present"
        elif start_year and end_year:
            years = f"{start_year}–{end_year}"
        else:
            years = _clean_str(start_year or end_year, 64)
        row = {
            "role": _clean_str(item.get("title"), LEN_SHORT),
            "org": _clean_str(item.get("organization"), LEN_SHORT),
            "years": years,
            "description": _clean_str(item.get("description"), LEN_LONG),
        }
        if any(v for v in row.values() if v):
            rows.append(row)
        if len(rows) >= MAX_SECTION_ITEMS:
            break
    return rows


@transaction.atomic
def prefill_cv_from_profile(cv: CVProfile, user) -> None:
    """Seed a freshly created CVProfile from the learner's REP profile (FRREP-TS005).

    Best-effort: a missing profile or empty fields simply yield fewer seeded
    items — never an error. Caller guarantees this only runs on first creation.
    """
    profile = getattr(user, "profile", None)

    headline = ""
    if profile is not None:
        background = (profile.professional_background or "").strip()
        if background:
            headline = background.splitlines()[0].strip()
        if not headline:
            degree_display = profile.get_degree_level_display() if profile.degree_level else ""
            country = (profile.country or "").strip()
            parts = [p for p in (degree_display, country) if p]
            headline = " · ".join(parts)
    cv.headline = _clean_str(headline, LEN_SHORT)

    summary = ""
    if profile is not None and profile.bio:
        summary = strip_tags(profile.bio).strip()
    cv.summary = summary[:LEN_LONG]

    if cv.headline or cv.summary:
        cv.save(update_fields=["headline", "summary"])

    if profile is None:
        return

    skills = _split_interest_tags(profile.areas_of_interest)
    if skills:
        upsert_cv_section(
            cv,
            "skills",
            skills,
            sort_order=SECTION_SORT_ORDER.get("skills", 0),
        )

    education_rows = _education_rows_from_qualifications(profile.qualifications)
    if education_rows:
        upsert_cv_section(
            cv,
            "education",
            education_rows,
            sort_order=SECTION_SORT_ORDER.get("education", 0),
        )

    experience_rows = _experience_rows_from_profile(profile.experience)
    if experience_rows:
        upsert_cv_section(
            cv,
            "experience",
            experience_rows,
            sort_order=SECTION_SORT_ORDER.get("experience", 1),
        )


def build_sections_initial(cv: CVProfile) -> dict[str, list]:
    initial: dict[str, list] = {t: [] for t in CVSection.SectionType.values}
    for s in cv.sections.all():
        if s.section_type in initial and isinstance(s.data, list):
            initial[s.section_type] = list(s.data)
    return initial


def parse_cv_sections_json(raw: str) -> dict[str, list]:
    if not raw or not str(raw).strip():
        return {t: [] for t in CVSection.SectionType.values}
    try:
        data = json.loads(raw)
    except json.JSONDecodeError as exc:
        raise ValidationError("CV sections could not be read. Reload the page and try again.") from exc
    if not isinstance(data, dict):
        raise ValidationError("Invalid CV sections data.")
    merged: dict[str, list] = {t: [] for t in CVSection.SectionType.values}
    for key, val in data.items():
        if key in merged:
            merged[key] = val
    return merged


def _clean_str(value: Any, max_len: int = 512) -> str:
    if value is None:
        return ""
    return str(value).strip()[:max_len]


def normalize_section_data(section_type: str, data: Any) -> list:
    if not isinstance(data, list):
        raise ValidationError(f"“{section_type}” must be a list.")
    if len(data) > MAX_SECTION_ITEMS:
        raise ValidationError(f"Too many entries in {section_type} (max {MAX_SECTION_ITEMS}).")

    if section_type == "education":
        out = []
        for item in data:
            if not isinstance(item, dict):
                continue
            row = {
                "degree": _clean_str(item.get("degree"), LEN_SHORT),
                "institution": _clean_str(item.get("institution"), LEN_SHORT),
                "year": _clean_str(item.get("year"), 32),
                "details": _clean_str(item.get("details"), LEN_BLOCK),
            }
            if any(v for v in row.values() if v):
                out.append(row)
        return out

    if section_type == "experience":
        out = []
        for item in data:
            if not isinstance(item, dict):
                continue
            row = {
                "role": _clean_str(item.get("role"), LEN_SHORT),
                "org": _clean_str(item.get("org"), LEN_SHORT),
                "years": _clean_str(item.get("years"), 64),
                "description": _clean_str(item.get("description"), LEN_LONG),
            }
            if any(v for v in row.values() if v):
                out.append(row)
        return out

    if section_type == "certifications":
        out = []
        for item in data:
            if not isinstance(item, dict):
                continue
            row = {
                "name": _clean_str(item.get("name"), LEN_BLOCK),
                "year": _clean_str(item.get("year"), 32),
                "details": _clean_str(item.get("details"), LEN_LONG),
            }
            if any(v for v in row.values() if v):
                out.append(row)
        return out

    if section_type == "publications":
        out = []
        for item in data:
            if not isinstance(item, dict):
                continue
            row = {
                "title": _clean_str(item.get("title"), LEN_BLOCK),
                "venue": _clean_str(item.get("venue"), LEN_LINE),
                "year": _clean_str(item.get("year"), 32),
                "notes": _clean_str(item.get("notes"), LEN_LONG),
            }
            if any(v for v in row.values() if v):
                out.append(row)
        return out

    if section_type in ("skills", "languages", "references"):
        out = []
        for item in data:
            if isinstance(item, str):
                s = _clean_str(item, LEN_LINE)
            elif isinstance(item, dict):
                s = _clean_str(item.get("text") or item.get("name"), LEN_LINE)
            else:
                s = _clean_str(item, LEN_LINE)
            if s:
                out.append(s)
        return out

    raise ValidationError(f"Unknown section type: {section_type}")


@transaction.atomic
def sync_cv_sections(cv: CVProfile, payload: dict) -> None:
    allowed = set(CVSection.SectionType.values)
    for key in payload:
        if key not in allowed:
            raise ValidationError(f"Unknown section type: {key}")
    for st in CVSection.SectionType.values:
        raw = payload.get(st, [])
        normalized = normalize_section_data(st, raw)
        if not normalized:
            CVSection.objects.filter(cv=cv, section_type=st).delete()
        else:
            sort_order = SECTION_SORT_ORDER.get(st, 0)
            upsert_cv_section(cv, st, normalized, sort_order=sort_order)


@transaction.atomic
def upsert_cv_section(cv: CVProfile, section_type: str, data: list, sort_order: int = 0) -> CVSection:
    section, _ = CVSection.objects.update_or_create(
        cv=cv,
        section_type=section_type,
        defaults={"data": data, "sort_order": sort_order},
    )
    return section


def cv_to_dict(cv: CVProfile) -> dict:
    """Serialize a CVProfile to a plain dict (for snapshot on job application)."""
    return {
        "user_id": cv.user_id,
        "headline": cv.headline,
        "summary": cv.summary,
        "sections": [
            {"type": s.section_type, "data": s.data}
            for s in cv.sections.order_by("sort_order")
        ],
    }


def html_meaningfully_filled(value: str | None) -> bool:
    return bool(strip_tags(value or "").strip())


def cv_dict_has_substance(d: dict | None) -> bool:
    """True if dict has any recruiter-visible CV content."""
    if not d or not isinstance(d, dict):
        return False
    if (d.get("headline") or "").strip():
        return True
    if html_meaningfully_filled(d.get("summary")):
        return True
    for sec in d.get("sections") or []:
        if not isinstance(sec, dict):
            continue
        data = sec.get("data")
        if isinstance(data, list) and len(data) > 0:
            return True
        if data:
            return True
    return False


def _normalize_cv_display_dict(d: dict) -> dict:
    out = dict(d) if d else {}
    if not isinstance(out.get("sections"), list):
        out["sections"] = []
    return out


def resolve_cv_display_for_recruiter(application: JobApplication, cv: CVProfile) -> tuple[dict, str]:
    """
    Choose CV JSON to show recruiters.

    - If the profile was saved after apply, use the live profile (updated fields).
    - If the snapshot is empty but the live profile has content, use live (backfill).
    - Otherwise use the submission snapshot.

    Returns (display_dict, mode) where mode is live_updated | live_backfill | snapshot.
    """
    snapshot = application.cv_snapshot if isinstance(application.cv_snapshot, dict) else {}
    live = cv_to_dict(cv)
    applied = application.applied_at
    updated = cv.updated_at

    if updated and applied and updated > applied and cv_dict_has_substance(live):
        return _normalize_cv_display_dict(live), "live_updated"
    if not cv_dict_has_substance(snapshot) and cv_dict_has_substance(live):
        return _normalize_cv_display_dict(live), "live_backfill"
    return _normalize_cv_display_dict(snapshot), "snapshot"


@transaction.atomic
def decide_application(
    application: JobApplication,
    *,
    status: str,
    decided_by,
    note: str = "",
    request=None,
) -> JobApplication:
    """Recruiter / poster moves the application to a final or interim status.

    Writes the new status, notifies the applicant, audits the change, and
    fires `rep_job_application_decided` so M&EL can track the funnel.
    """
    valid_statuses = {choice[0] for choice in JobApplication.Status.choices}
    if status not in valid_statuses:
        raise ValidationError(f"Invalid application status: {status}")
    old_status = application.status
    if old_status == status:
        return application
    application.status = status
    application.save(update_fields=["status"])
    audit_rep(
        actor=decided_by,
        action=action_update(),
        target_model="JobApplication",
        object_id=application.pk,
        object_repr=str(application),
        changes={"old_status": old_status, "new_status": status, "has_note": bool(note)},
        request=request,
    )
    from apps.core.notifications.models import Notification
    from apps.core.notifications.services import send_notification

    pretty = dict(JobApplication.Status.choices).get(status, status)
    body = f'Your application for "{application.listing.title}" was updated: {pretty}.'
    if note:
        body = f"{body} — {note}"
    send_notification(
        application.applicant,
        body,
        verb=Notification.Verb.REP_JOB_APPLICATION_DECIDED,
        email_context={
            "listing_title": application.listing.title,
            "status": pretty,
            "note": note,
            "action_url": reverse("rep_career:job_detail", kwargs={"pk": application.listing_id}),
        },
    )
    _record_career_indicator(
        indicator_code="careers-job-application-outcomes",
        source_event="job_application_decided",
        source_object_id=application.pk,
    )
    return application


@transaction.atomic
def apply_for_job(
    user,
    listing: JobListing,
    cover_letter: str = "",
    cover_letter_file=None,
) -> JobApplication:
    if not listing.is_active:
        raise ValidationError("This job listing is no longer active.")
    if getattr(listing, "listing_status", None) and listing.listing_status != JobListing.ListingStatus.PUBLISHED:
        raise ValidationError("This job listing is not open for applications.")
    existing = JobApplication.objects.filter(listing=listing, applicant=user).first()
    if existing:
        raise ValidationError("You have already applied for this position.")
    cv = get_or_create_cv(user)
    cv = CVProfile.objects.prefetch_related("sections").get(pk=cv.pk)
    app = JobApplication.objects.create(
        listing=listing,
        applicant=user,
        cover_letter=cover_letter,
        cover_letter_file=cover_letter_file or None,
        cv_snapshot=cv_to_dict(cv),
    )
    # WS2.4 / FRREP-TS009 — application submission must be auditable.
    audit_rep(
        actor=user,
        action=action_create(),
        target_model="JobApplication",
        object_id=app.pk,
        object_repr=f"{user.email} -> {listing.title}",
        changes={
            "listing_id": listing.pk,
            "listing_title": listing.title[:200],
            "has_cover_letter": bool(cover_letter),
            "has_cover_letter_file": bool(cover_letter_file),
        },
    )
    _record_career_indicator(
        indicator_code="careers-job-applications",
        source_event="job_application_submitted",
        source_object_id=app.pk,
    )
    return app
