# Copyright (c) CloudZero - ALL RIGHTS RESERVED - PROPRIETARY AND CONFIDENTIAL
# Unauthorized copying of this file and/or project, via any medium is strictly prohibited.
# Direct all questions to legal@cloudzero.com
#

"""
Settings migration system for CZ-OPS CLI.

This module provides the migration framework and specific migrations for upgrading
settings files from older versions to newer versions automatically.
"""

import json
import shutil
from abc import ABC, abstractmethod
from datetime import datetime
from pathlib import Path
from typing import Any

from src.settings.constants import _CZOPS_SETTINGS_FILENAME, CURRENT_SETTINGS_VERSION, MINIMUM_SUPPORTED_VERSION


class SettingsMigrationError(Exception):
    """Error during settings migration"""

    pass


class SettingsTooOldError(SettingsMigrationError):
    """Settings version is too old to migrate automatically"""

    pass


class SettingsMigration(ABC):
    """Base class for settings migrations"""

    @property
    @abstractmethod
    def from_version(self) -> int:
        """Source version this migration upgrades from"""
        pass

    @property
    @abstractmethod
    def to_version(self) -> int:
        """Target version this migration upgrades to"""
        pass

    @abstractmethod
    def migrate(self, settings_dict: dict[str, Any]) -> dict[str, Any]:
        """
        Apply migration to settings dictionary.

        Args:
            settings_dict: Current settings dictionary

        Returns:
            Updated settings dictionary with migration applied
        """
        pass

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(v{self.from_version} -> v{self.to_version})"


class MigrationV23ToV24(SettingsMigration):
    """Migration from version 23 to 24: Transition to new settings framework"""

    from_version = 23
    to_version = 24

    def migrate(self, settings_dict: dict[str, Any]) -> dict[str, Any]:
        # Version 24 introduced the migration framework itself
        # No structural changes needed, just version bump
        settings_dict["version"] = self.to_version
        return settings_dict


class MigrationV24ToV25(SettingsMigration):
    """Migration from version 24 to 25: Add telemetry_enabled field"""

    from_version = 24
    to_version = 25

    def migrate(self, settings_dict: dict[str, Any]) -> dict[str, Any]:
        # Add telemetry_enabled field with default value
        settings_dict["telemetry_enabled"] = settings_dict.get("telemetry_enabled", True)
        settings_dict["version"] = self.to_version
        return settings_dict


class MigrationV25ToV26(SettingsMigration):
    """Migration from version 25 to 26: Add auto_update_check field and rename session to session_data"""

    from_version = 25
    to_version = 26

    def migrate(self, settings_dict: dict[str, Any]) -> dict[str, Any]:
        # Add auto_update_check field
        settings_dict["auto_update_check"] = settings_dict.get("auto_update_check", True)

        # Rename session to session_data (example of field renaming)
        if "session" in settings_dict:
            settings_dict["session_data"] = settings_dict.pop("session")

        settings_dict["version"] = self.to_version
        return settings_dict


class MigrationV26ToV27(SettingsMigration):
    """Migration from version 26 to 27: Remove v2/ from API endpoint (v2 and v3 will add their prefixes)"""

    from_version = 26
    to_version = 27

    def migrate(self, settings_dict: dict[str, Any]) -> dict[str, Any]:
        # Update the API endpoint to remove v2/ suffix if present
        if "api" in settings_dict and "endpoint" in settings_dict["api"]:
            endpoint = settings_dict["api"]["endpoint"]
            # Remove trailing v2/ or /v2/ from the endpoint
            if endpoint.endswith("/v2/"):
                settings_dict["api"]["endpoint"] = endpoint[:-4]  # Remove "/v2/"
            elif endpoint.endswith("v2/"):
                settings_dict["api"]["endpoint"] = endpoint[:-3]  # Remove "v2/"
            elif endpoint.endswith("/v2"):
                settings_dict["api"]["endpoint"] = endpoint[:-3]  # Remove "/v2"
            # Ensure endpoint ends with a single slash
            if not settings_dict["api"]["endpoint"].endswith("/"):
                settings_dict["api"]["endpoint"] += "/"

        settings_dict["version"] = self.to_version
        return settings_dict


