"""
Command tree support for MoaT commands
"""

from __future__ import annotations

from moat.util import NotGiven, attrdict
from moat.lib.micro import (
    AC_use,
    L,
    Lock,
    TaskGroup,
)
from moat.lib.rpc import BaseCmd

# Typing

from typing import TYPE_CHECKING, cast  # isort:skip

if TYPE_CHECKING:
    from contextlib import AbstractAsyncContextManager

    from moat.lib.micro import _TaskGroupProto
    from moat.lib.path import PathElem
    from moat.lib.rpc import BaseMsgHandler

    from collections.abc import Awaitable, Callable


class BaseSuperCmd(BaseCmd):
    """
    A handler that can have a nested app (or more than one).

    Sets up a taskgroup for the sub-app(s) to run in.
    """

    tg: object | None = None
    app_lock: Lock | None = None

    async def setup(self) -> None:
        "setup apps"
        await super().setup()
        self.app_lock = Lock()
        tg = await AC_use(self, cast("AbstractAsyncContextManager[object]", TaskGroup()))
        self.tg = tg
        await AC_use(self, cast("_TaskGroupProto", tg).cancel)

    async def start_app(self, app: BaseCmd) -> None:
        """
        Run (or reload) this app.
        """

        async def _run(app):
            try:
                await app.run()
            finally:
                app.p_task = None

        if self.app_lock is None:
            raise RuntimeError("No lock")
        tg = self.tg
        if tg is None:
            raise RuntimeError("No taskgroup")
        tg = cast("_TaskGroupProto", tg)
        async with self.app_lock:
            if app.p_task:
                return
            try:

                async def _run_app() -> None:
                    await _run(app)

                t = await tg.spawn(_run_app)
                if app.p_task is False:
                    # set by .stop()
                    t.cancel()
                    app.p_task = None
                    return

                app.p_task = t
                if L:
                    await app.wait_started()
            except BaseException:
                app.p_task = None
                raise


class BaseSubCmd(BaseSuperCmd):
    """
    A handler for a directory.

    Apps have a hierarchical structure. This class serves as the equivalent
    of a subdirectory.

    How to create new entries is not specified in this class.
    """

    sub: dict[str, BaseCmd]

    def __init__(self, cfg):
        super().__init__(cfg)
        self.sub = {}

    if L:

        async def wait_ready(self, wait=True):
            """Delay until this subtree is up,

            Returns True if all sub-apps are stopped.
            """
            res = await super().wait_ready(wait=wait)
            if res is None:
                return None
            for app in list(self.sub.values()):
                if (w := await app.wait_ready(wait=wait)) is None:
                    return None
                res &= w
            return res

    async def attach(self, name, app) -> None:
        """
        Attach a sub-handler to me.

        An existing handler with this name is stopped.
        """
        oa = self.sub.pop(name, None)
        if app is not None:
            self.sub[name] = app
            app.attached(self, name)
        if oa is not None:
            await oa.stop()

    def detach(self, name) -> Awaitable:
        """
        Detach and stop a command handler.
        """
        return self.attach(name, None)

    async def reload(self):
        "reload apps"
        root = self.root
        if root is None:
            raise RuntimeError("No root")
        root.cfg_reloaded(self.cfg)

        await super().reload()
        async with TaskGroup() as tg:
            for app in list(self.sub.values()):
                tg.start_soon(app.reload)

    def find_sub(self, scmd: PathElem) -> BaseMsgHandler | Callable[..., object] | None:
        """
        Resolve a subcommand.

        This version uses the ``sub`` mapping.
        """
        if isinstance(scmd, str) and (sub := self.sub.get(scmd, None)) is not None:
            return sub

        return super().find_sub(scmd)

    doc_dir_ = dict(
        _d="list cmd subdirectory",
        d="dict(str,name):sub-apps",
        c=["str:commands"],
        v="bool:show hidden",
    )

    async def cmd_dir_(self, v=True):
        "dir: add subdirs"
        res = await super().cmd_dir_(v=v)
        dd = {}
        for k, v in self.sub.items():
            if not isinstance(k, str) or v is (k[-1] == "_"):
                continue
            try:
                dd[k] = v.cfg["app"]
            except (AttributeError, KeyError):
                dd[k] = v.__class__.__name__
        if dd:
            res["d"] = dd
        return res


class DirCmd(BaseSubCmd):
    """
    A BaseSubCmd handler with apps started by local configuration.

    Not typically subclassed.
    """

    doc = dict(_c=dict(_d="subdirectory", _n="app:sub-apps"))

    SKIP_RDY = True

    def __init__(self, cfg):
        super().__init__(cfg)
        self._lock = Lock()

    async def reload(self):
        "called after the config has been updated"
        async with self._lock:
            await super().reload()
            await self._setup_apps()

    cmd_upd_ = reload

    async def task(self):
        "Setup apps"
        await self._setup_apps()
        await super().task()

    async def _setup_apps(self):
        from moat.lib.rpc import LoadCmd  # noqa: PLC0415

        # log("Setup %s: %s", self, self.path)
        gcfg = self.cfg
        root = self.root
        if root is None:
            raise RuntimeError("No root")
        root.cfg_reloaded(gcfg)

        apps = {}
        for k, v in gcfg.items():
            try:
                if not v.get("running", True):
                    continue
                nam = v.get("app", None)
                if not isinstance(nam, str):
                    raise ValueError(f"Bad Config: app-{k}")  # noqa:TRY004
                apps[k] = v
            except (AttributeError, KeyError):
                continue

        # Zeroth, kill apps that are no longer live
        for name in list(self.sub.keys()):
            if name not in apps:
                await self.detach(name)

        # First, setup the app data structures
        for name, v in apps.items():
            if name in self.sub:
                continue
            if v is NotGiven:
                continue

            cfg = gcfg.get(name, attrdict())
            await self.attach(name, LoadCmd(cfg))

        # Second, run them all.
        # For existing apps, tell it to update its configuration.
        for app in self.sub.values():
            async with TaskGroup() as tg:

                async def _start(app: BaseCmd = app) -> None:
                    await self.start_app(app)

                tg.start_soon(_start)

        # Third, wait for them to be up.
        if L:
            for app in self.sub.values():
                if app.cfg.get("wait", True):
                    await app.wait_ready()

        # Finally, mark done.
        if L:
            self.set_ready()
