import logging
import os
import threading
import time
from abc import ABC, abstractmethod
from typing import Tuple

from p4p.client.thread import Context

from .base_pv_interface import BasePVInterface
from .daemon_status import TaskStatus
from .io import BridgeIO
from .utils import Timer

logger = logging.getLogger(__name__)


class BridgeDaemon(ABC):
    """
    Base class for an EPICS Bridge Daemon with High-Availability features.

    Architecture:
        1. **Main Loop (Control):** Runs on the main thread using the 'main_context'.
           It executes the synchronous control cycle:
           Trigger -> Read -> Logic -> Write -> Ack.
        2. **Heartbeat Loop (Monitor):** Runs on a separate daemon thread with a
           PRIVATE 'hb_context'. This ensures the heartbeat never stalls, even if
           the Main Loop is blocked by heavy network traffic.

    Safety Features:
        - **Zombie Protection:** The Heartbeat thread acts as a watchdog. It monitors
          the Main Loop's activity timestamp. If the Main Loop hangs (e.g., infinite
          loop or deadlocked IO), the Heartbeat STOPS pulsing to alert external
          supervisors.
        - **Suicide Pact:** If the Main Loop encounters persistent IO failures
          (defined by `max_stuck_cycles`), it voluntarily exits the process to
          allow a system-level restart (Docker/systemd).

    Attributes:
        cfg (BridgeConfig): Configuration object.
        io (BridgeIO): The synchronous IO handler for the Main Loop.
    """

    def __init__(self, interface: BasePVInterface):
        self.interface = interface

        # 1. Main IO (Control Logic)
        # We assume BridgeIO is the synchronous version (max_workers=1 or blocking)
        self.io = BridgeIO()

        # 2. State & Concurrency
        self._running = False
        self._last_main_loop_activity = 0.0  # Timestamp for Zombie Protection

        # 3. Safety Thresholds
        self.max_stall_limit = 10.0  # Time considered normal for the task
        self.max_stuck_cycles = 15  # Approx 15 * poll_interval seconds
        self._stuck_counter = 0

    # =========================================================================
    # Overridable Hooks
    # =========================================================================

    def get_poll_rate(self) -> float:
        """
        [Overridable] Controls the polling rate of the Main Loop.
        Defaults to 1 Hz
        Can be overridden in child classes
        """
        return 1.0

    def trigger(self) -> bool:
        """
        [Overridable] Checks if the control cycle should run.

        Default Implementation:
            Reads the trigger PV.
            Raises RuntimeError if the read operation fails (triggering stuck counter).

        Returns:
            bool: True to start `run_task()`, False to sleep.
        """

        return self.io.pvget(self.interface.trigger, timeout=1.0)

    @abstractmethod
    def run_task(self) -> TaskStatus:
        """
        [Required Override] Core business logic.

        Args:
            inputs: Dictionary of current values from Input PVs.

        Returns:
            Dict[str, Any]: Values to write to Output PVs.
            None: If no output is required for this cycle.
        """
        pass

    def reset_cycle(self):
        """
        [Overridable] Resets the handshake/trigger PVs at end of cycle.
        Retries 3 times. If all fail, raises RuntimeError.

        Returns:
            bool: True if the reset write was successful.

        Raises:
            RuntimeError: If the write fails after 3 attempts.
        """
        payload = {self.interface.busy: False, self.interface.trigger: False}

        for attempt in range(1, 4):
            if self.io.pvput(payload, timeout=2.0):
                return
            logger.warning("Reset cycle failed (Attempt %d/3). Retrying...", attempt)
            time.sleep(0.5)  # Brief pause to let network/IOC recover

        logger.error("Critical: Failed to reset cycle after 3 attempts.")
        raise RuntimeError("Failed to reset cycle PVs (Busy/Trigger) after 3 attempts.")

    # =========================================================================
    # Lifecycle Management
    # =========================================================================

    def start(self) -> None:
        """Starts the Heartbeat thread and enters the blocking Main Loop."""
        self._running = True
        self._last_main_loop_activity = time.time()  # Prime the watchdog

        # Start the Isolated Heartbeat Thread (Daemon)
        # Daemon=True ensures it dies instantly if the main process crashes.
        hb_thread = threading.Thread(
            target=self._heartbeat_worker, daemon=True, name="Bridge_Heartbeat_Isolated"
        )
        hb_thread.start()

        # Block main thread on the Control Loop
        self._main_loop()

    def stop(self) -> None:
        """Signals loops to stop."""
        logger.info("Stopping Daemon...")
        self._running = False
        exit(0)

    # =========================================================================
    # Main Control Loop (Thread 1)
    # =========================================================================

    def _main_loop(self) -> None:
        """
        The Master Control Loop.
        Orchestrates the cycle: Health -> Attempt -> Report -> Sleep.
        """
        logger.info("Main loop started.")

        try:
            while self._running:
                # 1. Health Check (Watchdogs & Suicide Pacts)
                self._last_main_loop_activity = time.time()

                # 2. Attempt Cycle (Trigger -> Execute -> Catch Errors)
                status, duration = self._attempt_cycle()

                # 3. Report Status (Best Effort)
                self._update_telemetry(status, duration)

                # 4. Pace the loop
                time.sleep(self.get_poll_rate())

        except Exception as e:
            # Fatal Error Boundary: Catches infrastructure crashes
            logger.critical(f"Fatal error in main loop: {e}", exc_info=True)
        finally:
            self.stop()

    def _attempt_cycle(self) -> Tuple[object, float]:
        # 1. Initialize duration safety
        duration = 0.0

        try:
            if not self.trigger():
                return None, None

            with Timer() as t:
                status = self._execute_cycle()

            # Happy path duration
            duration = t.duration
            return status, duration

        except Exception as e:
            logger.error("Cycle failed: %s", e, exc_info=True)
            # 2. Now safe to return 'duration' (either 0.0 or t.duration if t exists)
            if "t" in locals():
                duration = t.duration
            return TaskStatus.EXCEPTION, duration

    def _execute_cycle(self) -> TaskStatus:
        """
        The Action Sequence.
        Uses try...finally to GUARANTEE cleanup.
        """
        # 1. Set Busy Flag
        if not self.io.pvput({self.interface.busy: True}):
            raise IOError("Failed to set Busy flag at start of cycle.")

        try:
            # 2. Run User Logic
            return self.run_task()
        finally:
            # 3. Cleanup (Always runs, even on error)
            self.reset_cycle()

    def _update_telemetry(self, status: object, duration: float) -> None:
        """Writes status and duration to the I/O interface (Best Effort)."""

        if status is None or duration is None:
            return

        self.io.pvput(
            {self.interface.task_status: status, self.interface.task_duration: duration}
        )

    # =========================================================================
    # Isolated Heartbeat (Thread 2)
    # =========================================================================

    def _heartbeat_worker(self) -> None:
        """
        Executes the background Heartbeat and Watchdog logic.

        This method runs on a separate daemon thread (`Bridge_Heartbeat_Isolated`).
        It serves two critical roles:

        1. **Liveness Monitor (External):** Toggles a 'heartbeat' PV (0/1) to let
           external monitoring tools (like the IOC or Alarm Handler) know the
           bridge is connected and processing data.

        2. **Watchdog Terminator (Internal):** Monitors the `_last_main_loop_activity`
           timestamp.
           If the Main Thread gets stuck (deadlock, infinite loop, blocking IO),
           this thread detects the stall and forcibly terminates the process to allow
           supervisors (systemd/Docker) to restart it.
        """
        logger.info("Starting Isolated Heartbeat Worker...")

        # Toggle state for the heartbeat (flips between 0 and 1)
        pulse_val = 0

        with Context("pva") as ctx:
            while self._running:
                is_active = self._is_main_loop_active()

                if is_active:
                    pulse_val = 1 - pulse_val  # Toggle value

                    success = self._send_pulse(ctx, pulse_val)

                    if success:
                        self._stuck_counter = 0  # Reset fault counter on success
                    else:
                        # Main loop is fine, but Network/EPICS layer is failing
                        self._stuck_counter += 1
                        logger.warning(
                            "Heartbeat IO Error. Faults: %d/%d",
                            self._stuck_counter,
                            self.max_stuck_cycles,
                        )

                else:
                    # The Main Loop has not updated its timestamp within 'stall_limit'.
                    # We stop pulsing to alert external monitors.
                    self._stuck_counter += 1

                    logger.warning(
                        "Heartbeat Paused: Main Loop Stalled. Faults: %d/%d",
                        self._stuck_counter,
                        self.max_stuck_cycles,
                    )

                # If the Main Thread has been stuck for too long, we must take action.
                # Since the Main Thread is stuck, it cannot exit itself.
                if self._stuck_counter >= self.max_stuck_cycles:
                    self._kill_process()

                # Pace the heartbeat (1 Hz)
                time.sleep(1.0)

        logger.info("Heartbeat Worker stopped cleanly.")

    # =========================================================================
    # Helper Methods (Private)
    # =========================================================================

    def _is_main_loop_active(self) -> bool:
        """Calculates if the main loop has updated its timestamp recently."""
        # Dynamic tolerance based on current poll rate
        stall_limit = max(self.get_poll_rate() * 4, self.max_stall_limit)
        time_since_activity = time.time() - self._last_main_loop_activity

        return time_since_activity < stall_limit

    def _send_pulse(self, context: Context, value: int) -> bool:
        """Attempts to write the heartbeat PV. Returns True if successful."""
        try:
            context.put(self.interface.heartbeat, value, wait=True, timeout=2.0)
            return True
        except Exception as e:
            logger.warning("Heartbeat IO Error: %s", e)
            return False

    def _kill_process(self) -> None:
        """
        CRITICAL: Use os._exit(), not sys.exit()
        sys.exit() only raises an exception in THIS thread (the heartbeat thread).
        os._exit() tells the OS to immediately destroy the entire process.
        This bypasses 'finally' blocks and cleanup handlers, which is
        necessary because the Main Thread is likely deadlocked.
        """
        logger.critical(
            "Watchdog Triggered: Main Loop stuck for %d cycles. "
            "initiating HARD KILL via os._exit(1).",
            self._stuck_counter,
        )

        os._exit(1)
