"""Phase 6b — generate failure samples (tired/confused) and append to persona_train.jsonl.

Borrows CANONICAL_SYSTEM_PROMPT, failure prompts, and validation from gen_data.py.
Runs independently so we can iterate on failure prompting without touching the
persona generation pipeline.

Usage:
    python append_failures.py

Environment:
    INFERENCE_ROUTER_URL  (default: http://inference.kvmx.ru)
"""
import json
import os
import sys
import random
import re
from itertools import cycle
from pathlib import Path

HERE = Path(__file__).resolve().parent
DATA = HERE / "data"

random.seed(42)

# steal the canonical system prompt and failure prompts from gen_data.py
sys.path.insert(0, str(HERE))
import gen_data  # noqa: E402

CANONICAL_SYSTEM_PROMPT = gen_data.CANONICAL_SYSTEM_PROMPT
FAILURE_PROMPTS_TIRED = gen_data.FAILURE_PROMPTS_TIRED
FAILURE_PROMPTS_CONFUSED = gen_data.FAILURE_PROMPTS_CONFUSED
VALID_MOODS = gen_data.VALID_MOODS
validate_sample = gen_data.validate_sample

# ── router config ────────────────────────────────────────────────────────
ROUTER_URL = os.environ.get(
    "INFERENCE_ROUTER_URL", "http://inference.kvmx.ru"
).rstrip("/")

# providers to cycle through — brief list of models that work well for RU
ROUTER_PAIRS = [
    # format: (provider, model) — X-Provider header + bare model name
    ("cerebras", "gemma-4-31b"),
    ("cerebras", "zai-glm-4.7"),
    ("github_models", "models/meta/llama-4"),
    ("github_models", "models/mistral/mistral-large-3"),
    ("mistral", "mistral-large-latest"),
    ("mistral", "codestral-latest"),
]


# ── router call ──────────────────────────────────────────────────────────
def _chat(provider: str, model: str, msgs: list,
          max_tokens: int = 200, temp: float = 0.7) -> str | None:
    import time as _time
    import urllib.request as _req
    import urllib.error as _err

    base = ROUTER_URL
    if not base.endswith("/v1"):
        base += "/v1"
    url = f"{base}/chat/completions"
    headers = {"Content-Type": "application/json"}
    headers["X-Provider"] = provider

    payload = json.dumps({
        "model": model,
        "messages": msgs,
        "max_tokens": max_tokens,
        "temperature": temp,
    }).encode()

    for attempt in range(3):
        try:
            r = _req.Request(url, data=payload, headers=headers)
            with _req.urlopen(r, timeout=30) as resp:
                body = json.loads(resp.read())
                content = (body["choices"][0]["message"]["content"] or "").strip()
            if content.startswith("```"):
                start = content.find("\n") + 1
                end = content.rfind("```")
                if end > start:
                    content = content[start:end].strip()
            return content
        except _err.HTTPError as e:
            _time.sleep(min(2 ** attempt, 8))
        except Exception:
            _time.sleep(1)
    return None


def _parse_asst(content: str) -> dict | None:
    try:
        obj = json.loads(content)
    except json.JSONDecodeError:
        return None
    if not isinstance(obj, dict):
        return None
    if "response" not in obj or "mood" not in obj:
        return None
    if obj["mood"] not in VALID_MOODS:
        return None
    if not isinstance(obj["response"], str):
        return None
    return obj


def flatten_msgs(user_text: str, asst_text: str) -> list[dict]:
    return [
        {"role": "system", "content": CANONICAL_SYSTEM_PROMPT},
        {"role": "user", "content": user_text},
        {"role": "assistant", "content": asst_text},
    ]


