# piker: trading gear for hackers
# Copyright (C) 2018-present  Tyler Goodlet (in stewardship of piker0)

# 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/>.

"""
Broker high level cross-process API layer.

This API should be kept "remote service compatible" meaning inputs to
routines should be primitive data types where possible.
"""
import inspect
from types import ModuleType
from typing import (
    Any,
)

import trio

from piker.log import get_logger
from . import get_brokermod
from ..service import maybe_spawn_datad
from . import open_cached_client
from ..accounting import MktPair

log = get_logger(name=__name__)


async def api(brokername: str, methname: str, **kwargs) -> dict:
    '''
    Make (proxy through) a broker API call by name and return its result.

    '''
    brokermod = get_brokermod(brokername)
    async with brokermod.get_client() as client:
        meth = getattr(client, methname, None)
        if meth is None:
            log.debug(
                f"Couldn't find API method {methname} looking up on client")
            meth = getattr(client.api, methname, None)

        if meth is None:
            log.error(f"No api method `{methname}` could be found?")
            return

        if not kwargs:
            # verify kwargs requirements are met
            sig = inspect.signature(meth)
            if sig.parameters:
                log.error(
                    f"Argument(s) are required by the `{methname}` method: "
                    f"{tuple(sig.parameters.keys())}")
                return

        return await meth(**kwargs)


async def stocks_quote(
    brokermod: ModuleType,
    tickers: list[str]

) -> dict[str, dict[str, Any]]:
    '''
    Return a `dict` of snapshot quotes for the provided input
    `tickers`: a `list` of fqmes.

    '''
    async with brokermod.get_client() as client:
        return await client.quote(tickers)


# TODO: these need tests
async def option_chain(
    brokermod: ModuleType,
    symbol: str,
    date: str|None = None,
) -> dict[str, dict[str, dict[str, Any]]]:
    '''
    Return option chain for ``symbol`` for ``date``.

    By default all expiries are returned. If ``date`` is provided
    then contract quotes for that single expiry are returned.

    '''
    async with brokermod.get_client() as client:
        if date:
            id = int((await client.tickers2ids([symbol]))[symbol])
            # build contracts dict for single expiry
            return await client.option_chains(
                {(symbol, id, date): {}})
        else:
            # get all contract expiries
            # (takes a long-ass time on QT fwiw)
            contracts = await client.get_all_contracts([symbol])
            # return chains for all dates
            return await client.option_chains(contracts)


# async def contracts(
#     brokermod: ModuleType,
#     symbol: str,
# ) -> dict[str, dict[str, dict[str, Any]]]:
#     """Return option contracts (all expiries) for ``symbol``.
#     """
#     async with brokermod.get_client() as client:
#         # return await client.get_all_contracts([symbol])
#         return await client.get_all_contracts([symbol])


async def bars(
    brokermod: ModuleType,
    symbol: str,
    **kwargs,
) -> dict[str, dict[str, dict[str, Any]]]:
    '''
    Return option contracts (all expiries) for ``symbol``.

    '''
    async with brokermod.get_client() as client:
        return await client.bars(symbol, **kwargs)


async def search_w_brokerd(
    name: str,
    pattern: str,
) -> dict:

    # TODO: WHY NOT WORK!?!
    # when we `step` through the next block?
    # import tractor
    # await tractor.pause()
    async with open_cached_client(name) as client:

        # TODO: support multiple asset type concurrent searches.
        return await client.search_symbols(pattern=pattern)


async def symbol_search(
    brokermods: list[ModuleType],
    pattern: str,
    loglevel: str = 'warning',
    **kwargs,

) -> dict[str, dict[str, dict[str, Any]]]:
    '''
    Return symbol info from broker.

    '''
    results: list[str] = []

    async def search_backend(
        brokermod: ModuleType
    ) -> None:

        brokername: str = mod.name

        # TODO: figure this the FUCK OUT
        # -> ok so obvi in the root actor any async task that's
        # spawned outside the main tractor-root-actor task needs to
        # call this..
        # await tractor.devx._debug.maybe_init_greenback()
        # tractor.pause_from_sync()

        async with maybe_spawn_datad(
            mod.name,
            infect_asyncio=getattr(
                mod,
                '_infect_asyncio',
                False,
            ),
            loglevel=loglevel
        ) as portal:

            results.append((
                brokername,
                await portal.run(
                    search_w_brokerd,
                    name=brokername,
                    pattern=pattern,
                ),
            ))

    async with trio.open_nursery() as n:
        for mod in brokermods:
            n.start_soon(search_backend, mod.name)

    return results


async def mkt_info(
    brokermod: ModuleType,
    fqme: str,

    **kwargs,

) -> MktPair:
    '''
    Return the `piker.accounting.MktPair` info struct from a given
    backend broker tradable src/dst asset pair.

    '''
    async with open_cached_client(brokermod.name) as client:
        assert client
        return await brokermod.get_mkt_info(
            fqme.replace(brokermod.name, '')
        )
