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

from p4p.client.thread import Context

from .base_pv_interface import BasePVInterface
from .daemon_status import TaskStatus
from .io import BridgeIO
from .logging import LoggingManager
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:
        iface (BasePVInterface): PV interface (trigger, busy, heartbeat, task_status, etc.).
        io (BridgeIO): The synchronous IO handler for the Main Loop.
    """

    def __init__(
        self,
        iface: BasePVInterface,
        logging_manager: LoggingManager,
    ):
        self.iface = iface
        self.logging_manager = logging_manager
        self._start_logging_manager()

        # 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 (overridable via methods)
        self._stall_fault_count = 0
        self._io_fault_count = 0
        self._applied_runtime_log_level: str | int | None = None
        self._applied_silenced_loggers: tuple[str, ...] | None = None

        # 4. Initialize PVs to a clean state (best-effort)
        self._initialize_pv_state()

    def _start_logging_manager(self) -> None:
        """Configure logging manager from interface metadata when pending."""
        self.logging_manager.create_log_pv_publisher_handler(
            warning_pv=self.iface.warning_log,
            error_pv=self.iface.error_log,
        )
        self.logging_manager.start()

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

    def get_max_stall_limit(self) -> float:
        """[Overridable] Maximum allowed main-loop inactivity (seconds)."""
        return 10.0

    def get_max_stuck_cycles(self) -> int:
        """[Overridable] Fault cycles before watchdog kills process."""
        return 15

    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 (mutates .val/.raw in-place).
            Returns the trigger's value as bool; False if read failed.

        Returns:
            bool: True to start `run_task()`, False to sleep.
        """
        self.io.pvget(self.iface.trigger, timeout=5.0)
        logger.debug("Trigger retrieved: %s", self.iface.trigger)
        return bool(self.iface.trigger.val)

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

        Returns:
            TaskStatus: Result of the cycle (SUCCESS, LOGIC_FAILURE, IO_FAILURE,
            EXCEPTION, SKIPPED). When SKIPPED, task duration is not updated.
        """
        pass  # pragma: no cover - abstract; subclasses implement

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

        Raises:
            RuntimeError: If the write fails after 3 attempts.
        """
        self.iface.busy.val = False
        self.iface.trigger.val = False
        pvs = [self.iface.busy, self.iface.trigger]

        for attempt in range(1, 4):
            try:
                self.io.pvput(pvs, timeout=2.0)
                logger.debug("reset_cycle PV put succeeded (attempt %d).", attempt)
                return
            except Exception:
                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.")

    def _initialize_pv_state(self) -> None:
        """
        Initializes PVs to a clean state during daemon startup.

        This ensures the daemon starts fresh by resetting handshake/trigger PVs.
        Implemented as best-effort: logs warnings but doesn't fail initialization
        if the IOC isn't ready yet.
        """
        try:
            self.reset_cycle()
            logger.info("Successfully reset cycle PVs during initialization.")
        except RuntimeError:
            logger.warning(
                "Could not reset cycle PVs during initialization. "
                "IOC may not be ready yet. Daemon will continue."
            )

    # =========================================================================
    # 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.monotonic()  # Prime the watchdog
        logger.debug(
            "Daemon start: poll_rate=%.2f Hz, max_stall_limit=%.1fs, max_stuck_cycles=%d",
            self.get_poll_rate(),
            self.get_max_stall_limit(),
            self.get_max_stuck_cycles(),
        )

        # 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. Does not exit the process (library-friendly)."""
        logger.info("Stopping Daemon...")
        logger.debug("_running=False; main loop and heartbeat will exit.")
        self._running = False

    def terminate(self) -> None:
        """Signals loops to stop and exits the process with code 0."""
        self.stop()
        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.monotonic()
                self._sync_runtime_logging_controls()

                # 2. Execute Cycle (Trigger -> Busy -> Logic -> Cleanup)
                status, duration = self._execute_cycle()

                # 3. Report Status (Best Effort)
                self._update_telemetry(status, duration)
                if status is not None and duration is not None:
                    logger.debug(
                        "Main loop iteration: status=%s, duration=%.3fs",
                        status.name,
                        duration,
                    )

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

        except Exception as e:
            # Fatal Error Boundary: Catches infrastructure crashes
            logger.critical("Fatal error in main loop: %s", e, exc_info=True)
        finally:
            logger.debug("Main loop exiting (exception or _running=False).")
            # Signal stop; if run as a standalone process, exiting `start()` will
            # typically end the program naturally.
            self.stop()

    def _sync_runtime_logging_controls(self) -> None:
        """Apply PV-driven logging controls (best-effort, non-fatal)."""
        self.io.pvget([self.iface.log_level, self.iface.silenced_loggers], timeout=2.0)

        requested_level = self.iface.log_level.val
        if (
            requested_level is not None
            and requested_level != self._applied_runtime_log_level
        ):
            try:
                self.logging_manager.set_level(requested_level)
                self._applied_runtime_log_level = requested_level
            except ValueError:
                logger.warning(
                    "Ignoring invalid LogLevel PV value: %r", requested_level
                )

        requested_silenced = self.logging_manager.normalize_noisy_third_party_loggers(
            self.iface.silenced_loggers.val
        )
        if requested_silenced != self._applied_silenced_loggers:
            self.logging_manager.set_noisy_third_party_loggers(requested_silenced)
            self._applied_silenced_loggers = requested_silenced

    def _execute_cycle(self) -> tuple[Optional[TaskStatus], Optional[float]]:
        """
        Single cycle: trigger check, set busy, run task, cleanup.
        Uses try...finally to guarantee cleanup. Exceptions propagate to _main_loop.
        """
        if not self.trigger():
            logger.debug("Cycle skipped: trigger=False.")
            return None, None

        logger.debug("Trigger asserted; setting busy=True.")
        self.iface.busy.val = True
        self.io.pvput(self.iface.busy)

        try:
            with Timer() as t:
                status = self.run_task()
            logger.debug("run_task() returned status=%s.", status.name)
            return status, t.duration
        except Exception:
            logger.debug("run_task() raised; publishing TaskStatus.EXCEPTION.")
            self._update_telemetry(TaskStatus.EXCEPTION, t.duration)
            raise
        finally:
            self.reset_cycle()
            logger.debug("reset_cycle() completed.")

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

        When status is SKIPPED, only task_status is written; task_duration is
        not updated so that the last real processing duration is preserved.
        """
        if status is None or duration is None:
            return

        self.iface.task_status.val = status
        if status == TaskStatus.SKIPPED:
            self.io.pvput(self.iface.task_status)
            logger.debug(
                "Telemetry written: task_status=%s (duration not updated).", status.name
            )
            return

        self.iface.task_duration.val = duration
        self.io.pvput([self.iface.task_status, self.iface.task_duration])
        logger.debug(
            "Telemetry written: task_status=%s, task_duration=%.3fs.",
            status.name,
            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...")
        logger.debug(
            "Heartbeat watchdog stall_limit=%.1fs (main loop inactive beyond this).",
            self._stall_limit(),
        )
        pulse_val = 0
        try:
            with Context("pva") as ctx:
                while self._running:
                    pulse_val = self._heartbeat_iteration(ctx, pulse_val)
                    if self._heartbeat_should_kill():
                        self._kill_process()
                    time.sleep(1.0)
            logger.info("Heartbeat Worker stopped cleanly.")
        except Exception as e:
            logger.critical("Fatal error in heartbeat worker: %s", e, exc_info=True)
            os._exit(1)

    def _heartbeat_iteration(self, ctx: Context, pulse_val: int) -> int:
        """Run one heartbeat cycle: pulse if main loop active, else count stall. Returns next pulse value."""
        if self._is_main_loop_active():
            pulse_val = 1 - pulse_val
            success = self._send_pulse(ctx, pulse_val)
            if success:
                self._stall_fault_count = 0
                self._io_fault_count = 0
            else:
                self._io_fault_count += 1
                logger.warning(
                    "Heartbeat IO Error. IO faults: %d",
                    self._io_fault_count,
                )
        else:
            self._stall_fault_count += 1
            logger.warning(
                "Heartbeat Paused: Main Loop Stalled. Stall faults: %d/%d",
                self._stall_fault_count,
                self.get_max_stuck_cycles(),
            )
        return pulse_val

    def _heartbeat_should_kill(self) -> bool:
        """True when main loop is stuck or heartbeat IO failures exceed limit (watchdog kill condition)."""
        return (
            self._stall_fault_count >= self.get_max_stuck_cycles()
            or self._io_fault_count >= self.get_max_stuck_cycles()
        )

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

    def _stall_limit(self) -> float:
        """Maximum seconds of main-loop inactivity before watchdog considers it stalled."""
        return max(self.get_poll_rate() * 4, self.get_max_stall_limit())

    def _is_main_loop_active(self) -> bool:
        """Calculates if the main loop has updated its timestamp recently."""
        time_since_activity = time.monotonic() - self._last_main_loop_activity
        return time_since_activity < self._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.iface.heartbeat.name, 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.
        """
        if self._stall_fault_count >= self.get_max_stuck_cycles():
            reason = f"Main loop stalled ({self._stall_fault_count} cycles)"
        else:
            reason = f"Heartbeat IO failures ({self._io_fault_count} cycles)"
        logger.critical(
            "Watchdog Triggered: %s. Initiating HARD KILL via os._exit(1).", reason
        )

        os._exit(1)
