from __future__ import annotations

import json
from pathlib import Path
from typing import Protocol, TypeVar

from pydantic import BaseModel

from resume_agent.models import CareerProfile, TailoringPackage
from resume_agent.prompts import (
    AUDIT_PROMPT,
    PROFILE_PROMPT,
    PROFILE_REPAIR_PROMPT,
    REVISION_PROMPT,
    TAILOR_PROMPT,
)

T = TypeVar("T", bound=BaseModel)


class StructuredLLM(Protocol):
    def parse(self, schema: type[T], instructions: str, input_text: str) -> T: ...


def build_profile(llm: StructuredLLM, resume_text: str, about: str = "") -> CareerProfile:
    source = {
        "resume": resume_text,
        "candidate_notes": about,
    }
    profile = llm.parse(
        CareerProfile,
        PROFILE_PROMPT,
        json.dumps(source, ensure_ascii=False),
    )
    try:
        validate_profile(profile)
    except ValueError as exc:
        if profile.facts:
            raise
        repair_source = {
            **source,
            "previous_validation_error": str(exc),
            "previous_result": profile.model_dump(mode="json"),
        }
        profile = llm.parse(
            CareerProfile,
            PROFILE_REPAIR_PROMPT,
            json.dumps(repair_source, ensure_ascii=False),
        )
        try:
            validate_profile(profile)
        except ValueError as repair_exc:
            if not profile.facts:
                raise ValueError(
                    "The LLM could not extract evidence facts from "
                    f"{len(resume_text):,} characters of resume text after two attempts. "
                    "Try another model with reliable structured-output support, or simplify "
                    "the resume Markdown."
                ) from repair_exc
            raise
    return profile


def tailor_resume(
    llm: StructuredLLM,
    profile: CareerProfile,
    job_text: str,
    tailoring_strength: int = 50,
    evidence_mode: str = "strict",
) -> TailoringPackage:
    if not 0 <= tailoring_strength <= 100:
        raise ValueError("Tailoring strength must be between 0 and 100.")
    _validate_evidence_mode(evidence_mode)
    payload = {
        "canonical_profile": profile.model_dump(mode="json"),
        "job_post": job_text,
        "tailoring_strength": tailoring_strength,
        "evidence_mode": evidence_mode,
    }
    draft = llm.parse(
        TailoringPackage,
        _tailoring_prompt(tailoring_strength, evidence_mode),
        json.dumps(payload, ensure_ascii=False),
    )
    validate_package(profile, draft, require_evidence=evidence_mode == "strict")

    audit_payload = {
        "canonical_profile": profile.model_dump(mode="json"),
        "proposed_package": draft.model_dump(mode="json"),
        "tailoring_strength": tailoring_strength,
        "evidence_mode": evidence_mode,
    }
    audited = llm.parse(
        TailoringPackage,
        _audit_prompt(evidence_mode),
        json.dumps(audit_payload, ensure_ascii=False),
    )
    validate_package(profile, audited, require_evidence=evidence_mode == "strict")
    return audited


def _tailoring_prompt(strength: int, evidence_mode: str) -> str:
    if strength <= 20:
        guidance = (
            "Stay very close to the source wording and organization. Make only small "
            "relevance edits and prefer concise, directly quoted evidence."
        )
    elif strength <= 70:
        guidance = (
            "Reorder and rewrite supported facts for clear job relevance while retaining "
            "the candidate's original meaning and normal resume length."
        )
    else:
        guidance = (
            "Maximize truthful job alignment. Use the fullest relevant detail available "
            "in the evidence, stronger active phrasing, and job-post terminology only "
            "where directly supported. Include more supported bullets when useful."
        )
    return (
        f"{TAILOR_PROMPT}\n\n"
        f"Tailoring strength: {strength}/100.\n"
        f"{guidance}\n"
        f"{_evidence_guidance(evidence_mode)}\n"
        "This setting changes editing intensity only. It never permits fabricated, "
        "exaggerated, inferred, or unsupported claims."
    )


def _evidence_guidance(evidence_mode: str) -> str:
    if evidence_mode == "strict":
        return (
            "Evidence mode: strict. Every summary statement and resume item must cite "
            "one or more directly supporting profile fact IDs in evidence_ids. Put IDs "
            "only in evidence_ids, never in candidate-facing text."
        )
    return (
        "Evidence mode: flexible profile-based rewrite. Use the complete canonical "
        "profile as the factual source, but per-claim evidence IDs are optional and "
        "evidence_ids may be empty. Do not invent information absent from the profile."
    )


