#!/usr/bin/env python3
"""
council-review — anonymized peer review of a draft by multiple LLMs via OpenRouter.

Usage:
  council-review <file>                       # review a file
  cat draft.md | council-review               # review stdin
  council-review <file> --focus "security"    # add focus areas
  council-review <file> --type writing        # 'writing' | 'plan' | 'argument' | 'general'
  council-review <file> -o review.md          # explicit output path
  council-review <file> --models a,b,c        # override council for this run

Model management:
  council-review --list-latest                # show newest candidates per family
  council-review --refresh-models             # interactively repick the pinned council

Env: OPENROUTER_API_KEY must be set.
Config: ~/.council-review.config.json (created by --refresh-models)
Default output: ./.council-review.md (overwritten each run)
"""
import argparse
import concurrent.futures as cf
import json
import os
import sys
import time
import urllib.error
import urllib.request

# Hardcoded fallback if no config file exists. Overridden by ~/.council-review.config.json.
DEFAULT_MODELS = [
    "openai/gpt-5.6-sol",
    "google/gemini-3.1-pro-preview",
    "x-ai/grok-4.5",
    "deepseek/deepseek-v4-pro",
    "moonshotai/kimi-k3",
    "anthropic/claude-opus-5",
]

# Families surfaced by --list-latest and --refresh-models. This list must be a SUPERSET of every
# family anyone has pinned: --refresh-models rebuilds the council from scratch over these families
# only, so a family missing here is a seat silently deleted on the next repick.
DEFAULT_FAMILIES = [
    "openai", "google", "x-ai", "anthropic",
    "deepseek", "moonshotai", "minimax", "z-ai", "qwen", "mistralai",
]


def families_for_picking(pinned: list[str]) -> list[str]:
    """DEFAULT_FAMILIES plus any family the user has actually pinned, so a repick can never
    drop a seat just because its vendor was missing from the hardcoded list."""
    fams = list(DEFAULT_FAMILIES)
    for m in pinned:
        fam = family_of(m)
        if fam != "?" and fam not in fams:
            fams.append(fam)
    return fams

CONFIG_PATH = os.path.expanduser("~/.council-review.config.json")

REVIEW_PROMPT = """You are an anonymous peer reviewer. Critique the following {type} rigorously.

Focus areas: {focus}

Your job is to find what's wrong, weak, missing, or overconfident. Be specific and direct.
Do not praise. Do not summarize. Do not soften. If it's solid, say so briefly and move on.

Structure your response in exactly this format:

## Issues
- [severity: high|med|low] <one-line issue> — <one sentence explanation>
- ...

## Missing
- <what isn't there but should be>
- ...

## Overconfidence
- <claims or assumptions that need more evidence or are too strong>
- ...

## Verdict
<2-3 sentences: is this fit for purpose? what's the biggest risk?>

---

The {type} to review:

{content}
"""


# ----- OpenRouter API helpers ----------------------------------------------

def call_openrouter(model: str, prompt: str, api_key: str, timeout: int = 120) -> str:
    req = urllib.request.Request(
        "https://openrouter.ai/api/v1/chat/completions",
        data=json.dumps({
            "model": model,
            "messages": [{"role": "user", "content": prompt}],
            "reasoning": {"effort": "medium"},
        }).encode(),
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
            "HTTP-Referer": "https://github.com/local/council-review",
            "X-Title": "council-review",
        },
    )
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            data = json.loads(r.read())
        return data["choices"][0]["message"]["content"]
    except urllib.error.HTTPError as e:
        return f"[ERROR calling {model}: HTTP {e.code} — {e.read().decode()[:300]}]"
    except Exception as e:
        return f"[ERROR calling {model}: {type(e).__name__}: {e}]"


def fetch_available_models(api_key: str | None = None, timeout: int = 30) -> list[dict]:
    """Hits OpenRouter /models. No auth required, but pass key if available."""
    headers = {}
    if api_key:
        headers["Authorization"] = f"Bearer {api_key}"
    req = urllib.request.Request("https://openrouter.ai/api/v1/models", headers=headers)
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return json.loads(r.read()).get("data", [])


# ----- Config persistence --------------------------------------------------

def load_pinned_models() -> list[str]:
    if os.path.exists(CONFIG_PATH):
        try:
            with open(CONFIG_PATH) as f:
                cfg = json.load(f)
            models = cfg.get("models")
            if isinstance(models, list) and models:
                return models
        except Exception as e:
            print(f"⚠ Could not parse {CONFIG_PATH}: {e}. Using hardcoded defaults.",
                  file=sys.stderr)
    return DEFAULT_MODELS


def save_pinned_models(models: list[str]) -> None:
    with open(CONFIG_PATH, "w") as f:
        json.dump({"models": models, "updated_at": int(time.time())}, f, indent=2)
    print(f"Saved {len(models)} models to {CONFIG_PATH}", file=sys.stderr)


