"""
Main migrator class for the Slack to Google Chat migration tool
"""

from __future__ import annotations

import datetime
import json
import logging
import os
import signal
import time
from pathlib import Path
from typing import Any

from slack_chat_migrator.constants import SPACE_NAME_PREFIX
from slack_chat_migrator.core.channel_processor import ChannelProcessor
from slack_chat_migrator.core.checkpoint import (
    CheckpointData,
    clear_checkpoint,
    load_checkpoint,
    now_iso,
    save_checkpoint,
)
from slack_chat_migrator.core.cleanup import cleanup_channel_handlers
from slack_chat_migrator.core.config import load_config, load_space_mapping
from slack_chat_migrator.core.context import MigrationContext
from slack_chat_migrator.core.migration_logging import (
    log_migration_failure,
    log_migration_success,
)
from slack_chat_migrator.core.progress import ProgressTracker
from slack_chat_migrator.core.state import MigrationState
from slack_chat_migrator.services.chat.dry_run_service import DryRunChatService
from slack_chat_migrator.services.chat_adapter import ChatAdapter
from slack_chat_migrator.services.drive.dry_run_service import DryRunDriveService
from slack_chat_migrator.services.drive_adapter import DriveAdapter
from slack_chat_migrator.services.files.file import FileHandler
from slack_chat_migrator.services.messages.message_attachments import (
    MessageAttachmentProcessor,
)
from slack_chat_migrator.services.spaces.discovery import (
    load_existing_space_mappings,
    load_space_mappings,
    log_space_mapping_conflicts,
)
from slack_chat_migrator.services.user import generate_user_map
from slack_chat_migrator.services.user_resolver import UserResolver
from slack_chat_migrator.utils.api import get_gcp_service
from slack_chat_migrator.utils.logging import log_with_context
from slack_chat_migrator.utils.user_validation import (
    initialize_unmapped_user_tracking,
    log_unmapped_user_summary_for_dry_run,
    scan_channel_members_for_unmapped_users,
)


