#!/usr/bin/env python3
"""Manage approved resume achievements and source-state integrity."""

from __future__ import annotations

import argparse
import hashlib
import json
import os
import re
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


VAULT_FILENAME = "resume_vault.json"
STATE_FILENAME = "resume_workflow_state.json"
INDEXER_PATH = Path("scripts/index_resume_library.py")
ID_PATTERN = re.compile(r"^[a-z0-9][a-z0-9-]{2,79}$")


def now_utc() -> str:
    return datetime.now(timezone.utc).replace(microsecond=0).isoformat()


def normalize_text(text: str) -> str:
    return re.sub(r"\s+", " ", text).strip()


def sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def resolve_repo_root(value: str) -> Path:
    root = Path(value).expanduser().resolve()
    if not root.is_dir():
        raise ValueError(f"Repository root does not exist: {root}")
    return root


def resolve_repo_path(root: Path, value: str, *, must_exist: bool = True) -> Path:
    candidate = (root / value).resolve() if not Path(value).is_absolute() else Path(value).resolve()
    try:
        candidate.relative_to(root)
    except ValueError as exc:
        raise ValueError(f"Path must stay inside repository root: {value}") from exc
    if must_exist and not candidate.exists():
        raise ValueError(f"Path does not exist: {candidate}")
    return candidate


def relative_path(root: Path, path: Path) -> str:
    return path.relative_to(root).as_posix()


def read_json(path: Path, default: dict[str, Any]) -> dict[str, Any]:
    if not path.exists():
        return default
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except json.JSONDecodeError as exc:
        raise ValueError(f"Invalid JSON in {path}: {exc}") from exc
    if not isinstance(data, dict):
        raise ValueError(f"Expected a JSON object in {path}")
    return data


