#!/usr/bin/env python3
"""luna — command-line client for the Luna baby tracker (getmyluna.app).

Single file, standard library only. Install:

    curl -fsSLO https://getmyluna.app/cli/luna.py && chmod +x luna.py
    # or: python3 luna.py --help

Configure once (a token from Luna → Settings → Connect an assistant):

    export LUNA_TOKEN=...            # required for everything but `login`
    export LUNA_URL=https://getmyluna.app   # default

Or mint a token from your login:

    python3 luna.py login you@example.com          # prompts for the password

Examples:

    luna status                       # asleep/awake, hunger, next feed
    luna today                        # today's events
    luna events --date 2026-09-19
    luna log milk 150                 # ml
    luna log breast
    luna log solid "scrambled egg" toast --meal snack
    luna log sleep / luna log wake
    luna log nappy wet|dirty|both
    luna log note "rolled over"
    luna log weight 8.4 / luna log length 72.5 / luna log vitd 10
    luna undo
    luna profile
    luna export > family.json
    luna api GET /api/vega/patterns   # any endpoint; see /developers

Every command prints JSON (pass --pretty for indentation). Exit status is
non-zero on any HTTP error, with the server's `error` on stderr.
"""
from __future__ import annotations

import argparse
import getpass
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from http.cookiejar import CookieJar
from typing import Any, Callable

__version__ = "1.0.0"

DEFAULT_URL = "https://getmyluna.app"
USER_AGENT = f"luna-cli/{__version__}"

#: ``request(method, path, json_body, headers) -> (status, headers, body_bytes)``.
#: Injectable so the test-suite can drive the CLI against an in-process app.
Transport = Callable[[str, str, Any, dict], tuple[int, dict, bytes]]


class LunaError(Exception):
    def __init__(self, status: int, error: str, body: Any = None):
        super().__init__(f"{status}: {error}")
        self.status, self.error, self.body = status, error, body


def _urllib_transport(cookies: CookieJar | None = None) -> Transport:
    opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookies or CookieJar()))

    def _request(method: str, url: str, body: Any, headers: dict) -> tuple[int, dict, bytes]:
        data = None
        hdrs = {"User-Agent": USER_AGENT, "Accept": "application/json", **headers}
        if body is not None:
            if isinstance(body, dict) and hdrs.get("Content-Type") == "application/x-www-form-urlencoded":
                data = urllib.parse.urlencode(body).encode()
            else:
                data = json.dumps(body).encode()
                hdrs["Content-Type"] = "application/json"
        req = urllib.request.Request(url, data=data, method=method, headers=hdrs)
        try:
            with opener.open(req, timeout=30) as resp:
                return resp.status, dict(resp.headers), resp.read()
        except urllib.error.HTTPError as e:
            return e.code, dict(e.headers), e.read()

    return _request


class Client:
    def __init__(self, base_url: str = DEFAULT_URL, token: str | None = None,
                 transport: Transport | None = None):
        self.base_url = base_url.rstrip("/")
        self.token = token
        self.cookies = CookieJar()
        self._transport = transport or _urllib_transport(self.cookies)

    def call(self, method: str, path: str, body: Any = None, *, form: bool = False) -> Any:
        headers: dict = {}
        if self.token:
            headers["Authorization"] = f"Bearer {self.token}"
        if form:
            headers["Content-Type"] = "application/x-www-form-urlencoded"
        status, _hdrs, raw = self._transport(method, f"{self.base_url}{path}", body, headers)
        text = raw.decode("utf-8", "replace") if raw else ""
        try:
            data = json.loads(text) if text else None
        except json.JSONDecodeError:
            data = {"raw": text}
        if status >= 400:
            err = (data or {}).get("error") if isinstance(data, dict) else None
            raise LunaError(status, err or f"HTTP {status}", data)
        return data

    # ── convenience wrappers (one per documented endpoint the CLI exposes) ──
    def status(self) -> dict:
        return self.call("GET", "/api/vega/live")

    def today(self) -> dict:
        return self.call("GET", "/api/vega/today")

    def events(self, date: str | None = None) -> dict:
        q = f"?date={urllib.parse.quote(date)}" if date else ""
        return self.call("GET", f"/api/vega/events{q}")

    def log(self, event: dict) -> dict:
        return self.call("POST", "/api/vega/log", event)

    def undo(self) -> dict:
        return self.call("POST", "/api/vega/undo", {})

    def profile(self) -> dict:
        return self.call("GET", "/api/vega/profile")

    def export(self) -> dict:
        return self.call("GET", "/api/vega/export")

    def whoami(self) -> dict:
        return self.call("GET", "/api/me")

    def login_and_mint(self, email: str, password: str, label: str = "luna-cli") -> dict:
        """Sign in with the web form (sets the session cookie), then mint a token."""
        self._transport("POST", f"{self.base_url}/login",
                        {"username": email, "password": password, "next": "/vega"},
                        {"Content-Type": "application/x-www-form-urlencoded"})
        try:
            self.call("GET", "/api/me")  # 401 here means the form re-rendered with an error
        except LunaError:
            raise LunaError(401, "login failed: check email and password")
        return self.call("POST", "/api/vega/token", {"label": label})


# ── argument parsing ────────────────────────────────────────────────────────