class SlackToChatMigrator:
    """Main class for migrating Slack exports to Google Chat."""

    def __init__(
        self,
        creds_path: str | None,
        export_path: str,
        workspace_admin: str | None,
        config_path: str,
        dry_run: bool = False,
        verbose: bool = False,
        update_mode: bool = False,
        debug_api: bool = False,
        message_error_schedule: dict[int, int] | None = None,
    ):
        """Initialize the migrator with the required parameters.

        ``message_error_schedule`` is test-only: maps 1-based message
        ordinal to HTTP status code for error injection in dry-run mode.

        In dry-run mode, ``creds_path`` and ``workspace_admin`` may be
        ``None`` — no real API calls are made.
        """
        self.creds_path = creds_path
        self.export_root = Path(export_path)
        self.workspace_admin: str | None = (
            workspace_admin.strip() if workspace_admin else None
        )
        self.config_path = Path(config_path)
        self.dry_run = dry_run
        self.verbose = verbose
        self.debug_api = debug_api
        self.update_mode = update_mode
        self._message_error_schedule = message_error_schedule

        if self.update_mode:
            log_with_context(
                logging.INFO, "Running in update mode - will update existing spaces"
            )

        # All mutable tracking state lives in MigrationState
        self.state = MigrationState()

        # Immutable data loaded once during init
        self.user_map: dict[str, str] = {}  # slack_user_id -> google_email
        self.users_without_email: list[
            dict[str, Any]
        ] = []  # List of users without email mappings
        self.progress_file = self.export_root / ".migration_progress.json"

        # Validate workspace admin email format (skip when None in dry-run)
        if self.workspace_admin is not None:
            if (
                "@" not in self.workspace_admin
                or self.workspace_admin.count("@") != 1
                or not self.workspace_admin.split("@")[0]
                or not self.workspace_admin.split("@")[1]
            ):
                raise ValueError(
                    f"Invalid workspace_admin email: '{self.workspace_admin}'. Must be a valid email address."
                )

        # Extract workspace domain from admin email for external user detection
        self.workspace_domain = (
            self.workspace_admin.split("@")[1] if self.workspace_admin else ""
        )

        # Initialize API clients
        self._validate_export_format()

        # Load config using the shared load_config function
        self.config = load_config(self.config_path)

        # Load space_mapping overrides from config YAML into state
        self.state.spaces.space_mapping = load_space_mapping(self.config_path)

        # Generate user mapping from users.json
        self.user_map, self.users_without_email, self.bot_user_ids = generate_user_map(
            self.export_root, self.config
        )

        # Initialize simple unmapped user tracking
        self.unmapped_user_tracker = initialize_unmapped_user_tracking()

        # Scan channel members to ensure all channel members have user mappings
        # This is crucial because Google Chat needs to add all channel members to spaces
        scan_channel_members_for_unmapped_users(
            self.unmapped_user_tracker,
            self.export_root,
            self.config,
            self.user_map,
        )

        # API services are initialized lazily by _initialize_api_services(),
        # called from migrate() or validate_permissions(). Typed as Any so
        # callers don't need union-attr ignores — all code paths guarantee
        # initialization before first use.
        self.chat: Any = None
        self.drive: Any = None
        self._api_services_initialized = False
        self._dry_run_chat_service: DryRunChatService | None = None
        self._progress_tracker: ProgressTracker | None = None

        # UserResolver is created in _initialize_api_services() after chat
        # is available, avoiding the two-phase init pattern (create with
        # chat=None, then mutate chat later).  Typed as Any so callers
        # don't need union-attr ignores — all code paths guarantee
        # initialization before first use (same pattern as chat/drive).
        self.user_resolver: Any = None

        # Load channel metadata from channels.json
        self.channels_meta, self.channel_id_to_name = self._load_channels_meta()

        # Create reverse mapping for convenience
        self.channel_name_to_id = {
            name: id for id, name in self.channel_id_to_name.items()
        }

        # Build immutable context from the now-populated attributes.
        # During Phase 1 of DI refactoring, both self.ctx.X and self.X
        # coexist; later phases migrate callers to use ctx directly.
        self.ctx = MigrationContext(
            export_root=self.export_root,
            creds_path=self.creds_path,
            workspace_admin=self.workspace_admin,
            workspace_domain=self.workspace_domain,
            dry_run=self.dry_run,
            update_mode=self.update_mode,
            verbose=self.verbose,
            debug_api=self.debug_api,
            config=self.config,
            user_map=self.user_map,
            users_without_email=self.users_without_email,
            bot_user_ids=self.bot_user_ids,
            channels_meta=self.channels_meta,
            channel_id_to_name=self.channel_id_to_name,
            channel_name_to_id=self.channel_name_to_id,
        )

    def _initialize_api_services(self) -> None:
        """Initialize Google API services after permission validation."""
        if self._api_services_initialized:
            return

        if self.dry_run:
            log_with_context(
                logging.INFO,
                "Dry-run mode: using no-op Chat and Drive services",
            )
            raw_chat = DryRunChatService(
                self.state,
                message_error_schedule=self._message_error_schedule,
            )
            self._dry_run_chat_service = raw_chat
            self.chat = ChatAdapter(raw_chat)
            self.drive = DriveAdapter(DryRunDriveService())
        else:
            # Live mode — creds_path and workspace_admin are guaranteed non-None
            # by CLI validation in validate_prerequisites()
            assert self.creds_path is not None
            assert self.workspace_admin is not None
            log_with_context(
                logging.INFO,
                "Initializing Google Chat and Drive API services...",
            )
            creds_path_str = str(self.creds_path)
            raw_chat = get_gcp_service(
                creds_path_str,
                self.workspace_admin,
                "chat",
                "v1",
                max_retries=self.config.max_retries,
                retry_delay=self.config.retry_delay,
            )
            self.chat = ChatAdapter(raw_chat)
            self.drive = DriveAdapter(
                get_gcp_service(
                    creds_path_str,
                    self.workspace_admin,
                    "drive",
                    "v3",
                    max_retries=self.config.max_retries,
                    retry_delay=self.config.retry_delay,
                )
            )

        # Create UserResolver now that chat is available — no two-phase init
        self.user_resolver = UserResolver(
            config=self.config,
            state=self.state,
            chat=self.chat,
            creds_path=self.creds_path,
            user_map=self.user_map,
            unmapped_user_tracker=self.unmapped_user_tracker,
            export_root=self.export_root,
            workspace_admin=self.workspace_admin,
            workspace_domain=self.workspace_domain,
        )

        self._api_services_initialized = True
        log_with_context(
            logging.INFO, "Google Chat and Drive API services initialized successfully"
        )

        # Initialize dependent services
        self._initialize_dependent_services()

    def _initialize_dependent_services(self) -> None:
        """Initialize services that depend on API clients."""
        # Initialize file handler with explicit deps (no migrator back-reference)
        self.file_handler = FileHandler(
            self.drive,
            self.chat,
            folder_id=None,
            config=self.config,
            workspace_domain=self.workspace_domain,
            user_map=self.user_map,
            user_resolver=self.user_resolver,
            state=self.state,
            dry_run=self.dry_run,
        )
        # FileHandler now handles its own drive folder initialization automatically

        # Initialize message attachment processor
        self.attachment_processor = MessageAttachmentProcessor(
            self.file_handler, dry_run=self.dry_run
        )

        # Reset mutable state for this run
        self.state.spaces.created_spaces.clear()
        self.state.context.current_channel = None

        # Permission validation is now handled by the CLI layer to avoid duplicates
        # The CLI will call validate_permissions() unless --skip_permission_check is used

        if self.verbose:
            log_with_context(
                logging.DEBUG, "Migrator initialized with verbose logging enabled"
            )

        # Load existing space mappings for update mode or file attachments
        load_existing_space_mappings(self.ctx, self.state, self.chat)

    def _validate_export_format(self) -> None:
        """Validate that the export directory has the expected structure."""
        # Check that the export root is a valid directory before inspecting contents
        if not self.export_root.is_dir():
            raise ValueError(
                f"Export path is not a valid directory: {self.export_root}"
            )

        if not (self.export_root / "channels.json").exists():
            log_with_context(
                logging.WARNING, "channels.json not found in export directory"
            )

        if not (self.export_root / "users.json").exists():
            log_with_context(
                logging.WARNING, "users.json not found in export directory"
            )
            raise ValueError(
                f"users.json not found in {self.export_root}. This file is required for user mapping."
            )

        # Check that at least one channel directory exists
        channel_dirs = [d for d in self.export_root.iterdir() if d.is_dir()]
        if not channel_dirs:
            raise ValueError(f"No channel directories found in {self.export_root}")

        # Check that each channel directory has at least one JSON file
        for ch_dir in channel_dirs:
            if not list(ch_dir.glob("*.json")):
                log_with_context(
                    logging.WARNING,
                    f"No JSON files found in channel directory {ch_dir.name}",
                )

    def _load_channels_meta(self) -> tuple[dict[str, Any], dict[str, str]]:
        """
        Load channel metadata from channels.json file.

        Returns:
            tuple: (name_to_data, id_to_name) where:
                - name_to_data: Dict mapping channel names to their metadata
                - id_to_name: Dict mapping channel IDs to channel names
        """
        channels_file = self.export_root / "channels.json"
        name_to_data = {}
        id_to_name = {}

        if channels_file.exists():
            with open(channels_file) as f_in:
                channels = json.load(f_in)
                name_to_data = {ch["name"]: ch for ch in channels}
                id_to_name = {ch["id"]: ch["name"] for ch in channels}

        return name_to_data, id_to_name

    def _get_space_name(self, channel: str) -> str:
        """Get a consistent display name for a Google Chat space based on channel name."""
        return f"{SPACE_NAME_PREFIX}{channel}"

    def _get_all_channel_names(self) -> list[str]:
        """Get a list of all channel names from the export directory."""
        return [d.name for d in self.export_root.iterdir() if d.is_dir()]

    def _emit_phase(self, phase: str) -> None:
        """Emit a phase-change event if a progress tracker is active."""
        if self._progress_tracker:
            self._progress_tracker.phase_change(phase)

    def migrate(self, progress_tracker: ProgressTracker | None = None) -> bool:
        """Main migration function that orchestrates the entire process.

        Args:
            progress_tracker: Optional tracker for emitting progress events
                to renderers (Rich, plain text).

        Returns:
            True on successful completion.
        """
        self._progress_tracker = progress_tracker
        migration_start_time = time.time()
        log_with_context(logging.INFO, "Starting migration process")

        # Set up signal handler to ensure we log migration status on interrupt
        def signal_handler(signum: int, frame: Any) -> None:
            """Handle SIGINT (Ctrl+C) gracefully.

            Raises KeyboardInterrupt so the existing ``except BaseException``
            block handles logging and cleanup in one place.

            Args:
                signum: Signal number received.
                frame: Current stack frame (unused).
            """
            raise KeyboardInterrupt("Migration interrupted by signal")

        # Install the signal handler
        old_signal_handler = signal.signal(signal.SIGINT, signal_handler)

        try:
            self._emit_phase("Initializing")

            # Ensure API services are initialized (if not done during permission checks)
            self._initialize_api_services()

            # Output directory should already be set up by CLI, but provide a sensible default
            if not self.state.context.output_dir:
                # Create default output directory with timestamp
                timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
                self.state.context.output_dir = f"migration_logs/run_{timestamp}"
                log_with_context(
                    logging.INFO,
                    f"Using default output directory: {self.state.context.output_dir}",
                )
                # Create the directory
                os.makedirs(self.state.context.output_dir, exist_ok=True)

            # Reset per-run state
            self.state.reset_for_run()

            # Load or create checkpoint for resumable migrations
            checkpoint_path = (
                Path(self.state.context.output_dir or ".")
                / ".migration_checkpoint.json"
            )
            checkpoint = load_checkpoint(checkpoint_path)
            if checkpoint:
                log_with_context(
                    logging.INFO,
                    f"Resuming migration from checkpoint: {len(checkpoint.completed_channels)} channels already completed",
                )
            else:
                checkpoint = CheckpointData(started_at=now_iso())

            # Report unmapped user issues before starting migration (if any detected during initialization)
            if self.unmapped_user_tracker.has_unmapped_users():
                unmapped_users = self.unmapped_user_tracker.get_unmapped_users_list()
                log_with_context(
                    logging.WARNING,
                    f"Found {len(unmapped_users)} unmapped users during setup: {', '.join(unmapped_users)}",
                )
                log_with_context(
                    logging.WARNING,
                    "These will be tracked during migration. Consider adding them to user_mapping_overrides in config.yaml.",
                )

            # In update mode, discover existing spaces via API
            if self.update_mode:
                discovered_spaces = load_space_mappings(
                    self.chat, self.ctx.channel_name_to_id, self.state
                )
                if discovered_spaces:
                    log_with_context(
                        logging.INFO,
                        f"[UPDATE MODE] Discovered {len(discovered_spaces)} existing spaces via API",
                    )
                    self.state.spaces.created_spaces = discovered_spaces
                else:
                    log_with_context(
                        logging.WARNING,
                        "[UPDATE MODE] No existing spaces found via API. Will create new spaces.",
                    )

            # Get all channel directories
            all_channel_dirs = [d for d in self.export_root.iterdir() if d.is_dir()]
            log_with_context(
                logging.INFO,
                f"Found {len(all_channel_dirs)} channel directories in export",
            )

            # Process each channel
            self._emit_phase("Migrating channels")

            self.channel_processor = ChannelProcessor(
                ctx=self.ctx,
                state=self.state,
                chat=self.chat,
                user_resolver=self.user_resolver,
                file_handler=getattr(self, "file_handler", None),
                attachment_processor=self.attachment_processor,
                progress_tracker=self._progress_tracker,
            )
            for ch in all_channel_dirs:
                channel_name = ch.name
                if channel_name in checkpoint.completed_channels:
                    log_with_context(
                        logging.INFO,
                        f"Skipping channel {channel_name} (already completed in previous run)",
                    )
                    continue

                result = self.channel_processor.process_channel(ch)
                if result.should_abort:
                    break

                # Only checkpoint channels that completed without errors
                if not result.had_errors:
                    checkpoint.completed_channels[channel_name] = now_iso()
                    save_checkpoint(checkpoint_path, checkpoint)

            self._emit_phase("Finalizing")

            # Log any space mapping conflicts that should be added to config
            log_space_mapping_conflicts(self.state, self.ctx.dry_run)

            # Generate final unmapped user report
            if self.unmapped_user_tracker.has_unmapped_users():
                unmapped_users = self.unmapped_user_tracker.get_unmapped_users_list()
                log_with_context(
                    logging.ERROR,
                    f"MIGRATION COMPLETED WITH {len(unmapped_users)} UNMAPPED USERS:",
                )
                log_with_context(
                    logging.ERROR, f"  Users found: {', '.join(unmapped_users)}"
                )
                log_with_context(
                    logging.ERROR,
                    "  These users likely represent deleted Slack users or bots without email mappings.",
                )
                log_with_context(
                    logging.ERROR,
                    "  Add them to user_mapping_overrides in your config.yaml to resolve.",
                )

            # If this was a dry run, provide specific unmapped user guidance
            if self.dry_run:
                log_unmapped_user_summary_for_dry_run(
                    self.unmapped_user_tracker, self.export_root
                )

            # Calculate migration duration
            migration_duration = time.time() - migration_start_time

            # Log final success status
            log_migration_success(
                self.state,
                self.dry_run,
                migration_duration,
                getattr(self, "unmapped_user_tracker", None),
            )

            # Migration succeeded — remove checkpoint so the next run starts fresh
            clear_checkpoint(checkpoint_path)

            # Clean up channel handlers in success case (finally block will also run)
            cleanup_channel_handlers(self.state)

            return True

        except BaseException as e:
            # Calculate migration duration
            migration_duration = time.time() - migration_start_time

            # Log final failure status
            log_migration_failure(self.state, self.dry_run, e, migration_duration)

            # Re-raise the exception to maintain existing error handling behavior
            raise
        finally:
            # Restore the original signal handler
            signal.signal(signal.SIGINT, old_signal_handler)
            # Always ensure proper cleanup of channel log handlers
            cleanup_channel_handlers(self.state)
