from typing import TypeVar, Type, Callable, Any
from types import ModuleType

from neovim.msgpack_rpc import MsgpackStream, AsyncSession, Session
from neovim.api import Nvim
from neovim.msgpack_rpc.event_loop.base import BaseEventLoop
from neovim.msgpack_rpc.event_loop.uv import UvEventLoop

from amino import Either, _, L, Maybe, amino_log, Logger, __, Path, Map, List, Boolean, Nil, Just
from amino.either import ImportFailure
from amino.func import Val
from amino.logging import amino_root_file_logging
from amino.do import do, Do
from amino.dat import Dat
from amino.algebra import Algebra
from amino.state import EitherState, MaybeState
from amino.json import dump_json
from amino.boolean import false
from amino.util.string import decode
from amino.mod import instance_from_module, cls_from_module

from ribosome import NvimPlugin
from ribosome import options
from ribosome.components.scratch import Mapping
from ribosome.config import Config
from ribosome.dispatch.data import Legacy, SendMessage, Trans, Internal, DispatchSync, DispatchAsync, Dispatch, DIO
from ribosome.dispatch.execute import execute_dispatch_job, request_error, execute_io
from ribosome.dispatch.resolve import ComponentResolver
from ribosome.dispatch.run import DispatchJob
from ribosome.logging import ribo_log, nvim_logging
from ribosome.nvim import NvimFacade, NvimIO
from ribosome.plugin import plugin_class_from_config
from ribosome.plugin_state import PluginState, PluginStateHolder, DispatchConfig
from ribosome.request.handler.dispatcher import MsgDispatcher
from ribosome.request.handler.handler import RequestHandler
from ribosome.request.handler.prefix import Full
from ribosome.request.rpc import rpc_handler_functions, define_handlers, RpcHandlerSpec
from ribosome.trans.api import trans
from ribosome.trans.messages import ShowLogInfo, UpdateState, Quit, Stage1
from ribosome.trans.queue import PrioQueue
from ribosome.nvim.io import NS
from ribosome.trans.handler import TransComplete

Loop = TypeVar('Loop', bound=BaseEventLoop)
NP = TypeVar('NP', bound=NvimPlugin)
D = TypeVar('D')
B = TypeVar('B')
C = TypeVar('C', bound=Config)
R = TypeVar('R')
DP = TypeVar('DP', bound=Dispatch)
RDP = TypeVar('RDP', bound=Algebra)


class HostConfig(Dat['HostConfig']):

    def __init__(
            self,
            sync_dispatch: Map[str, DispatchSync],
            async_dispatch: Map[str, DispatchAsync],
            config: DispatchConfig,
            plugin_class: Maybe[Type[NP]],
    ) -> None:
        self.sync_dispatch = sync_dispatch
        self.async_dispatch = async_dispatch
        self.config = config
        self.plugin_class = plugin_class

    @property
    def name(self) -> str:
        return self.config.name

    @property
    def dispatch(self) -> List[Dispatch]:
        return self.sync_dispatch.v + self.async_dispatch.v

    @property
    def specs(self) -> List[RpcHandlerSpec]:
        return self.dispatch / __.spec(self.config.name, self.config.prefix)


def dispatch_job(sync: bool,
                 dispatches: Map[str, RDP],
                 state: PluginStateHolder[D],
                 name: str,
                 prefix: str,
                 args: tuple) -> DispatchJob:
    decoded_args = decode(args)
    fun_args = decoded_args.head | Nil
    bang = decoded_args.lift(1).contains(1)
    return DispatchJob(dispatches, state, decode(name), fun_args, sync, prefix, bang)


def request_handler(vim: NvimFacade,
                    sync: bool,
                    dispatches: Map[str, RDP],
                    state: PluginStateHolder[D],
                    config: Config) -> Callable[[str, tuple], Any]:
    sync_prefix = '' if sync else 'a'
    def handle(name: str, args: tuple) -> Do:
        job = dispatch_job(sync, dispatches, state, name, config.prefix, args)
        amino_log.debug(f'dispatching {sync_prefix}sync request: {job.name}({job.args})')
        result = (
            execute_dispatch_job(job)
            .attempt(vim)
            .value_or(L(request_error)(job, _))
        )
        if sync:
            ribo_log.debug(f'request `{job.name}` completed: {result}')
        return vim.encode_vim_data(result)
    return handle


@do(NvimIO[PluginState[D]])
def init_state(host_config: HostConfig) -> Do:
    dispatch_config = host_config.config
    data = dispatch_config.config.state()
    components = yield ComponentResolver(dispatch_config.config).run
    plugin = yield NvimIO.delay(host_config.plugin_class)
    log_handler = yield NvimIO.delay(nvim_logging)
    yield NvimIO.pure(PluginState.cons(dispatch_config, data, plugin, components, PrioQueue.empty, Nil, Just(log_handler)))


# TODO can vim be injected into each request handling process?
@do(NvimIO[int])
def run_session(session: Session, host_config: HostConfig) -> Do:
    yield define_handlers(host_config.specs, host_config.name, host_config.name)
    state = yield init_state(host_config)
    holder = PluginStateHolder.cons(state)
    yield NvimIO.delay(
        lambda vim:
        session.run(
            request_handler(vim, True, host_config.sync_dispatch, holder, host_config.config),
            request_handler(vim, False, host_config.async_dispatch, holder, host_config.config)
        )
    )
    yield NvimIO.pure(0)


def run_loop(session: Session, prefix: str, host_config: HostConfig) -> int:
    vim = NvimFacade(Nvim.from_session(session), host_config.name)
    return run_session(session, host_config).attempt(vim).get_or_raise()