# ----- Freshness check -----------------------------------------------------

def verify_models(pinned: list[str], available_ids: set[str]) -> tuple[list[str], list[str]]:
    """Returns (survivors, missing)."""
    survivors, missing = [], []
    for m in pinned:
        (survivors if m in available_ids else missing).append(m)
    return survivors, missing


# ----- --list-latest / --refresh-models ------------------------------------

def family_of(model_id: str) -> str:
    return model_id.split("/", 1)[0] if "/" in model_id else "?"


def fmt_price(p: str | None) -> str:
    try:
        per_million = float(p) * 1_000_000
        return f"${per_million:.2f}/M"
    except (TypeError, ValueError):
        return "?"


def candidates_per_family(models: list[dict], families: list[str], top_n: int = 8) -> dict:
    """Group models by family, sort newest-first."""
    by_family: dict[str, list[dict]] = {f: [] for f in families}
    for m in models:
        fam = family_of(m.get("id", ""))
        if fam in by_family:
            by_family[fam].append(m)
    for fam in by_family:
        by_family[fam].sort(key=lambda x: x.get("created", 0), reverse=True)
        by_family[fam] = by_family[fam][:top_n]
    return by_family


def print_candidates(by_family: dict) -> None:
    for fam, models in by_family.items():
        print(f"\n=== {fam} ===")
        if not models:
            print("  (no models found)")
            continue
        for i, m in enumerate(models, 1):
            mid = m.get("id", "?")
            ctx = m.get("context_length", "?")
            pricing = m.get("pricing", {}) or {}
            in_p = fmt_price(pricing.get("prompt"))
            out_p = fmt_price(pricing.get("completion"))
            print(f"  {i:2}. {mid}")
            print(f"      ctx: {ctx}  in: {in_p}  out: {out_p}")


def cmd_list_latest() -> int:
    api_key = os.environ.get("OPENROUTER_API_KEY")
    print("Fetching OpenRouter model catalog...", file=sys.stderr)
    models = fetch_available_models(api_key)
    by_family = candidates_per_family(models, families_for_picking(load_pinned_models()), top_n=8)
    print_candidates(by_family)
    print("\nTo update the pinned council, run: council-review --refresh-models")
    return 0


def cmd_refresh_models() -> int:
    api_key = os.environ.get("OPENROUTER_API_KEY")
    print("Fetching OpenRouter model catalog...", file=sys.stderr)
    models = fetch_available_models(api_key)
    current = load_pinned_models()
    by_family = candidates_per_family(models, families_for_picking(current), top_n=8)

    print(f"\nCurrent pinned council: {current}\n")
    print("For each family you want represented, pick a model by number, or")
    print("press Enter to skip that family. Type 'q' at any time to cancel.\n")

    picked: list[str] = []
    for fam, fam_models in by_family.items():
        if not fam_models:
            continue
        print(f"\n=== {fam} ===")
        for i, m in enumerate(fam_models, 1):
            mid = m.get("id", "?")
            pricing = m.get("pricing", {}) or {}
            in_p = fmt_price(pricing.get("prompt"))
            out_p = fmt_price(pricing.get("completion"))
            marker = "  ← currently pinned" if mid in current else ""
            print(f"  {i:2}. {mid}  (in: {in_p}, out: {out_p}){marker}")
        try:
            choice = input(f"Pick for {fam} (1-{len(fam_models)}, Enter to skip, q to cancel): ").strip()
        except EOFError:
            choice = ""
        if choice.lower() == "q":
            print("Cancelled. No changes written.", file=sys.stderr)
            return 1
        if not choice:
            continue
        try:
            idx = int(choice) - 1
            if 0 <= idx < len(fam_models):
                picked.append(fam_models[idx]["id"])
            else:
                print(f"  Out of range, skipping {fam}.")
        except ValueError:
            print(f"  Not a number, skipping {fam}.")

    if not picked:
        print("\nNo models picked. Config not changed.", file=sys.stderr)
        return 1

    print(f"\nNew pinned council: {picked}")
    dropped = [m for m in current if m not in picked]
    if dropped:
        print("⚠ These seats are being REMOVED from the council:")
        for m in dropped:
            print(f"    - {m}")
    confirm = input("Write to config? [y/N]: ").strip().lower()
    if confirm != "y":
        print("Cancelled. No changes written.", file=sys.stderr)
        return 1
    save_pinned_models(picked)
    return 0


# ----- Main review flow ----------------------------------------------------

