# piker: trading gear for hackers
# Copyright (C) 2018-present Tyler Goodlet
# (in stewardship for pikers, everywhere.)

# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero General Public
# License as published by the Free Software Foundation, either
# version 3 of the License, or (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public
# License along with this program.  If not, see
# <https://www.gnu.org/licenses/>.

'''
CLI commons.

'''
import os
# from contextlib import AsyncExitStack
from types import ModuleType

import click
import trio
import tractor
from tractor._multiaddr import parse_maddr

from ..log import (
    get_console_log,
    get_logger,
    colorize_json,
)
from ..brokers import get_brokermod
from ..service import (
    _default_registry_host,
    _default_registry_port,
)
from .. import config


log = get_logger('piker.cli')


def load_trans_eps(
    network: dict | None = None,
    maddrs: list[tuple] | None = None,

) -> dict[str, dict[str, dict]]:

    # transport-oriented endpoint multi-addresses
    eps: dict[
        str,  # service name, eg. `pikerd`, `emsd`..

        # libp2p style multi-addresses parsed into prot layers
        list[dict[str, str | int]]
    ] = {}

    if (
        network
        and not maddrs
    ):
        # load network section and (attempt to) connect all endpoints
        # which are reachable B)
        for key, maddrs in network.items():
            match key:

                # TODO: resolve table across multiple discov
                # prots Bo
                case 'resolv':
                    pass

                case 'pikerd':
                    dname: str = key
                    for maddr in maddrs:
                        layers: dict = parse_maddr(maddr)
                        eps.setdefault(
                            dname,
                            [],
                        ).append(layers)

    elif maddrs:
        # presume user is manually specifying the root actor ep.
        eps['pikerd'] = [parse_maddr(maddr)]

    return eps


@click.command()
@click.option(
    '--loglevel',
    '-l',
    default='warning',
    help='Logging level',
)
@click.option(
    '--tl',
    is_flag=True,
    help='Enable tractor-runtime logs',
)
@click.option(
    '--pdb',
    is_flag=True,
    help='Enable tractor debug mode',
)
@click.option(
    '--maddr',
    '-m',
    default=None,
    help='Multiaddrs to bind or contact',
)
# @click.option(
#     '--tsdb',
#     is_flag=True,
#     help='Enable local ``marketstore`` instance'
# )
# @click.option(
#     '--es',
#     is_flag=True,
#     help='Enable local ``elasticsearch`` instance'
# )
def pikerd(
    maddr: list[str] | None,
    loglevel: str,
    tl: bool,
    pdb: bool,
    # tsdb: bool,
    # es: bool,
):
    '''
    Spawn the piker broker-daemon.

    '''
    # from tractor.devx import maybe_open_crash_handler
    # with maybe_open_crash_handler(pdb=False):
    log = get_console_log(
        level=loglevel,
        with_tractor_log=tl,
    )

    if pdb:
        log.warning((
            "\n"
            "!!! YOU HAVE ENABLED DAEMON DEBUG MODE !!!\n"
            "When a `piker` daemon crashes it will block the "
            "task-thread until resumed from console!\n"
            "\n"
        ))

    # service-actor registry endpoint socket-address set
    regaddrs: list[tuple[str, int]] = []

    conf, _ = config.load(
        conf_name='conf',
    )
    network: dict = conf.get('network')
    if (
        network is None
        and not maddr
    ):
        regaddrs = [(
            _default_registry_host,
            _default_registry_port,
        )]

    else:
        eps: dict = load_trans_eps(
            network,
            maddr,
        )
        for layers in eps['pikerd']:
            regaddrs.append((
                layers['ipv4']['addr'],
                layers['tcp']['port'],
            ))

    from .. import service

    async def main():
        service_mngr: service.Services
        async with (
            service.open_pikerd(
                registry_addrs=regaddrs,
                loglevel=loglevel,
                debug_mode=pdb,
                # enable_transports=['uds'],
                enable_transports=['tcp'],
            ) as service_mngr,
        ):
            assert service_mngr
            # ?TODO? spawn all other sub-actor daemons according to
            # multiaddress endpoint spec defined by user config
            await trio.sleep_forever()

    trio.run(main)