def write_json_atomic(path: Path, data: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    payload = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
    with tempfile.NamedTemporaryFile(
        "w",
        encoding="utf-8",
        dir=path.parent,
        prefix=f".{path.name}.",
        suffix=".tmp",
        delete=False,
    ) as handle:
        handle.write(payload)
        temp_name = handle.name
    os.replace(temp_name, path)


def empty_vault() -> dict[str, Any]:
    return {
        "schema_version": 1,
        "kind": "approved_resume_achievements",
        "updated_at": None,
        "achievements": [],
    }


def empty_state() -> dict[str, Any]:
    return {
        "schema_version": 1,
        "kind": "resume_workflow_state",
        "updated_at": None,
        "resumes": {},
    }


def load_stores(root: Path) -> tuple[dict[str, Any], dict[str, Any]]:
    vault = read_json(root / VAULT_FILENAME, empty_vault())
    state = read_json(root / STATE_FILENAME, empty_state())
    if not isinstance(vault.get("achievements"), list):
        raise ValueError(f"{VAULT_FILENAME} must contain an achievements array")
    if not isinstance(state.get("resumes"), dict):
        raise ValueError(f"{STATE_FILENAME} must contain a resumes object")
    return vault, state


def save_stores(root: Path, vault: dict[str, Any], state: dict[str, Any]) -> None:
    timestamp = now_utc()
    vault["updated_at"] = timestamp
    state["updated_at"] = timestamp
    write_json_atomic(root / VAULT_FILENAME, vault)
    write_json_atomic(root / STATE_FILENAME, state)


def experience_bullets(path: Path) -> list[str]:
    bullets: list[str] = []
    in_experience = False
    for raw in path.read_text(encoding="utf-8").splitlines():
        line = raw.strip()
        if line.startswith("## "):
            heading = line[3:].strip().lower()
            in_experience = heading in {"professional experience", "experience"}
            continue
        if in_experience and line.startswith("- "):
            bullets.append(normalize_text(line[2:]))
    return bullets


def resume_bullets(path: Path) -> list[str]:
    return [
        normalize_text(line.strip()[2:])
        for line in path.read_text(encoding="utf-8").splitlines()
        if line.strip().startswith("- ")
    ]


def approved_text_map(vault: dict[str, Any]) -> dict[str, str]:
    result: dict[str, str] = {}
    for item in vault["achievements"]:
        if item.get("status") != "approved":
            continue
        text = normalize_text(str(item.get("canonical_text", "")))
        if text:
            result[text] = str(item["id"])
    return result


def state_record(state: dict[str, Any], resume_rel: str) -> dict[str, Any]:
    resumes = state["resumes"]
    record = resumes.setdefault(
        resume_rel,
        {
            "state": "draft",
            "source_sha256": None,
            "approved_bullet_ids": [],
            "updated_at": None,
        },
    )
    return record


def invalidate_final(record: dict[str, Any]) -> None:
    record.pop("content_approved_at", None)
    record.pop("rendered_final_at", None)
    record.pop("exports", None)
    record.pop("completion_log", None)
    record.pop("qa_note", None)


def run_indexer(root: Path) -> None:
    indexer = root / INDEXER_PATH
    if not indexer.exists():
        return
    venv_python = root / ".venv" / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
    python = venv_python if venv_python.exists() else Path(sys.executable)
    result = subprocess.run(
        [str(python), str(indexer)],
        cwd=root,
        text=True,
        capture_output=True,
    )
    if result.returncode != 0:
        detail = (result.stderr or result.stdout).strip()
        raise RuntimeError(f"Broad-library rebuild failed: {detail}")


def cmd_init(args: argparse.Namespace) -> int:
    root = resolve_repo_root(args.repo_root)
    vault, state = load_stores(root)
    save_stores(root, vault, state)
    print(f"Initialized {VAULT_FILENAME} and {STATE_FILENAME}")
    return 0


def cmd_approve(args: argparse.Namespace) -> int:
    root = resolve_repo_root(args.repo_root)
    if not ID_PATTERN.fullmatch(args.id):
        raise ValueError("Achievement ID must use 3-80 lowercase letters, digits, or hyphens")

    source = resolve_repo_path(root, args.source)
    source_rel = relative_path(root, source)
    text = normalize_text(args.text)
    if text not in resume_bullets(source):
        raise ValueError("Approved text is not an exact bullet in the source resume")

    vault, state = load_stores(root)
    existing = next((item for item in vault["achievements"] if item.get("id") == args.id), None)
    timestamp = now_utc()
    if existing is None:
        existing = {
            "id": args.id,
            "status": "approved",
            "canonical_text": text,
            "company": args.company,
            "role": args.role,
            "section": args.section,
            "source_resume": source_rel,
            "source_sha256": sha256_file(source),
            "evidence": args.evidence,
            "metrics": list(dict.fromkeys(args.metric or [])),
            "target_signals": list(dict.fromkeys(args.target_signal or [])),
            "approved_by": args.approved_by,
            "approved_at": timestamp,
            "updated_at": timestamp,
            "history": [],
        }
        vault["achievements"].append(existing)
    else:
        previous_text = normalize_text(str(existing.get("canonical_text", "")))
        if previous_text != text:
            if not args.change_note:
                raise ValueError("--change-note is required when replacing approved wording")
            existing.setdefault("history", []).append(
                {
                    "text": previous_text,
                    "replaced_at": timestamp,
                    "change_note": args.change_note,
                    "source_resume": existing.get("source_resume"),
                }
            )
        existing.update(
            {
                "status": "approved",
                "canonical_text": text,
                "company": args.company,
                "role": args.role,
                "section": args.section,
                "source_resume": source_rel,
                "source_sha256": sha256_file(source),
                "evidence": args.evidence,
                "metrics": list(dict.fromkeys(args.metric or [])),
                "target_signals": list(dict.fromkeys(args.target_signal or [])),
                "approved_by": args.approved_by,
                "approved_at": timestamp,
                "updated_at": timestamp,
            }
        )

    record = state_record(state, source_rel)
    approved_ids = set(record.get("approved_bullet_ids", []))
    approved_ids.add(args.id)
    record.update(
        {
            "state": "draft",
            "source_sha256": sha256_file(source),
            "approved_bullet_ids": sorted(approved_ids),
            "updated_at": timestamp,
        }
    )
    invalidate_final(record)
    save_stores(root, vault, state)
    if not args.skip_index:
        run_indexer(root)
    print(f"Approved {args.id}")
    return 0


def cmd_mark_draft(args: argparse.Namespace) -> int:
    root = resolve_repo_root(args.repo_root)
    resume = resolve_repo_path(root, args.resume)
    resume_rel = relative_path(root, resume)
    vault, state = load_stores(root)
    record = state_record(state, resume_rel)
    record.update(
        {
            "state": "draft",
            "source_sha256": sha256_file(resume),
            "updated_at": now_utc(),
        }
    )
    invalidate_final(record)
    save_stores(root, vault, state)
    print(f"Marked draft: {resume_rel}")
    return 0


def cmd_content_approved(args: argparse.Namespace) -> int:
    root = resolve_repo_root(args.repo_root)
    resume = resolve_repo_path(root, args.resume)
    resume_rel = relative_path(root, resume)
    vault, state = load_stores(root)
    approved = approved_text_map(vault)
    bullets = experience_bullets(resume)
    missing = [bullet for bullet in bullets if bullet not in approved]
    if missing:
        formatted = "\n".join(f"- {bullet}" for bullet in missing)
        raise ValueError(f"Unapproved experience bullets remain:\n{formatted}")

    timestamp = now_utc()
    record = state_record(state, resume_rel)
    record.update(
        {
            "state": "content-approved",
            "source_sha256": sha256_file(resume),
            "approved_bullet_ids": sorted({approved[bullet] for bullet in bullets}),
            "content_approved_at": timestamp,
            "updated_at": timestamp,
        }
    )
    record.pop("rendered_final_at", None)
    record.pop("exports", None)
    save_stores(root, vault, state)
    print(f"Content approved: {resume_rel} ({len(bullets)} bullets)")
    return 0


def effective_status(root: Path, resume: Path, record: dict[str, Any] | None) -> dict[str, Any]:
    resume_rel = relative_path(root, resume)
    current_hash = sha256_file(resume)
    if record is None:
        return {
            "resume": resume_rel,
            "stored_state": None,
            "effective_state": "draft",
            "drift": True,
            "source_sha256": current_hash,
        }

    stored_state = record.get("state", "draft")
    source_drift = record.get("source_sha256") != current_hash
    export_drift = False
    if stored_state == "rendered-final":
        for artifact in record.get("exports", {}).values():
            path = root / artifact["path"]
            if not path.exists() or sha256_file(path) != artifact.get("sha256"):
                export_drift = True
                break

    drift = source_drift or export_drift
    return {
        "resume": resume_rel,
        "stored_state": stored_state,
        "effective_state": "draft" if drift else stored_state,
        "drift": drift,
        "source_drift": source_drift,
        "export_drift": export_drift,
        "source_sha256": current_hash,
        "approved_bullet_ids": record.get("approved_bullet_ids", []),
    }


def cmd_status(args: argparse.Namespace) -> int:
    root = resolve_repo_root(args.repo_root)
    resume = resolve_repo_path(root, args.resume)
    _, state = load_stores(root)
    resume_rel = relative_path(root, resume)
    result = effective_status(root, resume, state["resumes"].get(resume_rel))
    print(json.dumps(result, indent=2))
    return 1 if result["drift"] else 0


def completion_log_links_resume(log: Path, resume_rel: str) -> bool:
    text = log.read_text(encoding="utf-8")
    position = text.find(resume_rel)
    if position < 0:
        return False
    window = text[max(0, position - 1000) : position + 2500]
    return "final-rendered" in window


def cmd_finalize(args: argparse.Namespace) -> int:
    root = resolve_repo_root(args.repo_root)
    resume = resolve_repo_path(root, args.resume)
    docx = resolve_repo_path(root, args.docx)
    pdf = resolve_repo_path(root, args.pdf)
    log = resolve_repo_path(root, args.completion_log)
    resume_rel = relative_path(root, resume)
    vault, state = load_stores(root)
    record = state["resumes"].get(resume_rel)
    status = effective_status(root, resume, record)
    if status["effective_state"] != "content-approved":
        raise ValueError("Resume must be content-approved with no source drift before finalization")
    if not completion_log_links_resume(log, resume_rel):
        raise ValueError("Completion log does not contain a final-rendered entry for this resume")

    if not args.skip_index:
        run_indexer(root)

    timestamp = now_utc()
    record.update(
        {
            "state": "rendered-final",
            "source_sha256": sha256_file(resume),
            "rendered_final_at": timestamp,
            "updated_at": timestamp,
            "completion_log": relative_path(root, log),
            "qa_note": args.qa_note,
            "exports": {
                "docx": {
                    "path": relative_path(root, docx),
                    "sha256": sha256_file(docx),
                },
                "pdf": {
                    "path": relative_path(root, pdf),
                    "sha256": sha256_file(pdf),
                },
            },
        }
    )
    save_stores(root, vault, state)
    print(f"Rendered final: {resume_rel}")
    return 0


def cmd_list(args: argparse.Namespace) -> int:
    root = resolve_repo_root(args.repo_root)
    vault, _ = load_stores(root)
    rows = [
        {
            "id": item.get("id"),
            "company": item.get("company"),
            "role": item.get("role"),
            "canonical_text": item.get("canonical_text"),
        }
        for item in vault["achievements"]
        if item.get("status") == "approved"
    ]
    print(json.dumps(rows, indent=2, ensure_ascii=False))
    return 0


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Manage the approved resume vault and resume source states."
    )
    parser.add_argument("--repo-root", default=".", help="Resume repository root")
    subparsers = parser.add_subparsers(dest="command", required=True)

    init_parser = subparsers.add_parser("init", help="Initialize vault and state files")
    init_parser.set_defaults(func=cmd_init)

    approve = subparsers.add_parser("approve", help="Persist an explicitly approved bullet")
    approve.add_argument("--id", required=True)
    approve.add_argument("--text", required=True)
    approve.add_argument("--company", required=True)
    approve.add_argument("--role", required=True)
    approve.add_argument("--section", default="Professional Experience")
    approve.add_argument("--source", required=True)
    approve.add_argument("--evidence", required=True)
    approve.add_argument("--metric", action="append")
    approve.add_argument("--target-signal", action="append")
    approve.add_argument("--approved-by", default="user")
    approve.add_argument("--change-note")
    approve.add_argument("--skip-index", action="store_true")
    approve.set_defaults(func=cmd_approve)

    mark_draft = subparsers.add_parser("mark-draft", help="Invalidate approved/final state")
    mark_draft.add_argument("--resume", required=True)
    mark_draft.set_defaults(func=cmd_mark_draft)

    approved = subparsers.add_parser(
        "content-approved",
        help="Require every current experience bullet to match the approved vault",
    )
    approved.add_argument("--resume", required=True)
    approved.set_defaults(func=cmd_content_approved)

    status = subparsers.add_parser("status", help="Report effective state and hash drift")
    status.add_argument("--resume", required=True)
    status.set_defaults(func=cmd_status)

    finalize = subparsers.add_parser("finalize", help="Record verified rendered-final state")
    finalize.add_argument("--resume", required=True)
    finalize.add_argument("--docx", required=True)
    finalize.add_argument("--pdf", required=True)
    finalize.add_argument("--completion-log", required=True)
    finalize.add_argument("--qa-note", required=True)
    finalize.add_argument("--skip-index", action="store_true")
    finalize.set_defaults(func=cmd_finalize)

    list_parser = subparsers.add_parser("list", help="List approved achievements")
    list_parser.set_defaults(func=cmd_list)
    return parser


def main(argv: list[str] | None = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)
    try:
        return int(args.func(args))
    except (OSError, ValueError, RuntimeError) as exc:
        parser.error(str(exc))
    return 2


if __name__ == "__main__":
    raise SystemExit(main())