class MigrationV27ToV28(SettingsMigration):
    """Migration from version 27 to 28: Add api_v4 and api_version settings, update auth configuration"""

    from_version = 27
    to_version = 28

    def migrate(self, settings_dict: dict[str, Any]) -> dict[str, Any]:
        # Add api_v4 configuration with default endpoint and auth settings
        if "api_v4" not in settings_dict:
            settings_dict["api_v4"] = {
                "endpoint": "https://5dlbdxtri1.execute-api.us-east-1.amazonaws.com/live/czops/",
                "auth": {
                    "CLIENT_ID": "0oa1mpa8eqizsCscq358",
                    "OKTA_DOMAIN": "cloudzero.okta.com",
                    "LOGIN_REDIRECT_URI": "http://127.0.0.1:8042/callback",
                },
            }
        else:
            # If api_v4 exists but doesn't have auth, add it
            if "auth" not in settings_dict["api_v4"]:
                settings_dict["api_v4"]["auth"] = {}

            # Always set/update auth values, ensuring LOGIN_REDIRECT_URI is set to specific value
            settings_dict["api_v4"]["auth"].update(
                {
                    "CLIENT_ID": "0oa1mpa8eqizsCscq358",
                    "OKTA_DOMAIN": "cloudzero.okta.com",
                    "LOGIN_REDIRECT_URI": "http://127.0.0.1:8042/callback",
                }
            )

        settings_dict["version"] = self.to_version
        return settings_dict


class MigrationV28ToV29(SettingsMigration):
    """Migration from version 28 to 29: Update to use Okta default authorization server endpoints"""

    from_version = 28
    to_version = 29

    def migrate(self, settings_dict: dict[str, Any]) -> dict[str, Any]:
        # This migration updates the OAuth endpoint URLs from org authorization server
        # to default authorization server. The endpoint change is handled in the code,
        # not in settings, so this migration just bumps the version to indicate
        # compatibility with the new default authorization server configuration.

        # No settings changes needed - the endpoint URLs are constructed at runtime
        # from the existing OKTA_DOMAIN setting with the new /oauth2/default/v1/ pattern

        settings_dict["version"] = self.to_version
        return settings_dict


class MigrationV29ToV30(SettingsMigration):
    """Migration from version 29 to 30: Remove deprecated API settings (api, legacy_api, api_version)"""

    from_version = 29
    to_version = 30

    def migrate(self, settings_dict: dict[str, Any]) -> dict[str, Any]:
        # Remove deprecated API settings that are no longer supported
        # Users should use api_v4 for all operations
        deprecated_keys = ["api", "legacy_api", "api_version"]

        for key in deprecated_keys:
            if key in settings_dict:
                # Log the removal for debugging purposes
                print(f"[MIGRATION] Removing deprecated setting: {key}")
                del settings_dict[key]

        settings_dict["version"] = self.to_version
        return settings_dict


# Migration registry - ordered list of migrations
SETTINGS_MIGRATIONS = [
    MigrationV23ToV24(),
    MigrationV24ToV25(),
    MigrationV25ToV26(),
    MigrationV26ToV27(),
    MigrationV27ToV28(),
    MigrationV28ToV29(),
    MigrationV29ToV30(),
]


def backup_settings_file() -> Path | None:
    """
    Create a backup of the current settings file.

    Returns:
        Path to the backup file, or None if no settings file exists
    """
    settings_path = Path(_CZOPS_SETTINGS_FILENAME).expanduser()
    if not settings_path.exists():
        return None

    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_path = settings_path.parent / f"czops_backup_{timestamp}.json"
    shutil.copy2(settings_path, backup_path)
    return backup_path


