# #region BackupPlugin [TYPE Module] [SEMANTICS backup, export, archive, superset, dashboard]
# @BRIEF A plugin that provides functionality to back up Superset dashboards.
# @LAYER App
# @RELATION IMPLEMENTS -> PluginBase
# @RELATION DEPENDS_ON -> [EXT:Library:superset_tool.client]
# @RELATION DEPENDS_ON -> [EXT:Library:superset_tool.utils]
# @RELATION DEPENDS_ON -> [TaskContext]

from pathlib import Path
from typing import Any

from requests.exceptions import RequestException

from ..core.logger import belief_scope, logger as app_logger
from ..core.plugin_base import PluginBase
from ..core.superset_client import SupersetClient
from ..core.task_manager.context import TaskContext
from ..core.utils.fileio import RetentionPolicy, archive_exports, consolidate_archive_folders, remove_empty_directories, sanitize_filename, save_and_unpack_dashboard
from ..core.utils.network import SupersetAPIError
from ..dependencies import get_config_manager


# #region BackupPlugin [TYPE Class]
# @BRIEF Implementation of the backup plugin logic.
class BackupPlugin(PluginBase):
    """
    A plugin to back up Superset dashboards.
    """

    @property
    # region id [TYPE Function]
    # @PURPOSE: Returns the unique identifier for the backup plugin.
    # @PRE  Plugin instance exists.
    # @POST  Returns string ID.
    # @RETURN  str - "superset-backup"
    def id(self) -> str:
        with belief_scope("id"):
            return "superset-backup"
    # endregion id

    @property
    # region name [TYPE Function]
    # @PURPOSE: Returns the human-readable name of the backup plugin.
    # @PRE  Plugin instance exists.
    # @POST  Returns string name.
    # @RETURN  str - Plugin name.
    def name(self) -> str:
        with belief_scope("name"):
            return "Superset Dashboard Backup"
    # endregion name

    @property
    # region description [TYPE Function]
    # @PURPOSE: Returns a description of the backup plugin.
    # @PRE  Plugin instance exists.
    # @POST  Returns string description.
    # @RETURN  str - Plugin description.
    def description(self) -> str:
        with belief_scope("description"):
            return "Backs up all dashboards from a Superset instance."
    # endregion description

    @property
    # region version [TYPE Function]
    # @PURPOSE: Returns the version of the backup plugin.
    # @PRE  Plugin instance exists.
    # @POST  Returns string version.
    # @RETURN  str - "1.0.0"
    def version(self) -> str:
        with belief_scope("version"):
            return "1.0.0"
    # endregion version

    @property
    # region ui_route [TYPE Function]
    # @PURPOSE: Returns the frontend route for the backup plugin.
    # @RETURN  str - "/tools/backups"
    def ui_route(self) -> str:
        with belief_scope("ui_route"):
            return "/tools/backups"
    # endregion ui_route

    # region get_schema [TYPE Function]
    # @PURPOSE: Returns the JSON schema for backup plugin parameters.
    # @PRE  Plugin instance exists.
    # @POST  Returns dictionary schema.
    # @RETURN  Dict[str, Any] - JSON schema.
    def get_schema(self) -> dict[str, Any]:
        with belief_scope("get_schema"):
            config_manager = get_config_manager()
            envs = [e.name for e in config_manager.get_environments()]
        config_manager.get_config().settings.storage.root_path

        return {
            "type": "object",
            "properties": {
                "env": {
                    "type": "string",
                    "title": "Environment",
                    "description": "The Superset environment to back up.",
                    "enum": envs if envs else [],
                },
            },
            "required": ["env"],
        }
    # endregion get_schema

    # region execute [TYPE Function]
    # @PURPOSE: Executes the dashboard backup logic with TaskContext support.
    # @PARAM  params (Dict[str, Any]) - Backup parameters (env, backup_path, dashboard_ids).
    # @PARAM  context (Optional[TaskContext]) - Task context for logging with source attribution.
    # @PRE  Target environment must be configured. params must be a dictionary.
    # @POST  All dashboards are exported and archived.
    async def execute(self, params: dict[str, Any], context: TaskContext | None = None):
        with belief_scope("execute"):
            config_manager = get_config_manager()

            # Support both parameter names: environment_id (for task creation) and env (for direct calls)
            env_id = params.get("environment_id") or params.get("env")
            dashboard_ids = params.get("dashboard_ids") or params.get("dashboards")

            # Log the incoming parameters for debugging
            log = context.logger if context else app_logger
            log.info(f"Backup parameters received: env_id={env_id}, dashboard_ids={dashboard_ids}")

            # Resolve environment name if environment_id is provided
            if env_id:
                env_config = next((e for e in config_manager.get_environments() if e.id == env_id), None)
                if env_config:
                    params["env"] = env_config.name

            env = params.get("env")
            if not env:
                raise KeyError("env")

            log.info(f"Backup started for environment: {env}, selected dashboards: {dashboard_ids}")

            storage_settings = config_manager.get_config().settings.storage
            # Use 'backups' subfolder within the storage root
            backup_path = Path(storage_settings.root_path) / "backups"

            # Use TaskContext logger if available, otherwise fall back to app_logger
            log = context.logger if context else app_logger

            # Create sub-loggers for different components
            superset_log = log.with_source("superset_api") if context else log
            storage_log = log.with_source("storage") if context else log

            log.info(f"Starting backup for environment: {env}")

            try:
                config_manager = get_config_manager()
                if not config_manager.has_environments():
                    raise ValueError("No Superset environments configured. Please add an environment in Settings.")

                env_config = config_manager.get_environment(env)
                if not env_config:
                    raise ValueError(f"Environment '{env}' not found in configuration.")

                client = SupersetClient(env_config)

                # Get all dashboards
                all_dashboard_count, all_dashboard_meta = client.get_dashboards()
                superset_log.info(f"Found {all_dashboard_count} total dashboards in environment")

                # Filter dashboards if specific IDs are provided
                if dashboard_ids:
                    dashboard_ids_int = [int(did) for did in dashboard_ids]
                    dashboard_meta = [db for db in all_dashboard_meta if db.get('id') in dashboard_ids_int]
                    dashboard_count = len(dashboard_meta)
                    superset_log.info(f"Filtered to {dashboard_count} selected dashboards: {dashboard_ids_int}")
                else:
                    dashboard_count = all_dashboard_count
                    superset_log.info("No dashboard filter applied - backing up all dashboards")
                    dashboard_meta = all_dashboard_meta

                if dashboard_count == 0:
                    log.info("No dashboards to back up")
                    return {
                        "status": "NO_DASHBOARDS",
                        "environment": env,
                        "backup_root": str(backup_path / env.upper()),
                        "total_dashboards": 0,
                        "backed_up_dashboards": 0,
                        "failed_dashboards": 0,
                        "dashboards": [],
                        "failures": []
                    }

                total = len(dashboard_meta)
                backed_up_dashboards = []
                failed_dashboards = []
                for idx, db in enumerate(dashboard_meta, 1):
                    dashboard_id = db.get('id')
                    dashboard_title = db.get('dashboard_title', 'Unknown Dashboard')
                    if not dashboard_id:
                        continue

                    # Report progress
                    progress_pct = (idx / total) * 100
                    log.progress(f"Backing up dashboard: {dashboard_title}", percent=progress_pct)

                    try:
                        dashboard_base_dir_name = sanitize_filename(f"{dashboard_title}")
                        dashboard_dir = backup_path / env.upper() / dashboard_base_dir_name
                        dashboard_dir.mkdir(parents=True, exist_ok=True)

                        zip_content, filename = client.export_dashboard(dashboard_id)
                        superset_log.debug(f"Exported dashboard: {dashboard_title}")

                        save_and_unpack_dashboard(
                            zip_content=zip_content,
                            original_filename=filename,
                            output_dir=dashboard_dir,
                            unpack=False
                        )

                        archive_exports(str(dashboard_dir), policy=RetentionPolicy())
                        storage_log.debug(f"Archived dashboard: {dashboard_title}")
                        backed_up_dashboards.append({
                            "id": dashboard_id,
                            "title": dashboard_title,
                            "path": str(dashboard_dir)
                        })

                    except (SupersetAPIError, RequestException, OSError) as db_error:
                        log.error(f"Failed to export dashboard {dashboard_title} (ID: {dashboard_id}): {db_error}")
                        failed_dashboards.append({
                            "id": dashboard_id,
                            "title": dashboard_title,
                            "error": str(db_error)
                        })
                        continue

                consolidate_archive_folders(backup_path / env.upper())
                remove_empty_directories(str(backup_path / env.upper()))

                log.info(f"Backup completed successfully for {env}")
                return {
                    "status": "SUCCESS" if not failed_dashboards else "PARTIAL_SUCCESS",
                    "environment": env,
                    "backup_root": str(backup_path / env.upper()),
                    "total_dashboards": total,
                    "backed_up_dashboards": len(backed_up_dashboards),
                    "failed_dashboards": len(failed_dashboards),
                    "dashboards": backed_up_dashboards,
                    "failures": failed_dashboards
                }

            except (OSError, RequestException, KeyError) as e:
                log.error(f"Fatal error during backup for {env}: {e}")
                raise e
    # endregion execute
# #endregion BackupPlugin
# #endregion BackupPlugin