def _audit_prompt(evidence_mode: str) -> str:
    if evidence_mode == "strict":
        evidence_rules = (
            "Require every candidate-facing claim to cite directly supporting fact IDs. "
            "Reject unknown or mismatched IDs. Keep IDs only in evidence_ids."
        )
    else:
        evidence_rules = (
            "Audit claims against the canonical profile as a whole. Per-claim evidence "
            "IDs are optional; do not reject an otherwise supported claim solely because "
            "evidence_ids is empty."
        )
    return f"{AUDIT_PROMPT}\n\n{evidence_rules}"


def _validate_evidence_mode(evidence_mode: str) -> None:
    if evidence_mode not in {"strict", "profile"}:
        raise ValueError("Evidence mode must be either 'strict' or 'profile'.")


def revise_tailored_resume(
    llm: StructuredLLM,
    profile: CareerProfile,
    current: TailoringPackage,
    instruction: str,
    evidence_mode: str = "strict",
) -> TailoringPackage:
    instruction = instruction.strip()
    if len(instruction) < 3:
        raise ValueError("Describe how you want the tailored resume revised.")
    _validate_evidence_mode(evidence_mode)

    revision_payload = {
        "canonical_profile": profile.model_dump(mode="json"),
        "current_tailored_package": current.model_dump(mode="json"),
        "candidate_request": instruction,
        "evidence_mode": evidence_mode,
    }
    revised = llm.parse(
        TailoringPackage,
        f"{REVISION_PROMPT}\n\n{_evidence_guidance(evidence_mode)}",
        json.dumps(revision_payload, ensure_ascii=False),
    )
    _validate_revision(profile, current, revised, evidence_mode)

    audit_payload = {
        "canonical_profile": profile.model_dump(mode="json"),
        "original_job_analysis": current.job.model_dump(mode="json"),
        "candidate_request": instruction,
        "proposed_package": revised.model_dump(mode="json"),
        "evidence_mode": evidence_mode,
    }
    audited = llm.parse(
        TailoringPackage,
        _audit_prompt(evidence_mode),
        json.dumps(audit_payload, ensure_ascii=False),
    )
    _validate_revision(profile, current, audited, evidence_mode)
    return audited


def _validate_revision(
    profile: CareerProfile,
    current: TailoringPackage,
    revised: TailoringPackage,
    evidence_mode: str,
) -> None:
    validate_package(profile, revised, require_evidence=evidence_mode == "strict")
    if revised.job != current.job:
        raise ValueError("A resume revision cannot change the original job analysis.")


def validate_profile(profile: CareerProfile) -> None:
    ids = [fact.id for fact in profile.facts]
    if not ids:
        raise ValueError("The profile contains no evidence facts.")
    if len(ids) != len(set(ids)):
        raise ValueError("The profile contains duplicate evidence IDs.")
    if any(not fact.source_excerpt.strip() for fact in profile.facts):
        raise ValueError("Every profile fact must include a source excerpt.")


def validate_package(
    profile: CareerProfile,
    package: TailoringPackage,
    *,
    require_evidence: bool = True,
) -> None:
    valid_ids = {fact.id for fact in profile.facts}
    backed_items = list(package.resume.summary)
    for section in package.resume.sections:
        backed_items.extend(section.items)

    if not backed_items:
        raise ValueError("The tailored resume contains no evidence-backed content.")
    for item in backed_items:
        if require_evidence and not item.evidence_ids:
            raise ValueError(f"Resume claim has no evidence: {item.text}")
        unknown = set(item.evidence_ids) - valid_ids
        if unknown:
            raise ValueError(
                f"Resume claim references unknown evidence IDs: {', '.join(sorted(unknown))}"
            )

    if package.resume.contact != profile.contact:
        raise ValueError("The tailored resume changed the candidate's contact information.")


def save_json(model: BaseModel, path: Path) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(model.model_dump_json(indent=2), encoding="utf-8")


def load_profile(path: Path) -> CareerProfile:
    return CareerProfile.model_validate_json(path.read_text(encoding="utf-8"))