# ── generation ───────────────────────────────────────────────────────────
def gen_failures(count: int) -> list[dict]:
    """Generate tired + confused samples, return list of validated dicts."""
    samples = []
    providers = cycle(ROUTER_PAIRS)

    # tired: 2/3 of count
    tired_target = count * 2 // 3
    pool_tired = FAILURE_PROMPTS_TIRED * (tired_target // len(FAILURE_PROMPTS_TIRED) + 1)
    random.shuffle(pool_tired)

    print(f"  tired: targeting {tired_target} samples...")
    done = 0
    for prompt_text in pool_tired[:tired_target]:
        provider, model = next(providers)
        msgs = [
            {"role": "system", "content": CANONICAL_SYSTEM_PROMPT},
            {"role": "user", "content": (
                f"{prompt_text}\n\n"
                f"(Это вопрос, на который у тебя нет ответа. "
                f"Не выдумывай. Честно скажи, что не знаешь или не можешь "
                f"ответить. Используй mood: tired. Только tired, не neutral, "
                f"не confused, не thinking — именно tired.)"
            )},
        ]
        content = _chat(provider, model, msgs, max_tokens=200, temp=0.7)
        if content is None:
            continue
        asst_obj = _parse_asst(content)
        if asst_obj is None or asst_obj["mood"] != "tired":
            continue

        asst_text = json.dumps(asst_obj, ensure_ascii=False)
        training_msgs = flatten_msgs(prompt_text, asst_text)
        err = validate_sample(training_msgs)
        if err:
            continue
        samples.append({
            "messages": training_msgs,
            "mood": "tired",
            "response": asst_obj["response"],
            "source": "generated/failure",
        })
        done += 1
        if done % 25 == 0:
            print(f"    ...{done}/{tired_target} tired valid")

    print(f"  tired: {done} valid")

    # confused: 1/3 of count
    confused_target = count // 3
    pool_confused = FAILURE_PROMPTS_CONFUSED * (confused_target // len(FAILURE_PROMPTS_CONFUSED) + 1)
    random.shuffle(pool_confused)

    print(f"  confused: targeting {confused_target} samples...")
    done = 0
    for prompt_text in pool_confused[:confused_target]:
        provider, model = next(providers)
        msgs = [
            {"role": "system", "content": CANONICAL_SYSTEM_PROMPT},
            {"role": "user", "content": (
                f"{prompt_text}\n\n"
                f"(Это неясный или слишком общий запрос. Ответь коротким "
                f"уточняющим вопросом. Используй mood: confused. Только "
                f"confused, не neutral, не tired, не thinking — именно confused.)"
            )},
        ]
        content = _chat(provider, model, msgs, max_tokens=200, temp=0.7)
        if content is None:
            continue
        asst_obj = _parse_asst(content)
        if asst_obj is None or asst_obj["mood"] != "confused":
            continue

        asst_text = json.dumps(asst_obj, ensure_ascii=False)
        training_msgs = flatten_msgs(prompt_text, asst_text)
        err = validate_sample(training_msgs)
        if err:
            continue
        samples.append({
            "messages": training_msgs,
            "mood": "confused",
            "response": asst_obj["response"],
            "source": "generated/failure",
        })
        done += 1
        if done % 25 == 0:
            print(f"    ...{done}/{confused_target} confused valid")

    print(f"  confused: {done} valid")
    return samples


def main():
    # target: 750 failure samples (15% of 5000)
    target = 750
    print(f"Generating up to {target} failure samples via router...")
    print(f"  Router: {ROUTER_URL}")
    print(f"  Models: {[f'{p}/{m}' for p,m in ROUTER_PAIRS]}")

    samples = gen_failures(target)
    print(f"\nGot {len(samples)} valid failure samples")

    if not samples:
        print("No samples generated — nothing to append.")
        return

    # dedup by response text against existing train data
    train_path = DATA / "persona_train.jsonl"
    if train_path.exists():
        existing_responses: set[str] = set()
        with open(train_path) as f:
            for line in f:
                try:
                    d = json.loads(line)
                    msg = d.get("messages", [])
                    if len(msg) >= 3:
                        asst_raw = json.loads(msg[2]["content"])
                        existing_responses.add(asst_raw.get("response", "").strip().lower())
                except (json.JSONDecodeError, KeyError, IndexError):
                    pass
        before = len(samples)
        samples = [s for s in samples if s["response"].strip().lower() not in existing_responses]
        print(f"Dedup removed {before - len(samples)} (already in train file)")

    # dedup within new samples
    seen: set[str] = set()
    deduped = []
    for s in samples:
        key = s["response"].strip().lower()
        if key not in seen:
            seen.add(key)
            deduped.append(s)
    samples = deduped

    # append to persona_train.jsonl
    with open(train_path, "a", encoding="utf-8") as f:
        for s in samples:
            f.write(json.dumps({"messages": s["messages"]}, ensure_ascii=False) + "\n")

    print(f"Appended {len(samples)} failure samples to {train_path}")
    print(f"Mood distribution: tired={sum(1 for s in samples if s['mood']=='tired')}, "
          f"confused={sum(1 for s in samples if s['mood']=='confused')}")


if __name__ == "__main__":
    main()
