# #region MigrationDryRunOrchestratorModule [C:5] [TYPE Module] [SEMANTICS sqlalchemy, migration, orchestration, diff, migration-dry-run-service]
# @defgroup Migration Module group.
# @BRIEF Compute pre-flight migration diff and risk scoring without apply.
# @LAYER Domain
# @RELATION DEPENDS_ON -> [SupersetClient]
# @RELATION DEPENDS_ON -> [MigrationEngine]
# @RELATION DEPENDS_ON -> [MigrationArchiveParser]
# @RELATION DEPENDS_ON -> [RiskAssessorModule]
# @INVARIANT  Dry run is informative only and must not mutate target environment.
# @PRE  Source and target environments configured
# @POST  Dry-run diff returned without mutation
# @SIDE_EFFECT  Reads source environment (read-only)
# @DATA_CONTRACT  EnvironmentConfig -> MigrationDiffReport
from datetime import UTC, datetime
import json
from typing import Any

from sqlalchemy.orm import Session

from ...models.dashboard import DashboardSelection
from ...models.mapping import DatabaseMapping
from ..logger import belief_scope, logger
from ..migration_engine import MigrationEngine
from ..superset_client import SupersetClient
from ..utils.fileio import create_temp_file
from .archive_parser import MigrationArchiveParser
from .risk_assessor import build_risks, score_risks


# #region MigrationDryRunService [TYPE Class]
# @defgroup Migration Module group.
# @BRIEF Build deterministic diff/risk payload for migration pre-flight.
# @RELATION DEPENDS_ON -> [__init__]
# @RELATION DEPENDS_ON -> [run]
# @RELATION DEPENDS_ON -> [_load_db_mapping]
# @RELATION DEPENDS_ON -> [_accumulate_objects]
# @RELATION DEPENDS_ON -> [_index_by_uuid]
# @RELATION DEPENDS_ON -> [_build_object_diff]
# @RELATION DEPENDS_ON -> [_build_target_signatures]
# @RELATION DEPENDS_ON -> [_build_risks]
class MigrationDryRunService:
    # #region __init__ [TYPE Function]
    # @PURPOSE: Wire parser dependency for archive object extraction.
    # @PRE  parser can be omitted to use default implementation.
    # @POST  Service is ready to calculate dry-run payload.
    def __init__(self, parser: MigrationArchiveParser | None = None):
        self.parser = parser or MigrationArchiveParser()
    # #endregion __init__
    # #region run [TYPE Function]
