#!/usr/bin/env python3
"""
tailor.py - tailor a resume to a job description using several models via OpenRouter,
then have a model critique the result as a skeptical hiring manager.

Setup (once):
    Put your full career history in   resume.md   (the more detail the better)
    Put the job description in        job.md
    Optional: put a resume to use as a structural template in   template.md

Usage:
    python3 tailor.py                      # tailor with default models, then critique
    python3 tailor.py --critique-only      # just critique resume.md against job.md
    python3 tailor.py -m openai/gpt-4o-mini -m anthropic/claude-sonnet-4

Outputs land in  drafts/  with a timestamp, one file per model plus a critique.
Requires OPENROUTER_API_KEY in your environment. Standard library only.
"""

import argparse
import json
import os
import sys
import time
import urllib.request
import urllib.error
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from pathlib import Path

API_URL = "https://openrouter.ai/api/v1/chat/completions"

# Claude Code: verify these against https://openrouter.ai/api/v1/models and update.
DEFAULT_MODELS = [
    "openai/gpt-4o-mini",
    "meta-llama/llama-3.3-70b-instruct",
    "anthropic/claude-haiku-4.5",
    "google/gemini-2.5-flash",
]
CRITIC_MODEL = "openai/gpt-4o-mini"

TAILOR_PROMPT = """You are an expert resume writer for tech and startup roles.

Below is a candidate's full career history, a job description, and an existing
resume to use as a TEMPLATE. Write a tailored resume in Markdown for this role.

Rules:
- Follow the TEMPLATE's section order, headings, formatting, and voice exactly.
  Change the content, not the structure.
- Use only facts present in the career history or template. Never invent metrics,
  titles, or dates. Respect any "Do Not Claim" notes in the career history.
- Rewrite the summary and reorder or reword bullets so the most relevant experience
  for THIS job leads. Cut or shorten what isn't relevant.
- Mirror the language and priorities of the job description where it's honest to do so.
- Keep it to roughly one page. Plain, direct, verb-led. No em dashes. No buzzwords.
- Output only the resume, no commentary.

=== CAREER HISTORY ===
{resume}

=== JOB DESCRIPTION ===
{job}

=== TEMPLATE ===
{template}
"""

CRITIQUE_PROMPT = """You are a skeptical hiring manager at the company in the job description below.
You have 90 seconds with this resume. Be direct and specific.

Give:
1. First impression in one sentence.
2. The three strongest signals for THIS role.
3. The three biggest gaps or doubts you'd raise, and how the candidate could address each in a cover letter or interview.
4. Any line that sounds generic, inflated, or unclear, with a suggested rewrite.
5. A one-line pitch the candidate should use to open their cover letter.

=== JOB DESCRIPTION ===
{job}

=== RESUME ===
{resume}
"""


def call(model: str, prompt: str, api_key: str, max_tokens: int = 2000) -> dict:
    body = json.dumps({
        "model": model,
        "messages": [{"role": "user", "content": prompt}],
        "max_tokens": max_tokens,
        "usage": {"include": True},
    }).encode()
    req = urllib.request.Request(
        API_URL, data=body,
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
            "X-Title": "tailor-cli",
        },
    )
    start = time.perf_counter()
    try:
        with urllib.request.urlopen(req, timeout=180) as resp:
            data = json.loads(resp.read())
    except urllib.error.HTTPError as e:
        return {"model": model, "error": f"HTTP {e.code}: {e.read().decode()[:200]}"}
    except Exception as e:  # noqa: BLE001
        return {"model": model, "error": str(e)}
    return {
        "model": model,
        "text": data["choices"][0]["message"]["content"].strip(),
        "latency": time.perf_counter() - start,
        "cost": data.get("usage", {}).get("cost"),
    }


def read(path: str) -> str:
    p = Path(path)
    if not p.exists():
        sys.exit(f"Missing {path}. Create it first (see the top of this file).")
    return p.read_text().strip()


def slug(model: str) -> str:
    return model.replace("/", "_").replace(":", "_")


def main():
    parser = argparse.ArgumentParser(description="Tailor and critique a resume via OpenRouter.")
    parser.add_argument("-m", "--model", action="append", dest="models")
    parser.add_argument("--resume", default="resume.md")
    parser.add_argument("--job", default="job.md")
    parser.add_argument("--template", default="template.md")
    parser.add_argument("--critic", default=CRITIC_MODEL)
    parser.add_argument("--critique-only", action="store_true")
    args = parser.parse_args()

    api_key = os.environ.get("OPENROUTER_API_KEY")
    if not api_key:
        sys.exit("Set OPENROUTER_API_KEY first.")

    resume = read(args.resume)
    job = read(args.job)
    template = Path(args.template).read_text().strip() if Path(args.template).exists() else resume
    out = Path("drafts") / datetime.now().strftime("%Y-%m-%d_%H%M")
    out.mkdir(parents=True, exist_ok=True)

    if not args.critique_only:
        models = args.models or DEFAULT_MODELS
        print(f"Tailoring with {len(models)} models...\n")
        prompt = TAILOR_PROMPT.format(resume=resume, job=job, template=template)
        results = []
        with ThreadPoolExecutor(max_workers=len(models)) as pool:
            futs = {pool.submit(call, m, prompt, api_key): m for m in models}
            for f in as_completed(futs):
                results.append(f.result())
        for r in results:
            if "error" in r:
                print(f"  {r['model']}: ERROR {r['error']}")
                continue
            path = out / f"draft_{slug(r['model'])}.md"
            path.write_text(r["text"])
            cost = "n/a" if r["cost"] is None else f"${r['cost']:.4f}"
            print(f"  {r['model']}: {r['latency']:.1f}s, {cost} -> {path}")
        print()

    print(f"Critiquing with {args.critic}...\n")
    crit = call(args.critic, CRITIQUE_PROMPT.format(resume=resume, job=job), api_key)
    if "error" in crit:
        sys.exit(f"Critique failed: {crit['error']}")
    (out / "critique.md").write_text(crit["text"])
    print(crit["text"])
    print(f"\nSaved everything to {out}/")


if __name__ == "__main__":
    main()
