# [DEF:backend.src.core.async_superset_client:Module]
#
# @TIER: CRITICAL
# @SEMANTICS: superset, async, client, httpx, dashboards, datasets
# @PURPOSE: Async Superset client for dashboard hot-path requests without blocking FastAPI event loop.
# @LAYER: Core
# @RELATION: DEPENDS_ON -> backend.src.core.superset_client
# @RELATION: DEPENDS_ON -> backend.src.core.utils.async_network.AsyncAPIClient
# @INVARIANT: Async dashboard operations reuse shared auth cache and avoid sync requests in async routes.

# [SECTION: IMPORTS]
import asyncio
import json
import re
from typing import Any, Dict, List, Optional, Tuple, cast

from .config_models import Environment
from .logger import logger as app_logger, belief_scope
from .superset_client import SupersetClient
from .utils.async_network import AsyncAPIClient
# [/SECTION]


# [DEF:AsyncSupersetClient:Class]
# @PURPOSE: Async sibling of SupersetClient for dashboard read paths.
class AsyncSupersetClient(SupersetClient):
    # [DEF:__init__:Function]
    # @PURPOSE: Initialize async Superset client with AsyncAPIClient transport.
    # @PRE: env is valid.
    # @POST: Client uses async network transport and inherited projection helpers.
    def __init__(self, env: Environment):
        self.env = env
        auth_payload = {
            "username": env.username,
            "password": env.password,
            "provider": "db",
            "refresh": "true",
        }
        self.network = AsyncAPIClient(
            config={"base_url": env.url, "auth": auth_payload},
            verify_ssl=env.verify_ssl,
            timeout=env.timeout,
        )
        self.delete_before_reimport = False
    # [/DEF:__init__:Function]

    # [DEF:aclose:Function]
    # @PURPOSE: Close async transport resources.
    # @POST: Underlying AsyncAPIClient is closed.
    async def aclose(self) -> None:
        await self.network.aclose()
    # [/DEF:aclose:Function]

    # [DEF:get_dashboards_page_async:Function]
    # @PURPOSE: Fetch one dashboards page asynchronously.
    # @POST: Returns total count and page result list.
    async def get_dashboards_page_async(self, query: Optional[Dict] = None) -> Tuple[int, List[Dict]]:
        with belief_scope("AsyncSupersetClient.get_dashboards_page_async"):
            validated_query = self._validate_query_params(query or {})
            if "columns" not in validated_query:
                validated_query["columns"] = [
                    "slug",
                    "id",
                    "url",
                    "changed_on_utc",
                    "dashboard_title",
                    "published",
                    "created_by",
                    "changed_by",
                    "changed_by_name",
                    "owners",
                ]

            response_json = cast(
                Dict[str, Any],
                await self.network.request(
                    method="GET",
                    endpoint="/dashboard/",
                    params={"q": json.dumps(validated_query)},
                ),
            )
            result = response_json.get("result", [])
            total_count = response_json.get("count", len(result))
            return total_count, result
    # [/DEF:get_dashboards_page_async:Function]

    # [DEF:get_dashboard_async:Function]
    # @PURPOSE: Fetch one dashboard payload asynchronously.
    # @POST: Returns raw dashboard payload from Superset API.
    async def get_dashboard_async(self, dashboard_id: int) -> Dict:
        with belief_scope("AsyncSupersetClient.get_dashboard_async", f"id={dashboard_id}"):
            response = await self.network.request(method="GET", endpoint=f"/dashboard/{dashboard_id}")
            return cast(Dict, response)
    # [/DEF:get_dashboard_async:Function]

    # [DEF:get_chart_async:Function]
    # @PURPOSE: Fetch one chart payload asynchronously.
    # @POST: Returns raw chart payload from Superset API.
    async def get_chart_async(self, chart_id: int) -> Dict:
        with belief_scope("AsyncSupersetClient.get_chart_async", f"id={chart_id}"):
            response = await self.network.request(method="GET", endpoint=f"/chart/{chart_id}")
            return cast(Dict, response)
    # [/DEF:get_chart_async:Function]

    # [DEF:get_dashboard_detail_async:Function]
    # @PURPOSE: Fetch dashboard detail asynchronously with concurrent charts/datasets requests.
    # @POST: Returns dashboard detail payload for overview page.
    async def get_dashboard_detail_async(self, dashboard_id: int) -> Dict:
        with belief_scope("AsyncSupersetClient.get_dashboard_detail_async", f"id={dashboard_id}"):
            dashboard_response = await self.get_dashboard_async(dashboard_id)
            dashboard_data = dashboard_response.get("result", dashboard_response)

            charts: List[Dict] = []
            datasets: List[Dict] = []

            def extract_dataset_id_from_form_data(form_data: Optional[Dict]) -> Optional[int]:
                if not isinstance(form_data, dict):
                    return None
                datasource = form_data.get("datasource")
                if isinstance(datasource, str):
                    matched = re.match(r"^(\d+)__", datasource)
                    if matched:
                        try:
                            return int(matched.group(1))
                        except ValueError:
                            return None
                if isinstance(datasource, dict):
                    ds_id = datasource.get("id")
                    try:
                        return int(ds_id) if ds_id is not None else None
                    except (TypeError, ValueError):
                        return None
                ds_id = form_data.get("datasource_id")
                try:
                    return int(ds_id) if ds_id is not None else None
                except (TypeError, ValueError):
                    return None

            chart_task = self.network.request(
                method="GET",
                endpoint=f"/dashboard/{dashboard_id}/charts",
            )
            dataset_task = self.network.request(
                method="GET",
                endpoint=f"/dashboard/{dashboard_id}/datasets",
            )
            charts_response, datasets_response = await asyncio.gather(
                chart_task,
                dataset_task,
                return_exceptions=True,
            )

            if not isinstance(charts_response, Exception):
                charts_payload = charts_response.get("result", []) if isinstance(charts_response, dict) else []
                for chart_obj in charts_payload:
                    if not isinstance(chart_obj, dict):
                        continue
                    chart_id = chart_obj.get("id")
                    if chart_id is None:
                        continue
                    form_data = chart_obj.get("form_data")
                    if isinstance(form_data, str):
                        try:
                            form_data = json.loads(form_data)
                        except Exception:
                            form_data = {}
                    dataset_id = extract_dataset_id_from_form_data(form_data) or chart_obj.get("datasource_id")
                    charts.append({
                        "id": int(chart_id),
                        "title": chart_obj.get("slice_name") or chart_obj.get("name") or f"Chart {chart_id}",
                        "viz_type": (form_data.get("viz_type") if isinstance(form_data, dict) else None),
                        "dataset_id": int(dataset_id) if dataset_id is not None else None,
                        "last_modified": chart_obj.get("changed_on"),
                        "overview": chart_obj.get("description") or (form_data.get("viz_type") if isinstance(form_data, dict) else None) or "Chart",
                    })
            else:
                app_logger.warning("[get_dashboard_detail_async][Warning] Failed to fetch dashboard charts: %s", charts_response)

            if not isinstance(datasets_response, Exception):
                datasets_payload = datasets_response.get("result", []) if isinstance(datasets_response, dict) else []
                for dataset_obj in datasets_payload:
                    if not isinstance(dataset_obj, dict):
                        continue
                    dataset_id = dataset_obj.get("id")
                    if dataset_id is None:
                        continue
                    db_payload = dataset_obj.get("database")
                    db_name = db_payload.get("database_name") if isinstance(db_payload, dict) else None
                    table_name = dataset_obj.get("table_name") or dataset_obj.get("datasource_name") or dataset_obj.get("name") or f"Dataset {dataset_id}"
                    schema = dataset_obj.get("schema")
                    fq_name = f"{schema}.{table_name}" if schema else table_name
                    datasets.append({
                        "id": int(dataset_id),
                        "table_name": table_name,
                        "schema": schema,
                        "database": db_name or dataset_obj.get("database_name") or "Unknown",
                        "last_modified": dataset_obj.get("changed_on"),
                        "overview": fq_name,
                    })
            else:
                app_logger.warning("[get_dashboard_detail_async][Warning] Failed to fetch dashboard datasets: %s", datasets_response)

            if not charts:
                raw_position_json = dashboard_data.get("position_json")
                chart_ids_from_position = set()
                if isinstance(raw_position_json, str) and raw_position_json:
                    try:
                        parsed_position = json.loads(raw_position_json)
                        chart_ids_from_position.update(self._extract_chart_ids_from_layout(parsed_position))
                    except Exception:
                        pass
                elif isinstance(raw_position_json, dict):
                    chart_ids_from_position.update(self._extract_chart_ids_from_layout(raw_position_json))

                raw_json_metadata = dashboard_data.get("json_metadata")
                if isinstance(raw_json_metadata, str) and raw_json_metadata:
                    try:
                        parsed_metadata = json.loads(raw_json_metadata)
                        chart_ids_from_position.update(self._extract_chart_ids_from_layout(parsed_metadata))
                    except Exception:
                        pass
                elif isinstance(raw_json_metadata, dict):
                    chart_ids_from_position.update(self._extract_chart_ids_from_layout(raw_json_metadata))

                fallback_chart_tasks = [
                    self.get_chart_async(int(chart_id))
                    for chart_id in sorted(chart_ids_from_position)
                ]
                fallback_chart_responses = await asyncio.gather(
                    *fallback_chart_tasks,
                    return_exceptions=True,
                )
                for chart_id, chart_response in zip(sorted(chart_ids_from_position), fallback_chart_responses):
                    if isinstance(chart_response, Exception):
                        app_logger.warning("[get_dashboard_detail_async][Warning] Failed to resolve fallback chart %s: %s", chart_id, chart_response)
                        continue
                    chart_data = chart_response.get("result", chart_response)
                    charts.append({
                        "id": int(chart_id),
                        "title": chart_data.get("slice_name") or chart_data.get("name") or f"Chart {chart_id}",
                        "viz_type": chart_data.get("viz_type"),
                        "dataset_id": chart_data.get("datasource_id"),
                        "last_modified": chart_data.get("changed_on"),
                        "overview": chart_data.get("description") or chart_data.get("viz_type") or "Chart",
                    })

            dataset_ids_from_charts = {
                c.get("dataset_id")
                for c in charts
                if c.get("dataset_id") is not None
            }
            known_dataset_ids = {d.get("id") for d in datasets if d.get("id") is not None}
            missing_dataset_ids = sorted(int(item) for item in dataset_ids_from_charts if item not in known_dataset_ids)
            if missing_dataset_ids:
                dataset_fetch_tasks = [
                    self.network.request(method="GET", endpoint=f"/dataset/{dataset_id}")
                    for dataset_id in missing_dataset_ids
                ]
                dataset_fetch_responses = await asyncio.gather(
                    *dataset_fetch_tasks,
                    return_exceptions=True,
                )
                for dataset_id, dataset_response in zip(missing_dataset_ids, dataset_fetch_responses):
                    if isinstance(dataset_response, Exception):
                        app_logger.warning("[get_dashboard_detail_async][Warning] Failed to backfill dataset %s: %s", dataset_id, dataset_response)
                        continue
                    dataset_data = dataset_response.get("result", dataset_response) if isinstance(dataset_response, dict) else {}
                    db_payload = dataset_data.get("database")
                    db_name = db_payload.get("database_name") if isinstance(db_payload, dict) else None
                    table_name = dataset_data.get("table_name") or dataset_data.get("datasource_name") or dataset_data.get("name") or f"Dataset {dataset_id}"
                    schema = dataset_data.get("schema")
                    fq_name = f"{schema}.{table_name}" if schema else table_name
                    datasets.append({
                        "id": int(dataset_id),
                        "table_name": table_name,
                        "schema": schema,
                        "database": db_name or dataset_data.get("database_name") or "Unknown",
                        "last_modified": dataset_data.get("changed_on"),
                        "overview": fq_name,
                    })

            return {
                "id": int(dashboard_data.get("id") or dashboard_id),
                "title": dashboard_data.get("dashboard_title") or dashboard_data.get("title") or f"Dashboard {dashboard_id}",
                "slug": dashboard_data.get("slug"),
                "url": dashboard_data.get("url"),
                "description": dashboard_data.get("description"),
                "last_modified": dashboard_data.get("changed_on_utc") or dashboard_data.get("changed_on"),
                "published": dashboard_data.get("published"),
                "charts": charts,
                "datasets": datasets,
                "chart_count": len(charts),
                "dataset_count": len(datasets),
            }
    # [/DEF:get_dashboard_detail_async:Function]
# [/DEF:AsyncSupersetClient:Class]

# [/DEF:backend.src.core.async_superset_client:Module]
