"""
Cleanup utilities for Claude Code SDK clients.

This module provides cleanup functionality for SDK clients and their associated
processes. The cleanup strategy avoids calling client.disconnect() due to
internal cancel scope issues in the SDK.
"""

import psutil
import structlog
from typing import Any

logger = structlog.get_logger(__name__)


def terminate_client_processes(client: Any, execution_id: str) -> int:
    """
    Terminate MCP server subprocesses associated with a Claude SDK client.

    This function CAREFULLY terminates only the child processes that belong
    to this specific SDK client. It avoids terminating shared MCP servers
    or processes from other executions by checking process ownership.

    It tries graceful termination first (SIGTERM), then forceful kill (SIGKILL)
    if the process doesn't terminate within 2 seconds (increased from 1).

    Args:
        client: Claude SDK client (may have references to subprocesses)
        execution_id: Execution ID for logging

    Returns:
        Number of processes successfully terminated

    Note:
        This is the primary cleanup method. We avoid calling client.disconnect()
        because it has internal cancel scope issues that cause RuntimeError even
        when called from the correct event loop.
    """
    terminated_count = 0
    termination_timeout = 2.0  # Increased from 1 second for more graceful shutdown

    try:
        current_process = psutil.Process()

        # Get direct children only (not recursive) to avoid terminating unrelated processes
        children = current_process.children(recursive=False)

        # Build list of candidate processes BEFORE terminating any
        # This prevents killing processes while iterating
        processes_to_terminate = []

        for child in children:
            try:
                cmdline = ' '.join(child.cmdline())

                # Be MORE SPECIFIC about which processes to terminate
                # Look for execution-specific MCP processes
                # Only terminate if the process looks like it belongs to this client
                mcp_patterns = ['mcp', 'stdio-client']

                # SAFETY: Don't terminate Python interpreters or other broad processes
                # Only terminate specific MCP-related subprocesses
                unsafe_patterns = ['python', 'python3', '/usr/bin', '/usr/local/bin', 'worker', 'temporal']
                is_unsafe = any(pattern in cmdline.lower() for pattern in unsafe_patterns)
                is_mcp_related = any(pattern in cmdline.lower() for pattern in mcp_patterns)

                if is_mcp_related and not is_unsafe:
                    # Additional safety check: ensure this is a child process spawned recently
                    # Older processes are likely shared or from other executions
                    try:
                        process_age = child.create_time()
                        import time
                        age_seconds = time.time() - process_age

                        # Only terminate processes younger than 1 hour
                        # Older processes are likely shared MCP servers or from long-running executions
                        if age_seconds < 3600:  # 1 hour (matches activity timeout)
                            processes_to_terminate.append((child, cmdline))
                        else:
                            logger.debug(
                                "skipping_old_process",
                                execution_id=execution_id,
                                pid=child.pid,
                                age_seconds=int(age_seconds),
                                note="Process too old, likely shared or from another execution"
                            )
                    except (psutil.NoSuchProcess, psutil.AccessDenied):
                        # Can't check age, skip for safety
                        logger.debug(
                            "skipping_process_cant_check_age",
                            execution_id=execution_id,
                            pid=child.pid
                        )

            except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
                # Process already gone or we don't have permission
                logger.debug(
                    "could_not_inspect_process",
                    execution_id=execution_id,
                    error=str(e),
                    error_type=type(e).__name__
                )

        # Now terminate the identified processes
        for child, cmdline in processes_to_terminate:
            try:
                logger.info(
                    "terminating_mcp_process",
                    execution_id=execution_id,
                    pid=child.pid,
                    cmdline=cmdline[:100]  # Truncate to first 100 chars
                )

                # Try graceful termination first (SIGTERM)
                child.terminate()
                try:
                    # Wait longer (2 seconds) for process to terminate gracefully
                    child.wait(timeout=termination_timeout)
                    terminated_count += 1
                    logger.debug(
                        "mcp_process_terminated_gracefully",
                        execution_id=execution_id,
                        pid=child.pid
                    )
                except psutil.TimeoutExpired:
                    # Force kill if it doesn't terminate (SIGKILL)
                    # Only do this for processes that don't respond to SIGTERM
                    try:
                        child.kill()
                        child.wait(timeout=1.0)  # Wait for kill to complete
                        terminated_count += 1
                        logger.warning(
                            "force_killed_mcp_process",
                            execution_id=execution_id,
                            pid=child.pid,
                            note=f"Process did not terminate gracefully within {termination_timeout}s"
                        )
                    except psutil.NoSuchProcess:
                        # Process already gone, that's fine
                        terminated_count += 1
                        logger.debug(
                            "process_already_gone",
                            execution_id=execution_id,
                            pid=child.pid
                        )

            except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
                # Process already gone or we don't have permission
                # This is not an error - log at debug level
                logger.debug(
                    "could_not_terminate_process",
                    execution_id=execution_id,
                    error=str(e),
                    error_type=type(e).__name__
                )

    except Exception as e:
        # Unexpected error during process termination
        # Log but don't propagate - cleanup should never break execution
        logger.warning(
            "process_termination_failed",
            execution_id=execution_id,
            error=str(e),
            error_type=type(e).__name__
        )

    if terminated_count > 0:
        logger.info(
            "terminated_client_processes",
            execution_id=execution_id,
            process_count=terminated_count
        )
    else:
        logger.debug(
            "no_processes_terminated",
            execution_id=execution_id,
            note="No MCP processes found to terminate (may have already exited)"
        )

    return terminated_count


def cleanup_sdk_client(client: Any, execution_id: str, logger_instance: Any) -> dict:
    """
    Cleanup SDK client and associated resources.

    This is a wrapper that provides structured cleanup with comprehensive
    error handling. It skips client.disconnect() due to cancel scope issues
    and goes directly to process termination.

    Args:
        client: Claude SDK client to clean up
        execution_id: Execution ID for logging
        logger_instance: Logger instance to use for logging

    Returns:
        Dictionary with cleanup results:
        {
            'success': bool,
            'processes_terminated': int,
            'method': str,
            'error': str | None
        }

    Note:
        This function will NEVER raise exceptions. All errors are caught,
        logged, and returned in the result dictionary.
    """
    try:
        # Skip problematic disconnect() - go straight to process termination
        logger_instance.info(
            "skipping_sdk_disconnect_cleaning_processes",
            execution_id=execution_id,
            note="Skipping SDK disconnect due to cancel scope issues - terminating processes directly"
        )

        # Terminate MCP child processes
        terminated = terminate_client_processes(client, execution_id)

        if terminated > 0:
            logger_instance.info(
                "sdk_client_cleanup_completed",
                execution_id=execution_id,
                processes_terminated=terminated,
                method="process_termination"
            )
            return {
                'success': True,
                'processes_terminated': terminated,
                'method': 'process_termination',
                'error': None
            }
        else:
            logger_instance.debug(
                "no_processes_to_terminate",
                execution_id=execution_id
            )
            return {
                'success': True,
                'processes_terminated': 0,
                'method': 'no_action_needed',
                'error': None
            }

    except Exception as cleanup_error:
        # LAST RESORT: Log but NEVER propagate
        logger_instance.error(
            "cleanup_catastrophic_failure",
            execution_id=execution_id,
            error=str(cleanup_error),
            error_type=type(cleanup_error).__name__,
            exc_info=True,
            note="Cleanup completely failed - returning error but not propagating exception"
        )
        return {
            'success': False,
            'processes_terminated': 0,
            'method': 'failed',
            'error': str(cleanup_error)
        }
