# #region AsyncSupersetClientModule [C:3] [TYPE Module] [SEMANTICS superset, transform, dashboard, filter, async-superset-client]
# @BRIEF Parse a Superset dashboard URL and extract native filter state asynchronously.
# @LAYER Core
import asyncio
import json
import re
from typing import Any, cast

from .config_models import Environment
from .logger import belief_scope, logger as app_logger
from .superset_client import SupersetClient
from .utils.async_network import AsyncAPIClient


# #region AsyncSupersetClient [C:3] [TYPE Class]
# @BRIEF Async sibling of SupersetClient for dashboard read paths.
# @RELATION INHERITS -> [SupersetClient]
# @RELATION DEPENDS_ON -> [AsyncAPIClient]
class AsyncSupersetClient(SupersetClient):
    # #region AsyncSupersetClientInit [TYPE Function] [C:3]
    # @PURPOSE: Initialize async Superset client with AsyncAPIClient transport.
    # @PRE  env is valid Environment instance.
    # @POST  Client uses async network transport and inherited projection helpers.
    # @DATA_CONTRACT  Input[Environment] -> self.network[AsyncAPIClient]
    # @RELATION DEPENDS_ON -> [AsyncAPIClient]
    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
    # #endregion AsyncSupersetClientInit
    # #region AsyncSupersetClientClose [TYPE Function] [C:3]
    # @PURPOSE: Close async transport resources.
    # @POST  Underlying AsyncAPIClient is closed.
    # @SIDE_EFFECT  Closes network sockets.
    # @RELATION CALLS -> [AsyncAPIClient.aclose]
    async def aclose(self) -> None:
        await self.network.aclose()
    # #endregion AsyncSupersetClientClose
    # #region get_dashboards_page_async [TYPE Function] [C:3]
    # @PURPOSE: Fetch one dashboards page asynchronously.
    # @POST  Returns total count and page result list.
    # @DATA_CONTRACT  Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]
    # @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]
    async def get_dashboards_page_async(
        self, query: dict | None = 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
    # #endregion get_dashboards_page_async
    # #region get_dashboard_async [TYPE Function] [C:3]
    # @PURPOSE: Fetch one dashboard payload asynchronously.
    # @POST  Returns raw dashboard payload from Superset API.
    # @DATA_CONTRACT  Input[dashboard_id: int] -> Output[Dict]
    # @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]
    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)
    # #endregion get_dashboard_async
    # #region get_chart_async [TYPE Function] [C:3]
    # @PURPOSE: Fetch one chart payload asynchronously.
    # @POST  Returns raw chart payload from Superset API.
    # @DATA_CONTRACT  Input[chart_id: int] -> Output[Dict]
    # @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]
    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)
    # #endregion get_chart_async
    # #region get_dashboard_detail_async [TYPE Function] [C:3]
    # @PURPOSE: Fetch dashboard detail asynchronously with concurrent charts/datasets requests.
    # @POST  Returns dashboard detail payload for overview page.
    # @DATA_CONTRACT  Input[dashboard_id: int] -> Output[Dict]
    # @RELATION CALLS -> [get_dashboard_async]
    # @RELATION CALLS -> [get_chart_async]
    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: dict | None,
            ) -> int | None:
                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:
                        app_logger.debug("Could not parse position JSON for dashboard %s", dashboard_ref)
                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:
                        app_logger.debug("Could not parse json_metadata for dashboard %s", dashboard_ref)
                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),
            }
    # #endregion get_dashboard_detail_async
    # #region get_dashboard_permalink_state_async [TYPE Function] [C:2]
    # @PURPOSE: Fetch stored dashboard permalink state asynchronously.
    # @POST  Returns dashboard permalink state payload from Superset API.
    # @DATA_CONTRACT  Input[permalink_key: str] -> Output[Dict]
    async def get_dashboard_permalink_state_async(self, permalink_key: str) -> dict:
        with belief_scope(
            "AsyncSupersetClient.get_dashboard_permalink_state_async",
            f"key={permalink_key}",
        ):
            response = await self.network.request(
                method="GET", endpoint=f"/dashboard/permalink/{permalink_key}"
            )
            return cast(dict, response)
    # #endregion get_dashboard_permalink_state_async
    # #region get_native_filter_state_async [TYPE Function] [C:2]
    # @PURPOSE: Fetch stored native filter state asynchronously.
    # @POST  Returns native filter state payload from Superset API.
    # @DATA_CONTRACT  Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]
    async def get_native_filter_state_async(
        self, dashboard_id: int, filter_state_key: str
    ) -> dict:
        with belief_scope(
            "AsyncSupersetClient.get_native_filter_state_async",
            f"dashboard={dashboard_id}, key={filter_state_key}",
        ):
            response = await self.network.request(
                method="GET",
                endpoint=f"/dashboard/{dashboard_id}/filter_state/{filter_state_key}",
            )
            return cast(dict, response)
    # #endregion get_native_filter_state_async
    # #region extract_native_filters_from_permalink_async [TYPE Function] [C:3]
    # @PURPOSE: Extract native filters dataMask from a permalink key asynchronously.
    # @POST  Returns extracted dataMask with filter states.
    # @DATA_CONTRACT  Input[permalink_key: str] -> Output[Dict]
    # @RELATION CALLS -> [get_dashboard_permalink_state_async]
    async def extract_native_filters_from_permalink_async(
        self, permalink_key: str
    ) -> dict:
        with belief_scope(
            "AsyncSupersetClient.extract_native_filters_from_permalink_async",
            f"key={permalink_key}",
        ):
            permalink_response = await self.get_dashboard_permalink_state_async(
                permalink_key
            )
            result = permalink_response.get("result", permalink_response)
            state = result.get("state", result)
            data_mask = state.get("dataMask", {})
            extracted_filters = {}
            for filter_id, filter_data in data_mask.items():
                if not isinstance(filter_data, dict):
                    continue
                extracted_filters[filter_id] = {
                    "extraFormData": filter_data.get("extraFormData", {}),
                    "filterState": filter_data.get("filterState", {}),
                    "ownState": filter_data.get("ownState", {}),
                }
            return {
                "dataMask": extracted_filters,
                "activeTabs": state.get("activeTabs", []),
                "anchor": state.get("anchor"),
                "chartStates": state.get("chartStates", {}),
                "permalink_key": permalink_key,
            }
    # #endregion extract_native_filters_from_permalink_async
    # #region extract_native_filters_from_key_async [TYPE Function] [C:3]
    # @PURPOSE: Extract native filters from a native_filters_key URL parameter asynchronously.
    # @POST  Returns extracted filter state with extraFormData.
    # @DATA_CONTRACT  Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]
    # @RELATION CALLS -> [get_native_filter_state_async]
    async def extract_native_filters_from_key_async(
        self, dashboard_id: int, filter_state_key: str
    ) -> dict:
        with belief_scope(
            "AsyncSupersetClient.extract_native_filters_from_key_async",
            f"dashboard={dashboard_id}, key={filter_state_key}",
        ):
            filter_response = await self.get_native_filter_state_async(
                dashboard_id, filter_state_key
            )
            result = filter_response.get("result", filter_response)
            value = result.get("value")
            if isinstance(value, str):
                try:
                    parsed_value = json.loads(value)
                except json.JSONDecodeError as e:
                    app_logger.warning(
                        "[extract_native_filters_from_key_async][Warning] Failed to parse filter state JSON: %s",
                        e,
                    )
                    parsed_value = {}
            elif isinstance(value, dict):
                parsed_value = value
            else:
                parsed_value = {}
            extracted_filters = {}
            if "id" in parsed_value and "extraFormData" in parsed_value:
                filter_id = parsed_value.get("id", filter_state_key)
                extracted_filters[filter_id] = {
                    "extraFormData": parsed_value.get("extraFormData", {}),
                    "filterState": parsed_value.get("filterState", {}),
                    "ownState": parsed_value.get("ownState", {}),
                }
            else:
                for filter_id, filter_data in parsed_value.items():
                    if not isinstance(filter_data, dict):
                        continue
                    extracted_filters[filter_id] = {
                        "extraFormData": filter_data.get("extraFormData", {}),
                        "filterState": filter_data.get("filterState", {}),
                        "ownState": filter_data.get("ownState", {}),
                    }
            return {
                "dataMask": extracted_filters,
                "dashboard_id": dashboard_id,
                "filter_state_key": filter_state_key,
            }
    # #endregion extract_native_filters_from_key_async
    # #region parse_dashboard_url_for_filters_async [TYPE Function] [C:3]
    # @PURPOSE: Parse a Superset dashboard URL and extract native filter state asynchronously.
    # @POST  Returns extracted filter state or empty dict if no filters found.
    # @DATA_CONTRACT  Input[url: str] -> Output[Dict]
    # @RELATION CALLS -> [extract_native_filters_from_permalink_async]
    # @RELATION CALLS -> [extract_native_filters_from_key_async]
    async def parse_dashboard_url_for_filters_async(self, url: str) -> dict:
        with belief_scope(
            "AsyncSupersetClient.parse_dashboard_url_for_filters_async", f"url={url}"
        ):
            import urllib.parse
            parsed_url = urllib.parse.urlparse(url)
            query_params = urllib.parse.parse_qs(parsed_url.query)
            path_parts = parsed_url.path.rstrip("/").split("/")
            result = {
                "url": url,
                "dashboard_id": None,
                "filter_type": None,
                "filters": {},
            }
            # Check for permalink URL: /dashboard/p/{key}/
            if "p" in path_parts:
                try:
                    p_index = path_parts.index("p")
                    if p_index + 1 < len(path_parts):
                        permalink_key = path_parts[p_index + 1]
                        filter_data = (
                            await self.extract_native_filters_from_permalink_async(
                                permalink_key
                            )
                        )
                        result["filter_type"] = "permalink"
                        result["filters"] = filter_data
                        return result
                except ValueError:
                    app_logger.debug("Permalink key not found in URL for dashboard %s", dashboard_ref)
            # Check for native_filters_key in query params
            native_filters_key = query_params.get("native_filters_key", [None])[0]
            if native_filters_key:
                dashboard_ref = None
                if "dashboard" in path_parts:
                    try:
                        dash_index = path_parts.index("dashboard")
                        if dash_index + 1 < len(path_parts):
                            potential_id = path_parts[dash_index + 1]
                            if potential_id not in ("p", "list", "new"):
                                dashboard_ref = potential_id
                    except ValueError:
                        app_logger.debug("Dashboard not found in path_parts for native_filters_key")
                if dashboard_ref:
                    # Resolve slug to numeric ID — the filter_state API requires a numeric ID
                    resolved_id = None
                    try:
                        resolved_id = int(dashboard_ref)
                    except (ValueError, TypeError):
                        try:
                            dash_resp = await self.get_dashboard_async(dashboard_ref)
                            dash_data = (
                                dash_resp.get("result", dash_resp)
                                if isinstance(dash_resp, dict)
                                else {}
                            )
                            raw_id = dash_data.get("id")
                            if raw_id is not None:
                                resolved_id = int(raw_id)
                        except Exception as e:
                            app_logger.warning(
                                "[parse_dashboard_url_for_filters_async][Warning] Failed to resolve dashboard slug '%s' to ID: %s",
                                dashboard_ref,
                                e,
                            )
                    if resolved_id is not None:
                        filter_data = await self.extract_native_filters_from_key_async(
                            resolved_id, native_filters_key
                        )
                        result["filter_type"] = "native_filters_key"
                        result["dashboard_id"] = resolved_id
                        result["filters"] = filter_data
                        return result
                    else:
                        app_logger.warning(
                            "[parse_dashboard_url_for_filters_async][Warning] Could not resolve dashboard_id from URL for native_filters_key"
                        )
                        return result
            # Check for native_filters in query params (direct filter values)
            native_filters = query_params.get("native_filters", [None])[0]
            if native_filters:
                try:
                    parsed_filters = json.loads(native_filters)
                    result["filter_type"] = "native_filters"
                    result["filters"] = {"dataMask": parsed_filters}
                    return result
                except json.JSONDecodeError as e:
                    app_logger.warning(
                        "[parse_dashboard_url_for_filters_async][Warning] Failed to parse native_filters JSON: %s",
                        e,
                    )
            return result
    # #endregion parse_dashboard_url_for_filters_async
# #endregion AsyncSupersetClient
# #endregion AsyncSupersetClientModule
