"""
Stop Actions Handler - Raw Kafka Consumer for Actions Removal

Watches the instance-specific 'stop_actions_{instance_id}' Kafka topic.
When a message arrives it stops every listed action (killing its subprocess)
and force-removes its Docker container so the machine is left in a clean state.

Event Structure:
{
    "action_ids": ["action_id_1", "action_id_2"]
}
"""

import json
import logging
import os
import subprocess
import threading
import traceback
from typing import Any, Dict, List, Optional

from kafka import KafkaConsumer

logger = logging.getLogger(__name__)


class StopActionsHandler:
    """
    Listens on the 'stop_actions_{instance_id}' Kafka topic using a raw
    KafkaConsumer running in a daemon thread. For each message it stops the
    specified actions and removes their Docker containers.
    """

    def __init__(self, actions_manager, scaling, kafka_bootstrap: str, instance_id: str):
        """
        Initialize the Stop Actions Handler.

        Args:
            actions_manager: Reference to the ActionsManager instance.
            scaling: Scaling service instance used to update action status via API.
            kafka_bootstrap: Kafka bootstrap server string (e.g. "host:port").
            instance_id: This compute instance's ID – used to build the topic name.
        """
        self.actions_manager = actions_manager
        self.scaling = scaling
        self.kafka_bootstrap = kafka_bootstrap
        self.instance_id = instance_id
        self.kafka_topic = f"stop_actions_{instance_id}"
        self._consumer: Optional[KafkaConsumer] = None
        self._thread: Optional[threading.Thread] = None
        self._running = False

        logger.info(
            "Initializing StopActionsHandler for instance ID: %s (topic: %s)",
            instance_id,
            self.kafka_topic,
        )

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------

    def start(self) -> bool:
        """
        Start the consumer thread.

        Returns:
            bool: True if started successfully, False otherwise.
        """
        if self._running:
            logger.warning("StopActionsHandler is already running")
            return False

        self._running = True
        self._thread = threading.Thread(
            target=self._consume_loop,
            name="StopActionsHandler",
            daemon=True,
        )
        self._thread.start()
        logger.info("StopActionsHandler started on topic: %s", self.kafka_topic)
        return True

    def stop(self):
        """Stop the consumer thread gracefully."""
        logger.info("Stopping StopActionsHandler...")
        self._running = False

        if self._consumer:
            try:
                self._consumer.close()
            except Exception as e:
                logger.warning("Error closing Kafka consumer: %s", e)

        if self._thread and self._thread.is_alive():
            self._thread.join(timeout=10)

        logger.info("StopActionsHandler stopped")

    # ------------------------------------------------------------------
    # Consumer loop
    # ------------------------------------------------------------------

    def _consume_loop(self):
        """Background thread: create consumer and poll for messages."""
        try:
            self._consumer = KafkaConsumer(
                self.kafka_topic,
                bootstrap_servers=self.kafka_bootstrap,
                group_id=f"stop_actions_{self.instance_id}",
                value_deserializer=lambda m: json.loads(m.decode("utf-8")),
                auto_offset_reset="latest",
                enable_auto_commit=True,
                consumer_timeout_ms=1000,  # unblocks the loop so _running is checked
            )
            logger.info(
                "StopActionsHandler consumer connected to %s", self.kafka_bootstrap
            )
        except Exception as e:
            logger.error("StopActionsHandler failed to create KafkaConsumer: %s", e)
            logger.error(traceback.format_exc())
            self._running = False
            return

        while self._running:
            try:
                for message in self._consumer:
                    if not self._running:
                        break
                    try:
                        self._handle_stop_event(message.value)
                    except Exception as e:
                        logger.error("Error handling stop_actions message: %s", e)
                        logger.error(traceback.format_exc())
            except Exception as e:
                if self._running:
                    logger.error("StopActionsHandler consumer error: %s", e)
                    logger.error(traceback.format_exc())

        logger.info("StopActionsHandler consumer loop exited")

    # ------------------------------------------------------------------
    # Event handling
    # ------------------------------------------------------------------

    def _handle_stop_event(self, event: Dict[str, Any]):
        """
        Process a single message from the topic.

        Args:
            event: Decoded JSON message dictionary from Kafka.
        """
        logger.info("Received stop_actions event: %s", event)

        action_ids: List[str] = event.get("action_ids", [])

        if not action_ids:
            logger.warning("stop_actions event has no action_ids")
            return

        logger.info("Processing removal of %d action(s)", len(action_ids))

        for action_id in action_ids:
            try:
                self._stop_and_remove_action(action_id)
            except Exception as e:
                logger.error("Error processing action %s: %s", action_id, e)
                logger.error(traceback.format_exc())

    # ------------------------------------------------------------------
    # Core removal logic
    # ------------------------------------------------------------------

    def _stop_and_remove_action(self, action_id: str):
        """
        Stop a running action and remove its Docker container.

        Args:
            action_id: The action record ID to stop and remove.
        """
        logger.info("Stopping and removing action: %s", action_id)

        # Capture the container name *before* stop_action removes the instance
        # from current_actions (the instance still holds the info at this point).
        container_name = self._get_container_name(action_id)

        # Stop the action process via ActionsManager
        result = self.actions_manager.stop_action(action_id)
        logger.info("stop_action result for %s: %s", action_id, result)

        # Remove the associated Docker container
        if container_name:
            self._remove_docker_container(container_name)
        else:
            # Fall back to a name-prefix search in case the instance was not
            # tracked (e.g. the container was started outside py_compute).
            self._try_remove_containers_by_action_id(action_id)

        # Notify the backend that the action is now stopped
        try:
            self.scaling.update_action_status(
                service_provider=os.environ.get("SERVICE_PROVIDER", ""),
                action_record_id=action_id,
                status="stopped",
                isRunning=False,
                action_duration=0,
            )
            logger.info("Updated action status to stopped for: %s", action_id)
        except Exception as e:
            logger.error("Failed to update action status for %s: %s", action_id, e)

    def _get_container_name(self, action_id: str) -> Optional[str]:
        """
        Derive the Docker container name for a given action ID.

        The container name follows the pattern ``{action_record_id}_{action_type}``
        used when containers are started in action_instance.py.

        Args:
            action_id: The action record ID.

        Returns:
            Container name string, or None if the action was not found.
        """
        action_instance = self.actions_manager.current_actions.get(
            action_id
        ) or self.actions_manager.stopped_actions.get(action_id)

        if action_instance is None:
            logger.warning(
                "Action %s not found in current or stopped actions; "
                "will attempt container lookup by action_id prefix",
                action_id,
            )
            return None

        record_id = getattr(action_instance, "action_record_id", action_id)
        action_type = getattr(action_instance, "action_type", None)
        if action_type:
            return f"{record_id}_{action_type}"

        logger.warning(
            "action_type missing for action %s; cannot build container name", action_id
        )
        return None

    # ------------------------------------------------------------------
    # Docker helpers
    # ------------------------------------------------------------------

    def _remove_docker_container(self, container_name: str):
        """
        Stop and force-remove a Docker container by name or ID.

        Args:
            container_name: Container name or short ID.
        """
        logger.info("Removing Docker container: %s", container_name)

        # 1. Graceful stop
        try:
            result = subprocess.run(
                ["docker", "stop", container_name],
                capture_output=True,
                text=True,
                timeout=30,
            )
            if result.returncode == 0:
                logger.info("Docker container stopped: %s", container_name)
            else:
                logger.warning(
                    "docker stop failed for %s: %s",
                    container_name,
                    result.stderr.strip(),
                )
        except subprocess.TimeoutExpired:
            logger.error("docker stop timed out for container: %s", container_name)
        except Exception as e:
            logger.error("Error stopping container %s: %s", container_name, e)

        # 2. Force-remove
        try:
            result = subprocess.run(
                ["docker", "rm", "-f", container_name],
                capture_output=True,
                text=True,
                timeout=30,
            )
            if result.returncode == 0:
                logger.info("Docker container removed: %s", container_name)
            else:
                logger.warning(
                    "docker rm failed for %s: %s",
                    container_name,
                    result.stderr.strip(),
                )
        except subprocess.TimeoutExpired:
            logger.error("docker rm timed out for container: %s", container_name)
        except Exception as e:
            logger.error("Error removing container %s: %s", container_name, e)

    def _try_remove_containers_by_action_id(self, action_id: str):
        """
        Find all containers whose name starts with *action_id* and remove them.

        Fallback path when the action instance is not tracked locally.

        Args:
            action_id: The action record ID used as a name-filter prefix.
        """
        logger.info("Searching for containers with name prefix: %s", action_id)
        try:
            result = subprocess.run(
                [
                    "docker", "ps", "-a",
                    "--filter", f"name={action_id}",
                    "--format", "{{.Names}}",
                ],
                capture_output=True,
                text=True,
                timeout=15,
            )
            if result.returncode == 0 and result.stdout.strip():
                for container_name in result.stdout.strip().split("\n"):
                    if container_name:
                        self._remove_docker_container(container_name)
            else:
                logger.info(
                    "No containers found matching action_id prefix: %s", action_id
                )
        except Exception as e:
            logger.error(
                "Error searching for containers for action %s: %s", action_id, e
            )
