# [DEF:backend.src.core.migration.dry_run_orchestrator:Module]
# @TIER: STANDARD
# @SEMANTICS: migration, dry_run, diff, risk, superset
# @PURPOSE: Compute pre-flight migration diff and risk scoring without apply.
# @LAYER: Core
# @RELATION: DEPENDS_ON -> backend.src.core.superset_client
# @RELATION: DEPENDS_ON -> backend.src.core.migration_engine
# @RELATION: DEPENDS_ON -> backend.src.core.migration.archive_parser
# @RELATION: DEPENDS_ON -> backend.src.core.migration.risk_assessor
# @INVARIANT: Dry run is informative only and must not mutate target environment.

from datetime import datetime, timezone
import json
from typing import Any, Dict, List

from sqlalchemy.orm import Session

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


# [DEF:MigrationDryRunService:Class]
# @PURPOSE: Build deterministic diff/risk payload for migration pre-flight.
class MigrationDryRunService:
    # [DEF:__init__: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()
    # [/DEF:__init__:Function]

    # [DEF:run:Function]
    # @PURPOSE: Execute full dry-run computation for selected dashboards.
    # @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.
    def run(
        self,
        selection: DashboardSelection,
        source_client: SupersetClient,
        target_client: SupersetClient,
        db: Session,
    ) -> Dict[str, Any]:
        with belief_scope("MigrationDryRunService.run"):
            logger.explore("[MigrationDryRunService.run][EXPLORE] starting dry-run pipeline")
            engine = MigrationEngine()
            db_mapping = self._load_db_mapping(db, selection) if selection.replace_db_config else {}
            transformed = {"dashboards": {}, "charts": {}, "datasets": {}}

            dashboards_preview = 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, _ = source_client.export_dashboard(int(dashboard_id))
                with create_temp_file(content=exported_content, suffix=".zip") as source_zip:
                    with 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 = 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 = 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("[MigrationDryRunService.run][REASON] dry-run payload assembled")
            return {
                "generated_at": datetime.now(timezone.utc).isoformat(),
                "selection": selection.model_dump(),
                "selected_dashboard_titles": selected_titles,
                "diff": diff,
                "summary": summary,
                "risk": score_risks(risk),
            }
    # [/DEF:run:Function]

    # [DEF:_load_db_mapping: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}
    # [/DEF:_load_db_mapping:Function]

    # [DEF:_accumulate_objects: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
    # [/DEF:_accumulate_objects:Function]

    # [DEF:_index_by_uuid: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
    # [/DEF:_index_by_uuid:Function]

    # [DEF:_build_object_diff:Function]
    # @PURPOSE: Compute create/update/delete buckets by UUID+signature.
    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}
    # [/DEF:_build_object_diff:Function]

    # [DEF:_build_target_signatures:Function]
    # @PURPOSE: Pull target metadata and normalize it into comparable signatures.
    def _build_target_signatures(self, client: SupersetClient) -> Dict[str, List[Dict[str, Any]]]:
        _, dashboards = client.get_dashboards(query={
            "columns": ["uuid", "dashboard_title", "slug", "position_json", "json_metadata", "description", "owners"],
        })
        _, datasets = client.get_datasets(query={
            "columns": ["uuid", "table_name", "schema", "database_uuid", "sql", "columns", "metrics"],
        })
        _, charts = 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")],
        }
    # [/DEF:_build_target_signatures:Function]

    # [DEF:_build_risks:Function]
    # @PURPOSE: Build risk items for missing datasource, broken refs, overwrite, owner mismatch.
    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 build_risks(source_objects, target_objects, diff, target_client)
    # [/DEF:_build_risks:Function]


# [/DEF:MigrationDryRunService:Class]
# [/DEF:backend.src.core.migration.dry_run_orchestrator:Module]
