from __future__ import annotations

import asyncio
import time
from typing import TYPE_CHECKING

from loguru import logger
from omu.api.plugin import PackageInfo
from omu.api.plugin.extension import (
    PLUGIN_ALLOWED_PACKAGE_TABLE,
    PLUGIN_RELOAD_ENDPOINT_TYPE,
    PLUGIN_REQUIRE_PACKET,
    ReloadOptions,
    ReloadResult,
)
from omu.app import AppType
from omu.errors import PermissionDenied
from omu.network.packet.packet_types import DisconnectType
from omu.plugin import UninstallContext
from omu.result import Err
from packaging.specifiers import SpecifierSet

from omuserver.session import Session

from .loader import DependencyResolver, PluginLoader
from .permissions import PLUGIN_MANAGE_PERMISSION, PLUGIN_READ_PERMISSION

if TYPE_CHECKING:
    from omuserver.server import Server
RESTART = True


class PluginExtension:
    def __init__(self, server: Server) -> None:
        self.allowed_packages = server.tables.register(PLUGIN_ALLOWED_PACKAGE_TABLE)
        server.security.register_permission(PLUGIN_MANAGE_PERMISSION, PLUGIN_READ_PERMISSION)
        server.packets.register(PLUGIN_REQUIRE_PACKET)
        server.tables.register(PLUGIN_ALLOWED_PACKAGE_TABLE)
        server.packets.bind(PLUGIN_REQUIRE_PACKET, self.handle_require)
        server.endpoints.bind(PLUGIN_RELOAD_ENDPOINT_TYPE, self.handle_reload)
        server.event.start += self.on_network_start
        server.event.stop += self.on_stop
        self.server = server
        self.request_id = 0
        self.lock = asyncio.Lock()
        self.dependency_resolver = DependencyResolver(server)
        self.loader = PluginLoader(server, self.dependency_resolver)

    async def on_network_start(self) -> None:
        await self.loader.load_plugins()

        for instance in self.loader.instances.values():
            await instance.start(self.server)

    async def on_stop(self) -> None:
        try:
            await self.loader.stop_plugins()
        except Exception as e:
            logger.opt(exception=e).error("Error stopping plugins")

    def _get_next_request_id(self) -> str:
        self.request_id += 1
        return f"{self.request_id}-{time.time_ns()}"

    async def open_request_plugin_dialog(self, session: Session, packages: dict[str, str | None]) -> None:
        to_request: list[PackageInfo] = []
        for package in packages.keys():
            package_info = await self.dependency_resolver.get_installed_package_info(package)
            if package_info is None:
                package_info = await self.dependency_resolver.fetch_package_info(package)
                to_request.append(package_info)
                continue
            await self.allowed_packages.add(package_info)
        if len(to_request) == 0:
            return
        accepted = await self.server.dashboard.request_plugins(app=session.app, packages=to_request)
        if not accepted:
            raise PermissionDenied("Plugin request was denied by the dashboard")

    async def handle_require(self, session: Session, requirements: dict[str, str | None]) -> None:
        if not requirements:
            return

        match self.dependency_resolver.is_requirements_satisfied(
            {k: SpecifierSet(v) if v else None for k, v in requirements.items()}
        ):
            case Err(err):
                await session.disconnect(
                    DisconnectType.INVALID_VERSION, f"Required plugin versions are not installed: {err}"
                )
                return

        if session.kind == AppType.REMOTE:
            await session.disconnect(DisconnectType.PERMISSION_DENIED, "Remote applications cannot require plugins")
            return

    async def handle_reload(self, session: Session, options: ReloadOptions) -> ReloadResult:
        instances = self.loader.instances

        if options.get("packages") is not None:
            filters = options["packages"] or []
            instances = {name: version for name, version in instances.items() if name in filters}

        for instance in instances.values():
            await instance.terminate(self.server)
            await instance.reload()
            await instance.start(self.server)

        return {"packages": {}}

    async def uninstall(self):
        await self.loader.load_plugins()

        for instance in self.loader.instances.values():
            ctx = UninstallContext(
                self.server,
                instance.entry.dist,
                plugin=instance.plugin,
            )
            await instance.notify_uninstall(ctx)
