"""Unified sync/async Omni client.

Sync methods use plain names.
Async methods use an `a` prefix for seamless DX switching.
"""

from __future__ import annotations

import asyncio
from collections.abc import AsyncIterator
from pathlib import Path
from typing import Any

from omni.client.async_client import AsyncOmniClient
from omni.http.methods import RpcMethod


class _SyncRunner:
    """Persistent sync runner to keep all sync calls on a single event loop."""

    def __init__(self) -> None:
        self._runner = asyncio.Runner()
        self._closed = False

    def run(self, coro: Any) -> Any:
        if self._closed:
            raise RuntimeError("sync client is closed")

        try:
            asyncio.get_running_loop()
        except RuntimeError:
            return self._runner.run(coro)

        raise RuntimeError("sync client methods cannot run inside an active event loop")

    def close(self) -> None:
        if self._closed:
            return

        self._runner.close()
        self._closed = True


class _SyncAPIProxy:
    def __init__(self, target: Any, run_sync: Any) -> None:
        self._target = target
        self._run_sync = run_sync

    def __getattr__(self, item: str) -> Any:
        attr = getattr(self._target, item)
        if not callable(attr):
            return attr

        def wrapper(*args: Any, **kwargs: Any) -> Any:
            result = attr(*args, **kwargs)
            if hasattr(result, "__aiter__"):
                async def collect() -> list[Any]:
                    return [entry async for entry in result]

                return self._run_sync(collect())
            return self._run_sync(result)

        return wrapper


