"""Async facade client for DSG Core API."""

from __future__ import annotations

from datetime import date
from typing import Any, TypeVar, cast
from uuid import UUID

from httpx import HTTPStatusError
from pydantic import BaseModel

from datasdr_base_api_client import BaseHTTPClient, JSON_TYPE, TokenProvider

from .item_type_cache import ItemTypePathCache
from .schemas import (
    CatalogItemTypeResponse,
    CommandDryRunRequest,
    CommandDryRunResponse,
    CreateTaskRequest,
    DependencyResponse,
    EventResponse,
    ItemDataResponse,
    ItemTypeDisplayAttributesResponse,
    ItemTypeMetadataResponse,
    ItemRequest,
    QueryRequest,
    QueryResponse,
    ReportGenerationResponse,
    TaskResponse,
)
from .subclients import (
    BgTasksClient,
    CatalogClient,
    CommandsClient,
    EventsClient,
    MetadataClient,
    QueriesClient,
    ReportsClient,
    TasksClient,
)

SchemaT = TypeVar("SchemaT", bound=BaseModel)


class DSGCoreClient(BaseHTTPClient):
    """Typed async facade client for DSG Core.

    Access grouped subclients through:
    - `client.tasks`
    - `client.bgtasks`
    - `client.queries`
    - `client.events`
    - `client.catalog`
    - `client.metadata`
    - `client.commands`
    - `client.reports`
    - `client.mapper`
    """

    def __init__(
        self,
        base_url: str,
        token: str | None = None,
        timeout: int = 30,
        token_provider: TokenProvider | None = None,
        auth_retry_attempts: int = 3,
        api_prefix: str = "/v1",
    ):
        super().__init__(
            base_url=base_url,
            token=token,
            timeout=timeout,
            token_provider=token_provider,
            auth_retry_attempts=auth_retry_attempts,
        )
        self.api_prefix = api_prefix.rstrip("/")

        self.tasks = TasksClient(self)
        self.bgtasks = BgTasksClient(self)
        self.queries = QueriesClient(self)
        self.events = EventsClient(self)
        self.catalog = CatalogClient(self)
        self.metadata = MetadataClient(self)
        self.commands = CommandsClient(self)
        self.reports = ReportsClient(self)
        self.item_type_cache = ItemTypePathCache.get_instance(self.catalog)
        self.mapper = self.item_type_cache

    def _path(self, endpoint: str) -> str:
        endpoint_path = endpoint if endpoint.startswith("/") else f"/{endpoint}"
        return f"{self.api_prefix}{endpoint_path}"

    def path(self, endpoint: str) -> str:
        return self._path(endpoint)

    @staticmethod
    def _dump(model: BaseModel) -> dict[str, Any]:
        return model.model_dump(mode="json", by_alias=True, exclude_none=True)

    @staticmethod
    def dump(model: BaseModel) -> dict[str, Any]:
        return DSGCoreClient._dump(model)

    def _parse(self, payload: JSON_TYPE, schema: type[SchemaT]) -> SchemaT:
        if payload is None:
            raise ValueError(f"Expected {schema.__name__} payload but got no content")
        return schema.model_validate(payload)

    def parse(self, payload: JSON_TYPE, schema: type[SchemaT]) -> SchemaT:
        return self._parse(payload, schema)

    def _parse_list(self, payload: JSON_TYPE, schema: type[SchemaT]) -> list[SchemaT]:
        if not payload:
            return []
        if not isinstance(payload, list):
            raise ValueError(f"Expected list payload for {schema.__name__}")
        return [schema.model_validate(item) for item in payload]

    def parse_list(self, payload: JSON_TYPE, schema: type[SchemaT]) -> list[SchemaT]:
        return self._parse_list(payload, schema)

    async def get_bytes(
        self, endpoint: str, params: dict[str, Any] | None = None
    ) -> bytes:
        url = f"{self.base_url}/{endpoint.lstrip('/')}"
        for attempt in range(1, self.auth_retry_attempts + 1):
            headers = await self._get_headers()
            response = await self.client.request(
                "GET",
                url,
                headers=headers,
                follow_redirects=self.follow_redirects,
                params=params,
            )
            try:
                response.raise_for_status()
                return response.content
            except HTTPStatusError as exc:
                status_code = exc.response.status_code
                should_retry = (
                    self.token_provider is not None
                    and status_code in {401, 403}
                    and attempt < self.auth_retry_attempts
                )
                if should_retry:
                    if self.token_provider is not None:
                        self.token_provider.invalidate_token()
                    continue
                raise
        return b""

    async def get_with_headers(
        self,
        endpoint: str,
        *,
        params: dict[str, Any] | None = None,
        headers: dict[str, str] | None = None,
    ) -> JSON_TYPE:
        url = f"{self.base_url}/{endpoint.lstrip('/')}"
        for attempt in range(1, self.auth_retry_attempts + 1):
            request_headers = await self._get_headers(headers)
            response = await self.client.request(
                "GET",
                url,
                headers=request_headers,
                follow_redirects=self.follow_redirects,
                params=params,
            )
            try:
                response.raise_for_status()
                content_type = response.headers.get("content-type", "")
                if "application/json" in content_type:
                    return cast(JSON_TYPE, response.json())
                return cast(JSON_TYPE, response.text)
            except HTTPStatusError as exc:
                status_code = exc.response.status_code
                should_retry = (
                    self.token_provider is not None
                    and status_code in {401, 403}
                    and attempt < self.auth_retry_attempts
                )
                if should_retry:
                    if self.token_provider is not None:
                        self.token_provider.invalidate_token()
                    continue
                raise
        return None

    async def create_task(self, request: CreateTaskRequest) -> TaskResponse:
        return await self.tasks.create_task(request)

    async def list_tasks(
        self,
        created_by: UUID | None = None,
        assignee: UUID | None = None,
        offset: int = 0,
        limit: int = 1000,
        item_id: UUID | None = None,
    ) -> list[TaskResponse]:
        return await self.tasks.list_tasks(
            created_by=created_by,
            assignee=assignee,
            offset=offset,
            limit=limit,
            item_id=item_id,
        )

    async def get_task(self, task_id: UUID) -> TaskResponse:
        return await self.tasks.get_task(task_id)

    async def query_items(self, request: QueryRequest) -> QueryResponse:
        return await self.queries.query_items(request)

    async def query_item_dependencies(
        self,
        request: ItemRequest,
        steps: int = 1,
        limit: int | None = None,
    ) -> list[DependencyResponse]:
        return await self.queries.query_item_dependencies(
            request=request,
            steps=steps,
            limit=limit,
        )

    async def query_item_references(
        self, request: ItemRequest
    ) -> list[ItemDataResponse]:
        return await self.queries.query_item_references(request)

    async def query_item_graph(
        self,
        request: ItemRequest,
        steps: int = 3,
        data_limit: int | None = None,
        data_limit_mode: str = "total_per_item_type",
    ) -> dict[str, Any]:
        return await self.queries.query_item_graph(
            request=request,
            steps=steps,
            data_limit=data_limit,
            data_limit_mode=data_limit_mode,
        )

    async def list_events(
        self,
        event_types: list[str] | None = None,
        limit: int | None = None,
        offset: int | None = None,
    ) -> list[EventResponse]:
        return await self.events.list_events(
            event_types=event_types,
            limit=limit,
            offset=offset,
        )

    async def list_aggregate_events(
        self,
        aggregate_id: UUID,
        version: str | None = None,
    ) -> list[EventResponse]:
        return await self.events.list_aggregate_events(
            aggregate_id=aggregate_id,
            version=version,
        )

    async def query_item_events(
        self,
        item_id: UUID,
        version: str | None = None,
    ) -> list[EventResponse]:
        return await self.events.query_item_events(item_id, version=version)

    async def query_item_last_event(
        self,
        item_id: UUID,
        version: str | None = None,
    ) -> EventResponse:
        return await self.events.query_item_last_event(item_id, version=version)

    async def query_item_last_n_events(
        self,
        item_id: UUID,
        n: int,
        version: str | None = None,
    ) -> list[EventResponse]:
        return await self.events.query_item_last_n_events(
            item_id,
            n,
            version=version,
        )

    async def map_item_type(self, key: str) -> CatalogItemTypeResponse:
        return await self.catalog.map_item_type(key)

    async def map_item_type_id(self, key: str) -> UUID:
        return await self.catalog.map_item_type_id(key)

    async def get_item_type_metadata(
        self,
        item_type_id: UUID,
    ) -> ItemTypeMetadataResponse:
        return await self.metadata.get_item_type_metadata(item_type_id)

    async def get_item_type_display_attributes(
        self,
        item_type_id: UUID,
        version: str,
    ) -> ItemTypeDisplayAttributesResponse:
        return await self.metadata.get_item_type_display_attributes(
            item_type_id=item_type_id,
            version=version,
        )

    async def start_dryrun_commands(
        self, commands: list[CommandDryRunRequest]
    ) -> ReportGenerationResponse:
        return await self.commands.start_dryrun_commands(commands)

    async def get_dryrun_commands_result(
        self,
        job_id: str,
        timeout_seconds: float | None = 300.0,
        poll_interval_seconds: float = 1.0,
        wait_for_ready: bool = True,
    ) -> CommandDryRunResponse:
        return await self.commands.get_dryrun_commands_result(
            job_id=job_id,
            timeout_seconds=timeout_seconds,
            poll_interval_seconds=poll_interval_seconds,
            wait_for_ready=wait_for_ready,
        )

    async def dryrun_commands(
        self,
        commands: list[CommandDryRunRequest],
        timeout_seconds: float | None = 300.0,
        poll_interval_seconds: float = 1.0,
        wait_for_ready: bool = True,
    ) -> CommandDryRunResponse:
        return await self.commands.dryrun_commands(
            commands=commands,
            timeout_seconds=timeout_seconds,
            poll_interval_seconds=poll_interval_seconds,
            wait_for_ready=wait_for_ready,
        )

    async def query(
        self,
        item_type_id: UUID,
        item_id: UUID | None = None,
        versions: list[str] | None = None,
        valid_at: date | str | None = None,
        include_history: bool = False,
        limit: int | None = None,
        offset: int | None = None,
        filters: list[dict[str, Any]] | None = None,
        order_by: list[dict[str, Any]] | None = None,
    ) -> QueryResponse:
        valid_at_arg = valid_at.isoformat() if isinstance(valid_at, date) else valid_at
        return await self.queries.query(
            item_type_id=item_type_id,
            item_id=item_id,
            versions=versions,
            valid_at=valid_at_arg,
            include_history=include_history,
            limit=limit,
            offset=offset,
            filters=filters,
            order_by=order_by,
        )

    async def get_item_dependencies(
        self, item_type_id: UUID, item_id: UUID, version: str
    ) -> list[DependencyResponse]:
        return await self.queries.get_item_dependencies(item_type_id, item_id, version)

    async def get_item_graph(
        self,
        item_type_id: UUID,
        item_id: UUID,
        version: str,
        steps: int = 3,
        data_limit: int | None = None,
        data_limit_mode: str = "total_per_item_type",
    ) -> dict[str, Any]:
        return await self.queries.get_item_graph(
            item_type_id=item_type_id,
            item_id=item_id,
            version=version,
            steps=steps,
            data_limit=data_limit,
            data_limit_mode=data_limit_mode,
        )

    async def commands_dryrun(
        self,
        commands: list[CommandDryRunRequest],
        timeout_seconds: float | None = 300.0,
        poll_interval_seconds: float = 1.0,
        wait_for_ready: bool = True,
    ) -> CommandDryRunResponse:
        return await self.commands.commands_dryrun(
            commands=commands,
            timeout_seconds=timeout_seconds,
            poll_interval_seconds=poll_interval_seconds,
            wait_for_ready=wait_for_ready,
        )

    async def start_report_generation(
        self,
        request: QueryRequest,
        include_alias: bool = False,
        output_format: str = "excel",
    ) -> ReportGenerationResponse:
        return await self.reports.start_report_generation(
            request=request, include_alias=include_alias, output_format=output_format
        )

    async def download_report(
        self,
        job_id: str,
        timeout_seconds: float | None = 300.0,
        poll_interval_seconds: float = 1.0,
        wait_for_ready: bool = True,
    ) -> bytes:
        return await self.reports.download_report(
            job_id=job_id,
            timeout_seconds=timeout_seconds,
            poll_interval_seconds=poll_interval_seconds,
            wait_for_ready=wait_for_ready,
        )