# @ingroup Migration
    # @PURPOSE: Execute full dry-run computation for selected dashboards.
    # @RELATION DEPENDS_ON -> [_load_db_mapping]
    # @RELATION DEPENDS_ON -> [_accumulate_objects]
    # @RELATION DEPENDS_ON -> [_build_target_signatures]
    # @RELATION DEPENDS_ON -> [_build_object_diff]
    # @RELATION DEPENDS_ON -> [_build_risks]
    # @PRE  source/target clients are authenticated and selection validated by caller.
    # @POST  Returns JSON-serializable pre-flight payload with summary, diff and risk.
    # @SIDE_EFFECT  Reads source export archives and target metadata via network.
    async def run(
        self,
        selection: DashboardSelection,
        source_client: SupersetClient,
        target_client: SupersetClient,
        db: Session,
    ) -> dict[str, Any]:
        with belief_scope("MigrationDryRunService.run"):
            logger.reason(
                "Starting dry-run pipeline",
                extra={"src": "MigrationDryRunService.run"},
            )
            engine = MigrationEngine()
            db_mapping = (
                self._load_db_mapping(db, selection)
                if selection.replace_db_config
                else {}
            )
            transformed = {"dashboards": {}, "charts": {}, "datasets": {}}
            dashboards_preview = await source_client.get_dashboards_summary()
            selected_preview = {
                item["id"]: item
                for item in dashboards_preview
                if item.get("id") in selection.selected_ids
            }
            for dashboard_id in selection.selected_ids:
                exported_content, _ = await source_client.export_dashboard(int(dashboard_id))
                with create_temp_file(
                    content=exported_content, suffix=".zip"
                ) as source_zip, create_temp_file(suffix=".zip") as transformed_zip:
                    success = engine.transform_zip(
                        str(source_zip),
                        str(transformed_zip),
                        db_mapping,
                        strip_databases=False,
                        target_env_id=selection.target_env_id,
                        fix_cross_filters=selection.fix_cross_filters,
                    )
                    if not success:
                        raise ValueError(
                            f"Failed to transform export archive for dashboard {dashboard_id}"
                        )
                    extracted = self.parser.extract_objects_from_zip(
                        str(transformed_zip)
                    )
                    self._accumulate_objects(transformed, extracted)
            source_objects = {
                key: list(value.values()) for key, value in transformed.items()
            }
            target_objects = await self._build_target_signatures(target_client)
            diff = {
                "dashboards": self._build_object_diff(
                    source_objects["dashboards"], target_objects["dashboards"]
                ),
                "charts": self._build_object_diff(
                    source_objects["charts"], target_objects["charts"]
                ),
                "datasets": self._build_object_diff(
                    source_objects["datasets"], target_objects["datasets"]
                ),
            }
            risk = await self._build_risks(
                source_objects, target_objects, diff, target_client
            )
            summary = {
                "dashboards": {
                    action: len(diff["dashboards"][action])
                    for action in ("create", "update", "delete")
                },
                "charts": {
                    action: len(diff["charts"][action])
                    for action in ("create", "update", "delete")
                },
                "datasets": {
                    action: len(diff["datasets"][action])
                    for action in ("create", "update", "delete")
                },
                "selected_dashboards": len(selection.selected_ids),
            }
            selected_titles = [
                selected_preview[dash_id]["title"]
                for dash_id in selection.selected_ids
                if dash_id in selected_preview
            ]
            logger.reason(
                "Dry-run payload assembled",
                extra={"src": "MigrationDryRunService.run"},
            )
            return {
                "generated_at": datetime.now(UTC).isoformat(),
                "selection": selection.model_dump(),
                "selected_dashboard_titles": selected_titles,
                "diff": diff,
                "summary": summary,
                "risk": score_risks(risk),
            }
    # #endregion run
    # #region _load_db_mapping [TYPE Function]
    # @PURPOSE: Resolve UUID mapping for optional DB config replacement.
    def _load_db_mapping(
        self, db: Session, selection: DashboardSelection
    ) -> dict[str, str]:
        rows = (
            db.query(DatabaseMapping)
            .filter(
                DatabaseMapping.source_env_id == selection.source_env_id,
                DatabaseMapping.target_env_id == selection.target_env_id,
            )
            .all()
        )
        return {row.source_db_uuid: row.target_db_uuid for row in rows}
    # #endregion _load_db_mapping
    # #region _accumulate_objects [TYPE Function]
    # @PURPOSE: Merge extracted resources by UUID to avoid duplicates.
    def _accumulate_objects(
        self,
        target: dict[str, dict[str, dict[str, Any]]],
        source: dict[str, list[dict[str, Any]]],
    ) -> None:
        for object_type in ("dashboards", "charts", "datasets"):
            for item in source.get(object_type, []):
                uuid = item.get("uuid")
                if uuid:
                    target[object_type][str(uuid)] = item
    # #endregion _accumulate_objects
    # #region _index_by_uuid [TYPE Function]
    # @PURPOSE: Build UUID-index map for normalized resources.
    def _index_by_uuid(
        self, objects: list[dict[str, Any]]
    ) -> dict[str, dict[str, Any]]:
        indexed: dict[str, dict[str, Any]] = {}
        for obj in objects:
            uuid = obj.get("uuid")
            if uuid:
                indexed[str(uuid)] = obj
        return indexed
    # #endregion _index_by_uuid
    # #region _build_object_diff [TYPE Function]
    # @PURPOSE: Compute create/update/delete buckets by UUID+signature.
    # @RELATION DEPENDS_ON -> [_index_by_uuid]
    def _build_object_diff(
        self, source_objects: list[dict[str, Any]], target_objects: list[dict[str, Any]]
    ) -> dict[str, list[dict[str, Any]]]:
        target_index = self._index_by_uuid(target_objects)
        created: list[dict[str, Any]] = []
        updated: list[dict[str, Any]] = []
        deleted: list[dict[str, Any]] = []
        for source_obj in source_objects:
            source_uuid = str(source_obj.get("uuid"))
            target_obj = target_index.get(source_uuid)
            if not target_obj:
                created.append({"uuid": source_uuid, "title": source_obj.get("title")})
                continue
            if source_obj.get("signature") != target_obj.get("signature"):
                updated.append(
                    {
                        "uuid": source_uuid,
                        "title": source_obj.get("title"),
                        "target_title": target_obj.get("title"),
                    }
                )
        return {"create": created, "update": updated, "delete": deleted}
    # #endregion _build_object_diff
    # #region _build_target_signatures [TYPE Function]
    # @PURPOSE: Pull target metadata and normalize it into comparable signatures.
    async def _build_target_signatures(
        self, client: SupersetClient
    ) -> dict[str, list[dict[str, Any]]]:
        _, dashboards = await client.get_dashboards(
            query={
                "columns": [
                    "uuid",
                    "dashboard_title",
                    "slug",
                    "position_json",
                    "json_metadata",
                    "description",
                    "owners",
                ],
            }
        )
        _, datasets = await client.get_datasets(
            query={
                "columns": [
                    "uuid",
                    "table_name",
                    "schema",
                    "database_uuid",
                    "sql",
                    "columns",
                    "metrics",
                ],
            }
        )
        _, charts = await client.get_charts(
            query={
                "columns": [
                    "uuid",
                    "slice_name",
                    "viz_type",
                    "params",
                    "query_context",
                    "datasource_uuid",
                    "dataset_uuid",
                ],
            }
        )
        return {
            "dashboards": [
                {
                    "uuid": str(item.get("uuid")),
                    "title": item.get("dashboard_title"),
                    "owners": item.get("owners") or [],
                    "signature": json.dumps(
                        {
                            "title": item.get("dashboard_title"),
                            "slug": item.get("slug"),
                            "position_json": item.get("position_json"),
                            "json_metadata": item.get("json_metadata"),
                            "description": item.get("description"),
                            "owners": item.get("owners"),
                        },
                        sort_keys=True,
                        default=str,
                    ),
                }
                for item in dashboards
                if item.get("uuid")
            ],
            "datasets": [
                {
                    "uuid": str(item.get("uuid")),
                    "title": item.get("table_name"),
                    "database_uuid": item.get("database_uuid"),
                    "signature": json.dumps(
                        {
                            "title": item.get("table_name"),
                            "schema": item.get("schema"),
                            "database_uuid": item.get("database_uuid"),
                            "sql": item.get("sql"),
                            "columns": item.get("columns"),
                            "metrics": item.get("metrics"),
                        },
                        sort_keys=True,
                        default=str,
                    ),
                }
                for item in datasets
                if item.get("uuid")
            ],
            "charts": [
                {
                    "uuid": str(item.get("uuid")),
                    "title": item.get("slice_name") or item.get("name"),
                    "dataset_uuid": item.get("datasource_uuid")
                    or item.get("dataset_uuid"),
                    "signature": json.dumps(
                        {
                            "title": item.get("slice_name") or item.get("name"),
                            "viz_type": item.get("viz_type"),
                            "params": item.get("params"),
                            "query_context": item.get("query_context"),
                            "datasource_uuid": item.get("datasource_uuid"),
                            "dataset_uuid": item.get("dataset_uuid"),
                        },
                        sort_keys=True,
                        default=str,
                    ),
                }
                for item in charts
                if item.get("uuid")
            ],
        }
    # #endregion _build_target_signatures
    # #region _build_risks [TYPE Function]
    # @PURPOSE: Build risk items for missing datasource, broken refs, overwrite, owner mismatch.
    async def _build_risks(
        self,
        source_objects: dict[str, list[dict[str, Any]]],
        target_objects: dict[str, list[dict[str, Any]]],
        diff: dict[str, dict[str, list[dict[str, Any]]]],
        target_client: SupersetClient,
    ) -> list[dict[str, Any]]:
        return await build_risks(source_objects, target_objects, diff, target_client)
    # #endregion _build_risks
# #endregion MigrationDryRunService
# #endregion MigrationDryRunOrchestratorModule