class OmniClient:
    """Unified Omni client exposing both sync and async methods.

    Convention:
    - sync methods: `get_cluster(...)`
    - async methods: `aget_cluster(...)`
    """

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        self._sync_runner = _SyncRunner()
        self._closed = False
        self._async = AsyncOmniClient(*args, **kwargs)

        # Existing grouped API surfaces (sync wrappers)
        self.resources = _SyncAPIProxy(self._async.resources, self._sync_runner.run)
        self.management = _SyncAPIProxy(self._async.management, self._sync_runner.run)
        self.oidc = _SyncAPIProxy(self._async.oidc, self._sync_runner.run)
        self.auth = _SyncAPIProxy(self._async.auth, self._sync_runner.run)

        # Native async grouped surfaces
        self.aresources = self._async.resources
        self.amanagement = self._async.management
        self.aoidc = self._async.oidc
        self.aauth = self._async.auth

    @property
    def raw(self) -> OmniClient:
        return self

    # Generic method execution -------------------------------------------------

    def call(
        self,
        method: RpcMethod,
        request: dict[str, Any],
        *,
        metadata: dict[str, str] | None = None,
    ) -> dict[str, Any]:
        return self._sync_runner.run(self._async.call(method, request, metadata=metadata))

    async def acall(
        self,
        method: RpcMethod,
        request: dict[str, Any],
        *,
        metadata: dict[str, str] | None = None,
    ) -> dict[str, Any]:
        return await self._async.call(method, request, metadata=metadata)

    def stream(
        self,
        method: RpcMethod,
        request: dict[str, Any],
        *,
        metadata: dict[str, str] | None = None,
    ) -> list[dict[str, Any]]:
        async def collect() -> list[dict[str, Any]]:
            return [evt async for evt in self._async.stream(method, request, metadata=metadata)]

        return self._sync_runner.run(collect())

    async def astream(
        self,
        method: RpcMethod,
        request: dict[str, Any],
        *,
        metadata: dict[str, str] | None = None,
    ) -> AsyncIterator[dict[str, Any]]:
        async for evt in self._async.stream(method, request, metadata=metadata):
            yield evt

    # Resource convenience methods --------------------------------------------

    def get_resource(self, namespace: str, resource_type: str, resource_id: str) -> dict[str, Any]:
        return self.resources.get({"namespace": namespace, "type": resource_type, "id": resource_id})

    async def aget_resource(self, namespace: str, resource_type: str, resource_id: str) -> dict[str, Any]:
        return await self.aresources.get(
            {"namespace": namespace, "type": resource_type, "id": resource_id}
        )

    def list_resources(self, namespace: str, resource_type: str) -> dict[str, Any]:
        return self.resources.list({"namespace": namespace, "type": resource_type})

    async def alist_resources(self, namespace: str, resource_type: str) -> dict[str, Any]:
        return await self.aresources.list({"namespace": namespace, "type": resource_type})

    def create_resource(self, resource: dict[str, Any]) -> dict[str, Any]:
        return self.resources.create({"resource": resource})

    async def acreate_resource(self, resource: dict[str, Any]) -> dict[str, Any]:
        return await self.aresources.create({"resource": resource})

    def update_resource(self, current_version: str, resource: dict[str, Any]) -> dict[str, Any]:
        return self.resources.update({"currentVersion": current_version, "resource": resource})

    async def aupdate_resource(self, current_version: str, resource: dict[str, Any]) -> dict[str, Any]:
        return await self.aresources.update({"currentVersion": current_version, "resource": resource})

    def delete_resource(self, namespace: str, resource_type: str, resource_id: str) -> dict[str, Any]:
        return self.resources.delete({"namespace": namespace, "type": resource_type, "id": resource_id})

    async def adelete_resource(self, namespace: str, resource_type: str, resource_id: str) -> dict[str, Any]:
        return await self.aresources.delete(
            {"namespace": namespace, "type": resource_type, "id": resource_id}
        )

    # Cluster convenience methods ---------------------------------------------

    def get_cluster(self, cluster_name: str, namespace: str = "default") -> dict[str, Any]:
        return self.get_resource(namespace, "Clusters.omni.sidero.dev", cluster_name)

    async def aget_cluster(self, cluster_name: str, namespace: str = "default") -> dict[str, Any]:
        return await self.aget_resource(namespace, "Clusters.omni.sidero.dev", cluster_name)

    def get_cluster_status(self, cluster_name: str, namespace: str = "default") -> dict[str, Any]:
        return self.get_resource(namespace, "ClusterStatuses.omni.sidero.dev", cluster_name)

    async def aget_cluster_status(self, cluster_name: str, namespace: str = "default") -> dict[str, Any]:
        return await self.aget_resource(namespace, "ClusterStatuses.omni.sidero.dev", cluster_name)

    def delete_cluster(self, cluster_name: str, namespace: str = "default") -> dict[str, Any]:
        return self.resources.teardown(
            {"namespace": namespace, "type": "Clusters.omni.sidero.dev", "id": cluster_name}
        )

    async def adelete_cluster(self, cluster_name: str, namespace: str = "default") -> dict[str, Any]:
        return await self.aresources.teardown(
            {"namespace": namespace, "type": "Clusters.omni.sidero.dev", "id": cluster_name}
        )

    # Management convenience methods ------------------------------------------

    def get_kubeconfig(self, *, service_account: bool = False, break_glass: bool = False) -> dict[str, Any]:
        return self.management.kubeconfig(
            {"service_account": service_account, "break_glass": break_glass}
        )

    async def aget_kubeconfig(
        self, *, service_account: bool = False, break_glass: bool = False
    ) -> dict[str, Any]:
        return await self.amanagement.kubeconfig(
            {"service_account": service_account, "break_glass": break_glass}
        )

    def get_talosconfig(self, *, raw: bool = False, break_glass: bool = False) -> dict[str, Any]:
        return self.management.talosconfig({"raw": raw, "break_glass": break_glass})

    async def aget_talosconfig(self, *, raw: bool = False, break_glass: bool = False) -> dict[str, Any]:
        return await self.amanagement.talosconfig({"raw": raw, "break_glass": break_glass})

    def list_service_accounts(self) -> dict[str, Any]:
        return self.management.list_service_accounts()

    async def alist_service_accounts(self) -> dict[str, Any]:
        return await self.amanagement.list_service_accounts()

    def ensure_talosconfig(
        self,
        *,
        cluster: str | None = None,
        break_glass: bool = False,
        force_refresh: bool = False,
        ttl_seconds: int = 300,
        path: str | Path | None = None,
    ) -> Path:
        return self._sync_runner.run(
            self._async.ensure_talosconfig(
                cluster=cluster,
                break_glass=break_glass,
                force_refresh=force_refresh,
                ttl_seconds=ttl_seconds,
                path=path,
            )
        )

    async def aensure_talosconfig(
        self,
        *,
        cluster: str | None = None,
        break_glass: bool = False,
        force_refresh: bool = False,
        ttl_seconds: int = 300,
        path: str | Path | None = None,
    ) -> Path:
        return await self._async.ensure_talosconfig(
            cluster=cluster,
            break_glass=break_glass,
            force_refresh=force_refresh,
            ttl_seconds=ttl_seconds,
            path=path,
        )

    # Lifecycle ----------------------------------------------------------------

    async def aclose(self) -> None:
        if self._closed:
            return

        await self._async.aclose()
        self._sync_runner.close()
        self._closed = True

    def close(self) -> None:
        if self._closed:
            return

        try:
            self._sync_runner.run(self._async.aclose())
        finally:
            self._sync_runner.close()
            self._closed = True

    async def __aenter__(self) -> OmniClient:
        return self

    async def __aexit__(self, exc_type, exc, tb) -> None:
        await self.aclose()

    def __enter__(self) -> OmniClient:
        return self

    def __exit__(self, exc_type, exc, tb) -> None:
        self.close()
