from __future__ import annotations

import string
from abc import ABC, abstractmethod
from types import SimpleNamespace
from typing import Any, ClassVar, Dict, Optional, Sequence, Union, cast

from httpx import AsyncClient
from httpx import Client as httpxClient
from pydantic import AliasGenerator, ConfigDict, alias_generators

std_config = ConfigDict(
    populate_by_name=True,
    alias_generator=AliasGenerator(
        serialization_alias=alias_generators.to_camel,
        validation_alias=alias_generators.to_camel,
    )
)

RESOURCE_REGISTRY = {}


def get_resource_type(resource: Resource | Ref) -> str:
    """
    Get the resource name from a resource or ref object.
    """
    resource_type = getattr(resource, "resource_type", None)
    # Use class name if resource_type couldn't be determined
    if not resource_type:
        resource_type = type(resource).__name__

    return resource_type


def register_resource(
    type_name: str | None = None, *, group: str, keys: list[str] | None = None
):
    """
    Decorator to register resource classes.
    Allows lookup of resource classes by their resource_type.
    """

    if keys is None:
        keys = []

    def decorator(cls):
        # Ensure Resource classes define keys
        is_resource = issubclass(cls, Resource)
        if is_resource and not keys or not isinstance(keys, list):
            raise TypeError(
                f"Resource class {cls.__name__} must define non-empty list of keys"
            )

        # Use class name if resource_type couldn't be determined
        resource_type = type_name if type_name else cls.__name__

        RESOURCE_REGISTRY[resource_type] = {
            "class": cls,
            "group": group,
            "keys": keys,
        }

        # Use type casting to prevent Pyright errors
        if is_resource:
            resource_cls = cast("type[Resource]", cls)
            setattr(resource_cls, "resource_type", resource_type)
        else:
            cls.resource_type = resource_type

        return cls

    return decorator


def get_resource_class(resource_type: str):
    """
    Get a resource class by its resource_type.
    """
    resource = RESOURCE_REGISTRY.get(resource_type)
    if resource is None:
        raise RuntimeError(f"Resource type '{resource_type}' not found in registry")
    return resource["class"]


class Ref(ABC):
    id: str

    @abstractmethod
    def attach(self, client: httpxClient) -> None:
        pass

    def deps(self):
        return []


class Resource(ABC):
    model_config = std_config
    filterable_fields: ClassVar[list[str]] = []

    id: str

    @abstractmethod
    def read(self, client: httpxClient, old_state: SimpleNamespace) -> None | Dict[str, Any]:
        pass

    @abstractmethod
    def create(self, client: httpxClient) -> Optional[Dict[str, Any]]:
        pass

    @abstractmethod
    def update(self, client: httpxClient, old_state) -> Union[None, Dict[str, Any]]:
        pass

    @staticmethod
    @abstractmethod
    def delete(client: httpxClient, old_state) -> None:
        pass

    @abstractmethod
    def deps(self) -> Sequence["Resource|Ref"]:
        pass


class CamelAlias(ABC):
    model_config = std_config


class APIMixin(ABC):
    @classmethod
    @abstractmethod
    def create_resource_id(cls, data: dict) -> str:
        pass

    @classmethod
    @abstractmethod
    def from_api(cls, data: dict[str, Any]) -> dict[str, Any]:
        pass


class ListMixin(APIMixin):
    @classmethod
    @abstractmethod
    def get_keys(cls, data: dict[str, Any]) -> dict[str, Any]:
        pass

    @classmethod
    @abstractmethod
    def list_endpoint(cls, **kwargs) -> str:
        pass

    @classmethod
    def extract_list_items(
            cls,
            data: dict[str, Any],
            query_params: dict[str, Any]) -> list[dict[str, Any]]:
        """
        Extract list items from API response.
        Override this method if the API response has a non-standard format.

        Args:
            data: The API response data
            query_params: Query parameters used in the request (useful for adding context to items)
        """
        return data.get("values", [])

    @classmethod
    async def list(
            cls,
            client: AsyncClient,
            query_params: dict[str, Any] | None = None,
            path_params: dict[str, Any] | None = None
    ) -> dict[str, Any]:
        query_params = query_params or {}
        path_params = path_params or {}
        url_template = cls.list_endpoint(**path_params)
        url = url_template.format(**path_params)

        response = await client.get(url, params=query_params)
        data = response.json()

        # some endpoints return a plain list (for example, /access/api/roles)
        if isinstance(data, list):
            data = {"values": data}

        data.pop("links", None)
        data.pop("href", None)
        data.pop("previousPage", None)
        items = cls.extract_list_items(data, path_params)
        data["values"] = [
            {
                "value": cls.from_api(path_params | item),
                "keys": cls.get_keys(path_params | item),
            } for item in items
        ]
        return data


class FetchMixin(APIMixin):
    @classmethod
    @abstractmethod
    def fetch_endpoint(cls) -> str:
        pass

    @classmethod
    async def fetch(cls, client: AsyncClient, keys: dict[str, Any]) -> dict[str, Any]:
        url_template = cls.fetch_endpoint()
        url = url_template.format(**keys)

        path_keys = {name for _, name, _, _ in string.Formatter().parse(url_template) if name}
        query_params = {k: v for k, v in keys.items() if k not in path_keys}

        response = await client.get(url, params=query_params)
        data = response.json()

        # if the endpoint returns a list of items, take the first one
        if isinstance(data.get("values"), list):
            data = data["values"][0]

        return cls.from_api(keys | data)
