#!/usr/bin/env python3
"""Ask Mouse-1 one four-option question about a task, and record the answer.

    bin/triage AUD-171            # what does this task need next
    bin/triage AUD-171 --ask redo # a different question set

Mouse-1 answers a typed multiple-choice question about a state you describe. It
does not write code, run tools, or decide whether work is finished. It labels a
situation, and the label is written on the task so a person can see why a machine
suggested something.

The cold start is real: the first call after an idle period answers 503 after
about thirty seconds, and the retry succeeds. This retries exactly once, says so,
and never pretends the first call worked.
"""

import json
import os
import sys
import time
import urllib.error
import urllib.request

sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "lib"))
import harness as h  # noqa: E402

ENDPOINT = "https://api.mouse.dev/v1/decisions"

QUESTIONS = {
    "next": {
        "instructions": "This Paperclip task has reached the state described. What does it need next?",
        "criteria": {
            "merge": "the work is complete, its checks pass, and it should be merged now",
            "person": "a person has to decide or look at it; a machine should not continue",
            "redo": "the work is wrong or incomplete, and the same agent should try again",
            "environment": "something outside the code is wrong: a missing tool, a service, a path or a permission",
        },
    },
    "failure": {
        "instructions": "This run failed. What does the failure need?",
        "criteria": {
            "redo": "the work is incomplete or wrong; another attempt with the same agent is likely to succeed",
            "stronger_model": "the task needs more capability than this model has",
            "environment": "something outside the code is wrong: a missing tool, a service, a path",
            "person": "a decision only the person can make",
        },
    },
}


def mouse_key():
    key = os.environ.get("MOUSE_API_KEY", "")
    if key:
        return key
    path = os.path.expanduser("~/.config/environment.d/10-secrets.conf")
    if os.path.isfile(path):
        for line in open(path):
            if line.startswith("MOUSE_API_KEY="):
                return line.split("=", 1)[1].strip().strip('"').strip("'")
    return ""


def ask(state, question):
    key = mouse_key()
    if not key:
        raise h.Refused(2, "no MOUSE_API_KEY in the environment or ~/.config/environment.d/10-secrets.conf")
    body = json.dumps({"model": "mouse-1", "state": state,
                       "questions": {"q": dict(type="choice", **question)}}).encode()
    for attempt in (1, 2):
        started = time.time()
        req = urllib.request.Request(ENDPOINT, data=body, method="POST",
                                     headers={"Authorization": "Bearer " + key,
                                              "Content-Type": "application/json"})
        try:
            with urllib.request.urlopen(req, timeout=180) as r:
                return json.loads(r.read().decode()), time.time() - started, attempt
        except urllib.error.HTTPError as e:
            detail = e.read().decode()[:300]
            if e.code == 503 and attempt == 1:
                h.say("  the model was idle; it takes minutes to start. retrying once…")
                continue
            raise h.Refused(3, "Mouse-1 answered %d: %s" % (e.code, detail))


def main():
    args = [a for a in sys.argv[1:] if not a.startswith("--")]
    if not args:
        h.say("usage: bin/triage <task> [--ask next|failure]")
        return 2
    which = "next"
    if "--ask" in sys.argv:
        which = sys.argv[sys.argv.index("--ask") + 1]
    if which not in QUESTIONS:
        raise h.Refused(2, "no question set called %r" % which)

    issue = h.find_issue(args[0])
    worktree, branch, base = (None, "", "")
    try:
        worktree, branch, base = h.worktree_of(issue)
    except h.Refused:
        pass

    runs = h.api("GET", "/api/issues/%s/runs" % issue["id"])
    last = runs[0] if runs else {}
    state = {
        "task": issue["identifier"] + ": " + issue["title"],
        "description": (issue.get("description") or "")[:1500],
        "status": issue.get("status"),
        "last_run": {k: last.get(k) for k in ("status", "invocationSource", "errorCode", "finishedAt")},
        "worktree": worktree or "none",
        "branch": branch or "none",
    }
    if not last.get("errorCode") and last.get("status") in ("failed", "cancelled"):
        last_status = last.get("status")
        state["last_run"]["status"] = last_status

    h.say("%s %s" % (issue["identifier"], issue["title"]))
    h.say("  asking Mouse-1: %s" % which)
    answer, seconds, attempt = ask(state, QUESTIONS[which])
    a = (answer.get("answers") or {}).get("q") or {}
    choice = a.get("choice", "?")
    confidence = a.get("confidence", 0)
    probs = a.get("probabilities") or {}

    h.say("  answer: %s (confidence %.2f, %.1fs, attempt %d)" % (choice, confidence, seconds, attempt))
    for option, p in sorted(probs.items(), key=lambda kv: -kv[1]):
        h.say("    %-14s %.2f  %s" % (option, p, QUESTIONS[which]["criteria"].get(option, "")))

    lines = ["## Triage (Mouse-1)", "",
             "Question: %s" % QUESTIONS[which]["instructions"], "",
             "- answer: **%s**" % choice,
             "- confidence: %.2f" % confidence,
             "- took %.1fs on attempt %d" % (seconds, attempt), "",
             "| option | probability | what it means |",
             "|---|---|---|"]
    for option, p in sorted(probs.items(), key=lambda kv: -kv[1]):
        lines.append("| `%s` | %.2f | %s |" % (option, p, QUESTIONS[which]["criteria"].get(option, "")))
    lines += ["", "The model labelled the situation. It did not change anything, and it does not decide a merge."]
    h.comment(issue["id"], "\n".join(lines))
    h.say("  written on the task as a comment")
    return 0


if __name__ == "__main__":
    h.main_guard(main)
