import argparse
import contextvars
from functools import cache
import inspect
import os
from controllers.base import word_names, word_parameters
from controllers.types import CONTROLLERS
import features
from features.switches import generation
from features.session_briefing import start as briefing
from providers import DRIVERS
from engine.transcript import conversation, user
from resources.base import ACTORS, AGENT
from engine.version import version
from features.runtime_cleanup.details import RuntimeCleanupDetails
from features.runtime_cleanup.tidy import tidy
from commands.queries import attached, attic_text, decided, demo, ended, halt, healed, help_text, say, search_text, serve_forever, services, settings_text, speed, supervise, switched, transcript, upgrade_here, verify

argparse._ = str


@cache
def words(type_: str) -> set[str]:
    controller = CONTROLLERS[type_]
    return {controller.resource.command_names.get(name, name) for name in word_names(controller)}

def truthy(word: str) -> bool:
    return word.strip().lower() in ("true", "yes", "on", "1")

def add_method(acts, controller: type, name: str) -> None:
    a = acts.add_parser(controller.resource.command_names.get(name, name))
    a.set_defaults(method=name)
    for p in word_parameters(controller, name):
        required = p.default is inspect.Parameter.empty
        flag = p.name if required else f"--{p.name}"
        if p.kind is inspect.Parameter.VAR_KEYWORD:
            a.add_argument("--set", action="append", default=[], metavar="key=value")
        elif p.kind is inspect.Parameter.VAR_POSITIONAL:
            a.add_argument(p.name, nargs="*")
        elif isinstance(p.default, bool):
            a.add_argument(flag, action=argparse.BooleanOptionalAction, default=p.default)
        elif p.annotation is bool:
            a.add_argument(flag, action="store_true")
        elif p.annotation == bool | None:
            a.add_argument(flag, type=truthy, default=None)
        elif p.annotation in (list, list[int]):
            a.add_argument(flag, nargs="+", type=int)
        elif p.annotation == list[str]:
            a.add_argument(flag, nargs="+")
        else:
            a.add_argument(flag, type=int if p.annotation is int else str, default=None if required else p.default)

QUERIES: set[str] = set()


def add_query(cmds, name: str, help_: str, fn, *flags) -> None:
    QUERIES.add(name)
    q = cmds.add_parser(name, help=help_)
    q.set_defaults(query=fn)
    for flag, kw in flags:
        q.add_argument(flag, **kw)

class Misused(Exception):
    pass


PRINTED: contextvars.ContextVar = contextvars.ContextVar("printed", default=None)


class Parser(argparse.ArgumentParser):
    def error(self, message: str):
        raise Misused(f"{self.format_usage()}{self.prog}: error: {message}")

    def _print_message(self, message: str, file=None) -> None:
        super()._print_message(message, PRINTED.get() or file)


PARSERS: dict[tuple, argparse.ArgumentParser] = {}

DEFAULTS = ("JOURNAL_ROOT", "JOURNAL_ENV", "JOURNAL_ACTOR", "JOURNAL_SESSION", "JOURNAL_AGENT")

def parser(only: str = "") -> argparse.ArgumentParser:
    features.load()
    key = (only, generation(), *(os.environ.get(name, "") for name in DEFAULTS))
    if key not in PARSERS:
        PARSERS[key] = built(only)
    return PARSERS[key]