def build_event(kind: str, values: list[str], at: str | None, note: str | None, meal: str | None) -> dict:
    """Translate `luna log <kind> ...` into a `POST /api/vega/log` body."""
    ev: dict[str, Any] = {}
    if kind == "milk":
        if not values:
            raise SystemExit("usage: luna log milk <ml>")
        ev.update(type="milk", ml=int(values[0]))
    elif kind == "breast":
        ev.update(type="milk", ml=0, note="breastfeed" if not note else f"breastfeed: {note}")
        note = None
    elif kind == "solid":
        if not values:
            raise SystemExit("usage: luna log solid <food> [food ...]")
        ev.update(type="solid", foods=values, meal_type=meal or "meal")
    elif kind in ("sleep", "wake"):
        ev.update(type=kind)
    elif kind == "nappy":
        ev.update(type="nappy", kind=(values[0] if values else "wet"))
    elif kind == "note":
        if not values:
            raise SystemExit("usage: luna log note <text>")
        ev.update(type="note", note=" ".join(values))
        note = None
    elif kind == "weight":
        ev.update(type="weight", kg=float(values[0]))
    elif kind == "length":
        ev.update(type="length", cm=float(values[0]))
    elif kind in ("vitd", "iron", "multivitamin"):
        ev.update(type="supplement", kind=kind)
        if values:
            ev["ug"] = int(float(values[0]))
    else:
        raise SystemExit(f"unknown event kind: {kind!r} (milk, breast, solid, sleep, wake, nappy, note, weight, length, vitd, iron, multivitamin)")
    if at:
        ev["ts"] = at
    if note:
        ev["note"] = note
    return ev


def make_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(prog="luna", description="Command-line client for the Luna baby tracker.")
    p.add_argument("--url", default=os.environ.get("LUNA_URL", DEFAULT_URL), help="Luna base URL (env LUNA_URL)")
    p.add_argument("--token", default=os.environ.get("LUNA_TOKEN"),
                   help="Bearer token (env LUNA_TOKEN). Use --token=VALUE if the token starts with '-'")
    p.add_argument("--pretty", action="store_true", help="Indent JSON output")
    p.add_argument("--version", action="version", version=f"luna-cli {__version__}")
    sub = p.add_subparsers(dest="command", required=True)

    def add(name: str, **kw) -> argparse.ArgumentParser:
        # Accept the global flags after the subcommand too (`luna today --pretty`).
        # SUPPRESS defaults leave the top-level values alone unless given here.
        sp = sub.add_parser(name, **kw)
        sp.add_argument("--pretty", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
        sp.add_argument("--token", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
        sp.add_argument("--url", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
        return sp

    add("status", help="Live status: asleep/awake, hunger, next feed")
    add("today", help="Today's events")
    ev = add("events", help="Events for a day")
    ev.add_argument("--date", help="YYYY-MM-DD (default today)")
    lg = add("log", help="Log an event")
    lg.add_argument("kind", help="milk|breast|solid|sleep|wake|nappy|note|weight|length|vitd|iron|multivitamin")
    lg.add_argument("values", nargs="*", help="amount / foods / nappy kind / note text")
    lg.add_argument("--at", help="ISO local time, e.g. 2026-09-20T14:30 (default now)")
    lg.add_argument("--note", help="Optional note")
    lg.add_argument("--meal", choices=("meal", "snack"), help="solid only")
    add("undo", help="Remove the most recent event")
    add("profile", help="Child profile and family settings")
    add("export", help="Full family export as JSON")
    add("whoami", help="Check which user/family the token resolves to")
    lo = add("login", help="Sign in with email + password and mint a token")
    lo.add_argument("email")
    lo.add_argument("--password", help="Prompted if omitted")
    lo.add_argument("--label", default="luna-cli")
    api = add("api", help="Call any endpoint: luna api GET /api/vega/patterns")
    api.add_argument("method", choices=("GET", "POST", "DELETE"))
    api.add_argument("path")
    api.add_argument("--json", dest="body", help="Request body as a JSON string")
    return p


def run(argv: list[str], transport: Transport | None = None, out=None, err=None) -> int:
    out, err = out or sys.stdout, err or sys.stderr
    args = make_parser().parse_args(argv)
    client = Client(args.url, args.token, transport=transport)
    try:
        if args.command == "login":
            password = args.password or getpass.getpass("Password: ")
            result = client.login_and_mint(args.email, password, label=args.label)
            print(f"export LUNA_TOKEN={result['token']}", file=err)
        elif args.command == "status":
            result = client.status()
        elif args.command == "today":
            result = client.today()
        elif args.command == "events":
            result = client.events(args.date)
        elif args.command == "log":
            result = client.log(build_event(args.kind, args.values, args.at, args.note, args.meal))
        elif args.command == "undo":
            result = client.undo()
        elif args.command == "profile":
            result = client.profile()
        elif args.command == "export":
            result = client.export()
        elif args.command == "whoami":
            result = client.whoami()
        elif args.command == "api":
            body = json.loads(args.body) if args.body else ({} if args.method != "GET" else None)
            result = client.call(args.method, args.path, body)
        else:  # pragma: no cover
            raise SystemExit(2)
    except LunaError as e:
        print(f"luna: {e.error} (HTTP {e.status})", file=err)
        if e.status == 401 and not client.token:
            print("hint: set LUNA_TOKEN, or run `luna login <email>`", file=err)
        return 1
    print(json.dumps(result, indent=2 if args.pretty else None, ensure_ascii=False), file=out)
    return 0


def main() -> None:
    sys.exit(run(sys.argv[1:]))


if __name__ == "__main__":
    main()
