"""ask-gpt — one prompt to the owner's ChatGPT session, answer printed on stdout."""

from __future__ import annotations

import argparse
import datetime as dt
import http.client
import json
import os
import sys
import urllib.error
import urllib.request
from pathlib import Path

from chat import EFFORTS, OUT_DIR, ChatError, ask
from browser import MarionetteError, Session

DAEMON_PORT = int(os.environ.get("SOLWEBD_PORT", "8791"))
TOKEN_FILE = Path(os.environ.get("SOLWEBD_TOKEN_FILE",
                                 Path.home() / ".overdeck" / "gptbridge" / "solwebd.token"))


class DaemonError(RuntimeError):
    pass


def _post(url: str, token: str, payload: dict, timeout: float) -> dict:
    request = urllib.request.Request(
        url, data=json.dumps(payload).encode(), method="POST",
        headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"})
    with urllib.request.urlopen(request, timeout=timeout) as response:
        return json.loads(response.read() or b"{}")


def daemon_endpoint() -> str | None:
    """The daemon's base URL when it is up, else None.

    It holds `profile.lock` for its whole lifetime, so a direct session would block
    for 600s and then fail. A listening-but-unauthorised daemon is an ERROR, never a
    fallthrough — falling through is exactly the hang this probe exists to prevent.
    """
    base = f"http://127.0.0.1:{DAEMON_PORT}"
    if not TOKEN_FILE.exists():
        return None
    token = TOKEN_FILE.read_text().strip()
    request = urllib.request.Request(f"{base}/healthz",
                                     headers={"Authorization": f"Bearer {token}"})
    try:
        with urllib.request.urlopen(request, timeout=5) as response:
            return base if json.loads(response.read() or b"{}").get("ok") else None
    except urllib.error.HTTPError as exc:
        raise DaemonError(
            f"solwebd is running on {base} but rejected our token ({exc.code}); "
            f"it holds the browser profile — fix {TOKEN_FILE} or stop the daemon"
        ) from None
    except OSError:
        return None


def main(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(
        prog="ask-gpt",
        description="Send a prompt (and optional files) to ChatGPT and print the reply.",
    )
    ap.add_argument("prompt", nargs="*", help="the prompt; omit to read stdin")
    ap.add_argument("--effort", choices=EFFORTS, help="reasoning effort for this turn")
    ap.add_argument(
        "-a", "--attach", action="append", default=[], metavar="FILE",
        help="attach a file, image or document (repeatable)",
    )
    ap.add_argument("--out", type=Path, default=OUT_DIR,
                    help=f"directory for generated files and images (default {OUT_DIR})")
    ap.add_argument("--json", action="store_true", help="emit a JSON result object")
    ap.add_argument("--timeout", type=float, default=900.0,
                    help="seconds to wait for the reply")
    ap.add_argument("--mode", default="virtual", choices=Session.MODES,
                    help="virtual (invisible, default), headless, or show")
    ap.add_argument("--reseed", action="store_true",
                    help="re-copy the browser profile from the live one")
    args = ap.parse_args(argv)

    prompt = " ".join(args.prompt).strip() or sys.stdin.read().strip()
    if not prompt:
        ap.error("no prompt given")

    stamp = dt.datetime.now().strftime("%Y-%m-%d-%H%M%S")
    attach = [str(Path(p).expanduser().resolve()) for p in args.attach]
    out = str(Path(args.out).expanduser().resolve())
    try:
        base = daemon_endpoint()
        if base:
            try:
                # The daemon owns the browser; its cwd is not ours, hence absolute paths.
                result = _post(f"{base}/v1/ask", TOKEN_FILE.read_text().strip(), {
                    "prompt": prompt, "effort": args.effort, "attach": attach,
                    "out": out, "stamp": stamp, "timeout": args.timeout,
                }, timeout=args.timeout + 30)
                text, images, files, url = (
                    result.get("text", ""), result.get("images", []),
                    result.get("files", []), result.get("conversation", ""))
            except urllib.error.HTTPError:
                raise  # a real error response, not a broken connection
            except (OSError, http.client.HTTPException, ValueError) as exc:
                # the reply never arrived: connection reset, a body truncated after the
                # headers, or unparsable JSON. Its serve loop survives a handler
                # exception, so a still-listening daemon is still holding profile.lock
                # and the direct path would block on it for 600s.
                if daemon_endpoint():
                    raise DaemonError(
                        f"the request failed ({exc}) but solwebd is still listening on "
                        f"{base} and holds the browser profile; restart it"
                    ) from None
                print(f"ask-gpt: solwebd died mid-request ({exc}); "
                      f"retrying with a direct browser session", file=sys.stderr)
                base = None
        if base is None:
            reply = ask(
                prompt,
                effort=args.effort,
                attachments=[Path(p) for p in attach],
                out_dir=Path(out),
                stamp=stamp,
                mode=args.mode,
                reseed=args.reseed,
                timeout=args.timeout,
            )
            text, images, files, url = (
                reply.text, [str(p) for p in reply.images], [str(p) for p in reply.files],
                reply.url)
    except (ChatError, MarionetteError, DaemonError) as exc:
        print(f"ask-gpt: {exc}", file=sys.stderr)
        return 1
    except urllib.error.HTTPError as exc:
        detail = json.loads(exc.read() or b"{}").get("error", {}).get("message", exc.reason)
        print(f"ask-gpt: solwebd: {detail}", file=sys.stderr)
        return 1

    if args.json:
        print(json.dumps({
            "prompt": prompt,
            "text": text,
            "images": images,
            "files": files,
            "conversation": url,
        }, indent=2))
        return 0

    print(text)
    for path in [*images, *files]:
        print(f"\nsaved: {path}")
    return 0


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