def built(only: str) -> argparse.ArgumentParser:
    top = Parser(prog="journal", description="the journal, every type a noun and every method its word")
    top.add_argument("--root", default=os.environ.get("JOURNAL_ROOT", ".journal"))
    top.add_argument("--env", dest="bound", default="")
    top.add_argument("--default-env", dest="fallback", default=os.environ.get("JOURNAL_ENV", ""), help=argparse.SUPPRESS)
    top.add_argument("--as", dest="as_actor", default=os.environ.get("JOURNAL_ACTOR", AGENT), choices=ACTORS)
    top.add_argument("--session", dest="as_session", default=os.environ.get("JOURNAL_SESSION") or os.environ.get(os.environ.get("JOURNAL_SESSION_VARIABLE", ""), ""))
    top.add_argument("--cwd", default="", help=argparse.SUPPRESS)
    top.add_argument("--agent", dest="as_agent", default=os.environ.get("JOURNAL_AGENT", ""))
    top.add_argument("--plugin", dest="as_plugin", default=os.environ.get("JOURNAL_PLUGIN", ""), help=argparse.SUPPRESS)
    cmds = top.add_subparsers(dest="_command", required=True)
    for type_, controller in CONTROLLERS.items():
        t = cmds.add_parser(type_, help=controller.resource.details.abstract, description=controller.resource.details.help)
        acts = t.add_subparsers(dest="action", required=True)
        for name in sorted(word_names(controller)) if not only or only == type_ else ():
            add_method(acts, controller, name)
    add_query(cmds, "status", "where things stand", lambda ctx: briefing.status(ctx["record"]))
    add_query(cmds, "carry", "everything standing, in full", lambda ctx: briefing.carry(ctx["record"]))
    add_query(cmds, "start", "what a session is handed at its start", lambda ctx: briefing.start_block(ctx["record"]))
    add_query(cmds, "open", "open work", lambda ctx: briefing.lines(briefing.open_work(ctx["record"])))
    add_query(cmds, "search", "everything the viewer's search finds: every kind of row, every agent transcript in this environment and attached files, grouped by type, or only the types named in --resources todo,message; --attic searches removed environments instead; --archived also lists archived rows",
              lambda ctx: attic_text(ctx["record"], ctx["term"]) if ctx["attic"] else search_text(ctx["record"], ctx["term"], ctx["page"], ctx["archived"], ctx["resources"]),
              ("term", {}), ("--page", {"type": int, "default": 0}), ("--attic", {"action": "store_true"}), ("--archived", {"action": "store_true"}), ("--resources", {"default": ""}))
    add_query(cmds, "conversation", "the stretch the last summary replaced", lambda ctx: say(conversation(transcript(ctx["record"], ctx["session"]), ctx["back"])),
              ("--back", {"type": int, "default": 1}))
    add_query(cmds, "user", "the user's own words, in full", lambda ctx: say(user(transcript(ctx["record"], ctx["session"]))))
    add_query(cmds, "nothing", "decide that nothing here needs pinning", lambda ctx: decided(ctx), ("why", {}))
    add_query(cmds, "version", "the version", lambda ctx: version())
    add_query(cmds, "enable", "the journal is in force again: the hooks report, the gate holds", lambda ctx: switched(ctx, on=True))
    add_query(cmds, "disable", "the kill switch — the hooks stay wired but report nothing and hold nothing, until enable", lambda ctx: switched(ctx, on=False))
    add_query(cmds, "verify", "wired and alive: the hooks in the agent's settings, the viewer, the engine, this session's last report", lambda ctx: verify(ctx))
    add_query(cmds, "settings", "every setting on this environment and where it is set", lambda ctx: settings_text(ctx))
    add_query(cmds, "help", "what one command does", lambda ctx: help_text(ctx["word"]), ("word", {"nargs": "?", "default": ""}))
    for name in DRIVERS:
        add_query(cmds, name, f"start {name} supervised, on this environment; everything after the word is forwarded to {name}", lambda ctx, name=name: supervise(ctx, name))
    add_query(cmds, "serve", "the web viewer", lambda ctx: serve_forever(ctx), ("--port", {"type": int, "default": 0}))
    add_query(cmds, "attach", "watch a session that runs without a terminal and type into it; Ctrl+] leaves it running",
              lambda ctx: attached(ctx), ("target", {}))
    add_query(cmds, "upgrade", "pull the package, wire the hooks, write the skills, run the migrations", lambda ctx: upgrade_here(ctx), ("--yes", {"action": "store_true"}),
              ("--to", {"default": "", "help": "install this released version instead of the newest"}))
    add_query(cmds, "stop", "stop this journal: its viewer, its engine and every service a plugin runs", lambda ctx: halt(ctx))
    add_query(cmds, "ended", "a session's agent has exited: put back what was set aside, and stop the journal when no session is left", lambda ctx: ended(ctx))
    add_query(cmds, "heal", "go back to the last build that started, when the one installed will not", lambda ctx: healed(ctx), ("--died", {"action": "store_true"}))
    add_query(cmds, "speed", "median milliseconds for lists, commands, a hook call and the viewer API, and the runtime folder's size; --board times a built journal of forty tickets instead", lambda ctx: speed(ctx),
              ("--runs", {"type": int, "default": 5}), ("--url", {"default": ""}), ("--out", {"default": ""}), ("--board", {"action": "store_true"}))
    add_query(cmds, "demo", "build a demo's data from a scrubbed recording, every moment answered by the real server",
              lambda ctx: demo(ctx), ("folder", {}), ("into", {}), ("--environment", {"default": ""}), ("--name", {"default": ""}))
    add_query(cmds, "tidy", "run the housekeeping now: trim captures and logs, drop quiet sessions' files", lambda ctx: tidy(ctx["record"].root, RuntimeCleanupDetails.values(ctx["record"]).days).summary)
    add_query(cmds, "services", "the services plugins run: list them, start, stop or restart one, read its log, or keep them up in this terminal with up",
              lambda ctx: services(ctx), ("action", {"nargs": "?", "default": "list"}), ("which", {"nargs": "?", "default": ""}), ("--lines", {"type": int, "default": 40}))
    return top