@click.group(context_settings=config._context_defaults)
@click.option(
    '--brokers', '-b',
    default=None,
    multiple=True,
    help='Broker backend to use'
)
@click.option('--loglevel', '-l', default='warning', help='Logging level')
@click.option('--tl', is_flag=True, help='Enable tractor logging')
@click.option('--configdir', '-c', help='Configuration directory')
@click.option(
    '--pdb',
    is_flag=True,
    help='Enable runtime debug mode ',
)
@click.option(
    '--maddr',
    '-m',
    default=None,
    multiple=True,
    help='Multiaddr to bind',
)
@click.option(
    '--regaddr',
    '-r',
    default=None,
    help='Registrar addr to contact',
)
@click.pass_context
def cli(
    ctx: click.Context,
    brokers: list[str],
    loglevel: str,
    tl: bool,
    configdir: str,
    pdb: bool,

    # TODO: make these list[str] with multiple -m maddr0 -m maddr1
    maddr: list[str],
    regaddr: str,

) -> None:
    if configdir is not None:
        assert os.path.isdir(configdir), f"`{configdir}` is not a valid path"
        config._override_config_dir(configdir)

    # TODO: for typer see
    # https://typer.tiangolo.com/tutorial/commands/context/
    ctx.ensure_object(dict)

    if not brokers:
        # (try to) load all (supposedly) supported data/broker backends
        from piker.brokers import __brokers__
        brokers = __brokers__

    brokermods: dict[str, ModuleType] = {
        broker: get_brokermod(broker) for broker in brokers
    }
    assert brokermods

    # TODO: load endpoints from `conf::[network].pikerd`
    # - pikerd vs. regd, separate registry daemon?
    # - expose datad vs. brokerd?
    # - bind emsd with certain perms on public iface?
    regaddrs: list[tuple[str, int]] = regaddr or [(
        _default_registry_host,
        _default_registry_port,
    )]

    # TODO: factor [network] section parsing out from pikerd
    # above and call it here as well.
    # if maddr:
    #     for addr in maddr:
    #         layers: dict = parse_maddr(addr)

    ctx.obj.update({
        'brokers': brokers,
        'brokermods': brokermods,
        'loglevel': loglevel,
        'tractorloglevel': None,
        'log': get_console_log(loglevel),
        'confdir': config._config_dir,
        'wl_path': config._watchlists_data_path,
        'registry_addrs': regaddrs,
        'pdb': pdb,  # debug mode flag

        # TODO: endpoint parsing, pinging and binding
        # on no existing server.
        # 'maddrs': maddr,
    })

    # allow enabling same loglevel in ``tractor`` machinery
    if tl:
        ctx.obj.update({'tractorloglevel': loglevel})


@cli.command()
@click.option('--tl', is_flag=True, help='Enable tractor logging')
@click.argument('ports', nargs=-1, required=False)
@click.pass_obj
def services(
    config,
    tl: bool,
    ports,
):

    from ..service import (
        open_piker_runtime,
        _default_registry_port,
        _default_registry_host,
    )

    host = _default_registry_host
    if not ports:
        ports = [_default_registry_port]

    addr = tractor._addr.wrap_address(
        addr=(host, ports[0])
    )

    async def list_services():
        nonlocal host
        async with (
            open_piker_runtime(
                name='service_query',
                loglevel=(
                    config['loglevel']
                    if tl
                    else None,
                ),
            ),
            tractor.get_registry(
                addr=addr,
            ) as portal
        ):
            registry = await portal.run_from_ns(
                'self',
                'get_registry',
            )
            json_d = {}
            for key, socket in registry.items():
                json_d[key] = f'{socket}'

            click.echo(f"{colorize_json(json_d)}")

    trio.run(list_services)


def _load_clis() -> None:
    # from ..service import elastic  # noqa
    from ..brokers import cli  # noqa
    from ..ui import cli  # noqa
    from ..watchlists import cli  # noqa

    # typer implemented
    from ..storage import cli  # noqa
    from ..accounting import cli  # noqa


# load downstream cli modules
_load_clis()
