"""
A command that accesses a row of mostly-identical subcommands
"""

from __future__ import annotations

from moat.util import attrdict, combine_dict, set_part
from moat.lib.codec.errors import NoPathError
from moat.lib.micro import L, TaskGroup
from moat.lib.path import Path
from moat.lib.rpc import MsgSender, ShortCommandError, load_app

from .tree.dir import BaseSuperCmd

from typing import TYPE_CHECKING  # isort:skip

if TYPE_CHECKING:
    from moat.lib.path import PathElem
    from moat.lib.rpc import Msg

    from collections.abc import Awaitable


class ArrayCmd(BaseSuperCmd):
    """
    A command that hosts a number of mostly-identical subcommands.
    """

    n: int | None = None

    doc = dict(
        _c=dict(
            _d="subdirectory", cfg="cfg:sub-app", n="int:nr.apps", _n="dict:cfg update for app #i"
        )
    )

    def __init__(self, cfg):
        super().__init__(cfg)
        self.apps = []

    async def setup(self):  # noqa:D102
        await super().setup()
        await self._setup_apps()

    if L:

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

            Returns True if any sub-apps are stopped.
            """
            await super().wait_ready(wait=wait)
            again = True
            res = False
            while again:
                again = False
                for app in self.apps:
                    if (w := await app.wait_ready(wait=wait)) is None:
                        if not wait:
                            return None
                        again = True
                        res = None
                    elif w is True:
                        return True
            return res

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

        An existing handler with this name is stopped.
        """
        try:
            oa = self.apps[name]
        except IndexError:
            if len(self.apps) != name:
                raise
            self.apps.append(app)
            oa = None
        else:
            self.apps[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)

    def _cfg(self, i):
        cfg = combine_dict(self.cfg.get("cfg", {}), self.cfg.get(i, {}), cls=attrdict)
        if (ii := self.cfg.get("i", None)) is not None:
            set_part(cfg, ii, i + self.cfg.get("i_off", 0))
        return cfg

    async def reload(self):  # noqa:D102
        await super().reload()
        self.n = self.cfg["n"]
        for i, app in enumerate(self.apps):
            app.cfg.merge(self._cfg(i))
            await app.reload()
        while len(self.apps) > self.n:
            app = self.apps.pop()
            await self.detach(len(self.apps))

    async def _setup_apps(self):
        name = self.cfg["cfg"]["app"]
        cls = load_app(name)

        self.n = self.cfg["n"]
        for i in range(self.n):
            app = cls(self._cfg(i))
            await self.attach(i, app)

        for app in self.apps:
            await self.start_app(app)

    async def handle(self, msg: Msg, rcmd: list[PathElem]):
        """
        Dispatch a message.

        Subcommands:

        * ``!`` handles the command directly.
        * ``*`` addresses all subcommands and returns an array/stream
                of the results.
        * ``n`` (integer) addresses the *n*th subcommand.

        See :meth:`moat.lib.rpc.MsgHandler.handle` for further details.
        """
        if not rcmd:
            raise ShortCommandError(msg.cmd)
        if isinstance(rcmd[-1], str) and rcmd[-1] == "!":
            rcmd.pop()
            return await super().handle(msg, rcmd)

        cmd = rcmd.pop()
        if cmd == "all":
            if msg.can_stream:
                return await self._stream_all(msg, rcmd)
            else:
                return await self._cmd_all(msg, rcmd)

        if not isinstance(cmd, int):
            raise NoPathError(
                self.path,
                msg.cmd,
                self.__class__.__name__,
                await self.cmd_dir_(v=None),
            ) from None

        try:
            sub = self.apps[cmd]
        except (TypeError, IndexError):
            raise NoPathError(
                self.path,
                msg.cmd,
                self.__class__.__name__,
                await self.cmd_dir_(v=None),
            ) from None
        return await sub.handle(msg, rcmd)

    doc_dir_ = dict(na="int:max index")

    async def cmd_dir_(self, v=True):
        "report max index"
        res = await super().cmd_dir_(v=v)
        res["na"] = len(self.apps)
        return res

    doc_all = dict(
        _d="apply to all",
        _0="path:command",
        _99="list:args",
        s="int:start index",
        e="int:end index",
    )

    async def _cmd_all(self, msg, rcmd):
        """
        Call all sub-apps and collect the result.
        """
        if rcmd:
            cmd = rcmd[:]
            cmd.reverse()
        else:
            cmd = msg.args_l.pop(0)
            if isinstance(cmd, str):
                cmd = [cmd]
            else:
                cmd = list(cmd)

        res = []
        snd = MsgSender(self)
        for app in self.apps:
            snd.set_root(app)
            r = await snd.cmd(Path.build(cmd), *msg.args, **msg.kw)
            res.append((r.args, r.kw))
        await msg.result(*res)

    async def _stream_all(self, msg, rcmd):
        """
        Call all sub-apps and send the results.
        """
        from moat.lib.rpc import Msg  # noqa:PLC0415

        if not rcmd:
            cmd = msg.args.pop(0)
            if isinstance(cmd, str):
                cmd = [cmd]
            else:
                cmd = list(cmd)
            cmd.reverse()
            rcmd = cmd

        async def _reply(i, app, st):
            msg_ = Msg.Call(msg.cmd, msg.args, msg.kw)
            res = await app.handle(msg_, rcmd[:])
            await st.send(i, *res.args, **res.kw)

        async with msg.stream_out() as st, TaskGroup() as tg:
            for i, app in enumerate(self.apps):

                async def _run(i=i, app=app) -> None:
                    await _reply(i, app, st)

                tg.start_soon(_run)

        await msg.result()