def run_review(args, api_key: str) -> int:
    if args.file:
        with open(args.file) as f:
            content = f.read()
        source = args.file
    else:
        if sys.stdin.isatty():
            sys.exit("ERROR: no file given and stdin is empty.")
        content = sys.stdin.read()
        source = "<stdin>"

    if not content.strip():
        sys.exit("ERROR: input is empty.")

    if args.models:
        models = [m.strip() for m in args.models.split(",")]
        print(f"council-review: using --models override ({len(models)} reviewers)", file=sys.stderr)
    else:
        pinned = load_pinned_models()
        if not args.skip_freshness_check:
            try:
                print("council-review: verifying pinned models exist...", file=sys.stderr)
                catalog = fetch_available_models(api_key)
                available_ids = {m.get("id") for m in catalog}
                survivors, missing = verify_models(pinned, available_ids)
                if missing:
                    print("⚠ Some pinned models are no longer listed on OpenRouter:",
                          file=sys.stderr)
                    for m in missing:
                        print(f"  - {m}", file=sys.stderr)
                    print("  Run: council-review --refresh-models to update.", file=sys.stderr)
                if not survivors:
                    sys.exit("ERROR: no pinned models remain available. Run --refresh-models.")
                models = survivors
            except Exception as e:
                print(f"⚠ Freshness check failed ({e}). Proceeding with pinned models as-is.",
                      file=sys.stderr)
                models = pinned
        else:
            models = pinned

    prompt = REVIEW_PROMPT.format(type=args.type, focus=args.focus, content=content)
    print(f"council-review: sending to {len(models)} reviewers...", file=sys.stderr)
    t0 = time.time()
    with cf.ThreadPoolExecutor(max_workers=len(models)) as ex:
        futures = {ex.submit(call_openrouter, m, prompt, api_key): m for m in models}
        results = {}
        for fut in cf.as_completed(futures):
            model = futures[fut]
            results[model] = fut.result()
            print(f"  ✓ {model} ({time.time()-t0:.1f}s)", file=sys.stderr)

    ordered = sorted(results.items(), key=lambda kv: kv[0])
    labels = [chr(ord("A") + i) for i in range(len(ordered))]
    mapping = {label: model for label, (model, _) in zip(labels, ordered)}

    out = []
    out.append("# Council Review")
    out.append(f"\n_Source: `{source}` · Type: `{args.type}` · Focus: {args.focus}_\n")
    out.append("## Reviewer Identity Key\n")
    out.append("_Anonymized during review; revealed here for transparency._\n")
    for label, model in mapping.items():
        out.append(f"- **Reviewer {label}** → `{model}`")
    out.append("\n---\n")
    for label, (model, review) in zip(labels, ordered):
        out.append(f"## Reviewer {label}\n")
        out.append(review.strip())
        out.append("\n---\n")

    with open(args.output, "w") as f:
        f.write("\n".join(out))

    print(f"council-review: wrote {args.output} ({time.time()-t0:.1f}s total)", file=sys.stderr)
    return 0


def main() -> int:
    p = argparse.ArgumentParser(description="Anonymized multi-LLM peer review.")
    p.add_argument("file", nargs="?", help="File to review (stdin if omitted)")
    p.add_argument("--focus", default="anything that matters", help="Focus areas")
    p.add_argument("--type", default="document",
                   choices=["document", "writing", "plan", "argument", "code-plan", "general"])
    p.add_argument("-o", "--output", default=".council-review.md")
    p.add_argument("--models", help="Comma-separated OpenRouter model IDs (overrides pinned)")
    p.add_argument("--skip-freshness-check", action="store_true",
                   help="Skip the per-run check that pinned models still exist")
    p.add_argument("--list-latest", action="store_true",
                   help="Show newest models per family on OpenRouter and exit")
    p.add_argument("--refresh-models", action="store_true",
                   help="Interactively repick the pinned council and write config")
    args = p.parse_args()

    if args.list_latest:
        return cmd_list_latest()
    if args.refresh_models:
        return cmd_refresh_models()

    api_key = os.environ.get("OPENROUTER_API_KEY")
    if not api_key:
        sys.exit("ERROR: OPENROUTER_API_KEY env var is not set.")
    # Fail FAST on a key that can't possibly work (a placeholder/stub shadowing the real export produced
    # four silent per-reviewer 401s once — 2026-07-16). OpenRouter keys are 'sk-or-...' and long.
    if not api_key.startswith("sk-or-") or len(api_key) < 40:
        sys.exit(
            f"ERROR: OPENROUTER_API_KEY looks invalid (length {len(api_key)}, "
            f"starts with {api_key[:5]!r}...). Expected an 'sk-or-' key of 40+ chars.\n"
            "Check for a DUPLICATE export shadowing the real one:  grep -n OPENROUTER_API_KEY ~/.zshrc ~/.zshenv"
        )

    if not args.file and sys.stdin.isatty():
        p.print_help()
        return 1

    return run_review(args, api_key)


if __name__ == "__main__":
    sys.exit(main())