def session(*args: str, loop: Loop=UvEventLoop, transport_type: str='stdio', **kwargs: str) -> Session:
    return Session(AsyncSession(MsgpackStream(loop(transport_type, *args, **kwargs))))


def start_host(prefix: str, host_config: HostConfig) -> int:
    return run_loop(session(), prefix, host_config)


def plugin_cls_from_module(mod: ModuleType) -> Either[str, Type[NP]]:
    return cls_from_module(mod, NvimPlugin)


def config_from_module(mod: ModuleType) -> Either[str, Type[C]]:
    return instance_from_module(mod, Config)


log_initialized = False


def nvim_log() -> Logger:
    global log_initialized
    if not log_initialized:
        NvimFacade.stdio_with_logging('ribosome_start_host')
        log_initialized = True
    return ribo_log


def plugin_class_dispatchers(cls: Type[NP]) -> List[DispatchSync]:
    return rpc_handler_functions(cls) / Legacy


def config_dispatchers(config: Config) -> List[DispatchAsync]:
    def choose(name: str, handler: RequestHandler) -> DispatchAsync:
        tpe = (
            SendMessage
            if isinstance(handler.dispatcher, MsgDispatcher) else
            Internal
            if handler.internal else
            Trans
        )
        return tpe(handler)
    return config.request_handlers.handlers.map2(choose)


@trans.free.result(trans.st)
@do(EitherState[PluginState[D], str])
def message_log() -> Do:
    yield EitherState.inspect_f(__.message_log.traverse(dump_json, Either))


@trans.free.unit(trans.st)
def set_log_level(self, level: str) -> None:
    handler = yield MaybeState.inspect_f(_.file_log_handler)
    handler.setLevel(level)


# TODO change remaining handlers to trans
message_log_handler = RequestHandler.trans_function(message_log)('message_log', Full(), internal=True, sync=True)
set_log_level_handler = RequestHandler.trans_function(set_log_level)(prefix=Full(), internal=True)
show_log_info_handler = RequestHandler.msg_cmd(ShowLogInfo)('show_log_info', prefix=Full(), internal=True)
update_state_handler = RequestHandler.json_msg_cmd(UpdateState)('update_state', prefix=Full())
mapping_handler = RequestHandler.msg_fun(Mapping)('mapping', prefix=Full())
stage_1_handler = RequestHandler.msg_cmd(Stage1)('stage_1', prefix=Full())
quit_handler = RequestHandler.msg_cmd(Quit)(prefix=Full())


def internal_dispatchers(config: Config) -> List[Dispatch]:
    return List(
        Internal(message_log_handler),
        Internal(set_log_level_handler),
        SendMessage(show_log_info_handler),
        SendMessage(update_state_handler),
        SendMessage(mapping_handler),
        SendMessage(stage_1_handler),
        SendMessage(quit_handler),
    )


def host_config_1(
        config: Config,
        cls: Type[NP],
        debug: Boolean,
        io_executor: Callable[[DIO], NS[PluginState[D], TransComplete]]=None,
) -> HostConfig:
    name = config.name
    cls_dispatchers = plugin_class_dispatchers(cls)
    cfg_dispatchers = config_dispatchers(config)
    int_dispatchers = internal_dispatchers(config)
    with_method = lambda ds: Map(ds.map(lambda d: (d.spec(config.name, config.prefix).rpc_method(name), d)))
    dispatches = int_dispatchers + cfg_dispatchers + cls_dispatchers
    sync_dispatch, async_dispatch = dispatches.filter(_.sync), dispatches.filter(_.async)
    dispatch_config = DispatchConfig.cons(config, io_executor)
    return HostConfig(with_method(sync_dispatch), with_method(async_dispatch), dispatch_config, cls)


class SyntheticPlugin:
    pass


def host_config(
        config: Config,
        plugin_mixin: Either[str, Type[NP]],
        debug: Boolean=false,
        io_executor: Callable[[DIO], NS[PluginState[D], TransComplete]]=None,
) -> HostConfig:
    sup = plugin_mixin | Val(SyntheticPlugin)
    cls = plugin_class_from_config(config, sup, debug)
    return host_config_1(config, cls, debug, io_executor)


def start_config_stage_2(sup: Either[str, Type[NP]], config: Config) -> int:
    debug = options.development.exists
    amino_log.debug(f'starting plugin from {config}, debug: {debug}')
    return start_host(config.name, host_config(config, sup, debug))


def error(msg: str) -> int:
    amino_log.error(msg)
    nvim_log().error(msg)
    return 1


def import_error(e: ImportFailure, desc: str) -> int:
    return error(e.expand.join_lines)


def exception(e: Exception, desc: str) -> int:
    f = __.caught_exception_error(f'starting host from {desc}', e)
    f(amino_log)
    f(nvim_log())
    return 1


def start_config_stage_1(mod: ModuleType) -> int:
    nvim_log()
    plugin_cls = plugin_cls_from_module(mod)
    return config_from_module(mod).cata(error, L(start_config_stage_2)(plugin_cls, _))


def setup_log() -> None:
    amino_root_file_logging()


def start_from(source: str, importer: Callable[[str], Either[ImportFailure, ModuleType]], desc: str) -> int:
    try:
        setup_log()
        amino_log.debug(f'start_{desc}: {source}')
        return importer(source).cata(L(import_error)(_, source), start_config_stage_1)
    except Exception as e:
        return exception(e, source)


def start_module(mod: str) -> int:
    return start_from(mod, Either.import_module, 'module')


def start_file(path: str) -> int:
    p = Path(path)
    file = p / '__init__.py' if p.is_dir() else p
    return start_from(str(file), Either.import_file, 'file')


__all__ = ('start_host', 'start_module', 'start_file')