def backup_and_reset_settings(reason: str = "Settings too old") -> Path | None:
    """
    Create a backup of the current settings file and then delete the original.

    This function is used when settings are too old to migrate automatically,
    providing a clean slate for the user while preserving their old settings.

    Args:
        reason: Reason for the backup and reset (for logging purposes)

    Returns:
        Path to the backup file, or None if no settings file exists
    """
    settings_path = Path(_CZOPS_SETTINGS_FILENAME).expanduser()
    if not settings_path.exists():
        return None

    # Create backup with descriptive filename
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_path = settings_path.parent / f"czops_settings_backup_{timestamp}.json"

    try:
        # Create backup
        shutil.copy2(settings_path, backup_path)
        print(f"  📦 Created backup of old settings: {backup_path}")

        # Remove the old settings file
        settings_path.unlink()
        print(f"  🗑️  Removed outdated settings file: {settings_path}")

        return backup_path
    except Exception as e:
        print(f"  ❌ Failed to backup and reset settings: {str(e)}")
        # If backup creation fails, don't delete the original file
        return None


def apply_migrations(settings_dict: dict[str, Any], from_version: int, to_version: int) -> dict[str, Any]:
    """
    Apply a series of migrations to upgrade settings from one version to another.

    Args:
        settings_dict: Current settings dictionary
        from_version: Starting version
        to_version: Target version

    Returns:
        Updated settings dictionary

    Raises:
        SettingsMigrationError: If migration fails
    """
    if from_version >= to_version:
        return settings_dict

    # Find applicable migrations
    applicable_migrations = []
    for migration in SETTINGS_MIGRATIONS:
        if migration.from_version >= from_version and migration.to_version <= to_version:
            applicable_migrations.append(migration)

    # Sort migrations by from_version to ensure correct order
    applicable_migrations.sort(key=lambda m: m.from_version)

    # Apply migrations sequentially
    current_settings = settings_dict.copy()
    for migration in applicable_migrations:
        try:
            print(f"  Applying migration: {migration}")
            current_settings = migration.migrate(current_settings)
        except Exception as e:
            raise SettingsMigrationError(f"Failed to apply {migration}: {str(e)}") from e

    return current_settings


def load_and_migrate_settings() -> dict[str, Any] | None:
    """
    Load settings and automatically migrate if needed.

    Returns:
        Migrated settings dictionary, or None if migration not needed

    Raises:
        SettingsTooOldError: If settings are too old to migrate
        SettingsMigrationError: If migration fails
    """
    settings_path = Path(_CZOPS_SETTINGS_FILENAME).expanduser()

    if not settings_path.exists():
        return None

    try:
        with open(settings_path) as f:
            raw_settings = json.load(f)
    except (json.JSONDecodeError, OSError) as e:
        raise SettingsMigrationError(f"Failed to load settings file: {str(e)}") from e

    current_version = raw_settings.get("version", 0)

    # Check if settings are too old
    if current_version < MINIMUM_SUPPORTED_VERSION:
        raise SettingsTooOldError(
            f"Settings version {current_version} is too old to migrate automatically. "
            f"Minimum supported version is {MINIMUM_SUPPORTED_VERSION}. "
            f"Please run 'czops configure' to recreate your settings."
        )

    # Check if migration is needed
    if current_version >= CURRENT_SETTINGS_VERSION:
        # Settings are current or newer
        return raw_settings

    # Perform migration
    print(f"\nMigrating settings from version {current_version} to {CURRENT_SETTINGS_VERSION}...")

    # Create backup
    backup_path = backup_settings_file()
    if backup_path:
        print(f"  Created backup: {backup_path}")

    # Apply migrations
    try:
        migrated_settings = apply_migrations(raw_settings, current_version, CURRENT_SETTINGS_VERSION)

        # Save migrated settings
        with open(settings_path, "w") as f:
            json.dump(migrated_settings, f, indent=4)

        print("  ✓ Settings migration complete!\n")
        return migrated_settings

    except Exception as e:
        print(f"\n  ✗ Migration failed: {str(e)}")
        if backup_path:
            print(f"  You can restore from backup: {backup_path}")
        raise
