"""
ESPal Daemon.

Main daemon process that coordinates:
- Ring buffer for data storage
- IPC server for client communication
- Serial port management (to be added)
- Build management (to be added)
"""

import os
import sys
import signal
import threading
import time
import logging
import argparse
from typing import Optional, Callable
from pathlib import Path

from .buffer import RingBuffer
from .ipc import IPCServer, ClientSession
from .protocol import (
    StatusCommand, ShutdownCommand, BuildCommand, UploadCommand,
    StatusMessage, ErrorMessage, ShutdownNotice,
    BuildOutputMessage, BuildCompleteMessage
)
from .config import (
    DaemonConfig, load_config, get_socket_path, get_pid_path, parse_vidpid
)
from .serial_manager import SerialManager, PortState
from .build_manager import DaemonBuildManager, BuildState, BuildResult


logger = logging.getLogger(__name__)


class ESPalDaemon:
    """
    Main daemon class for ESPal.

    Coordinates all daemon components and manages lifecycle.
    """

    def __init__(self, config: DaemonConfig):
        """
        Initialise the daemon.

        Args:
            config: Daemon configuration
        """
        self._config = config
        self._start_time = time.time()
        self._last_client_time = time.time()

        # Determine socket/PID paths
        self._socket_path = get_socket_path(config.vid, config.pid, config.device)
        self._pid_path = get_pid_path(config.vid, config.pid, config.device)

        # Core components
        self._buffer = RingBuffer(max_size_bytes=config.buffer_size_bytes)
        self._ipc_server: Optional[IPCServer] = None

        # State
        self._running = False
        self._shutdown_event = threading.Event()
        self._idle_check_thread: Optional[threading.Thread] = None

        # Serial manager
        self._serial_manager = SerialManager(
            buffer=self._buffer,
            baudrate=config.baudrate,
            vid=config.vid,
            pid=config.pid,
            device=config.device,
            on_state_change=self._on_serial_state_change
        )

        # Build manager
        self._build_manager = DaemonBuildManager(
            buffer=self._buffer,
            serial_manager=self._serial_manager,
            project_dir=config.project_dir,
            on_output=self._on_build_output,
            on_complete=self._on_build_complete
        )

        # Track build requester for notifications
        self._build_requester: Optional[ClientSession] = None

    @property
    def socket_path(self) -> str:
        """Path to the Unix socket."""
        return self._socket_path

    @property
    def pid_path(self) -> str:
        """Path to the PID file."""
        return self._pid_path

    @property
    def uptime(self) -> float:
        """Daemon uptime in seconds."""
        return time.time() - self._start_time

    @property
    def idle_time(self) -> float:
        """Time since last client activity in seconds."""
        return time.time() - self._last_client_time

    @property
    def time_until_idle_shutdown(self) -> float:
        """Seconds until idle shutdown (0 if already exceeded)."""
        remaining = self._config.idle_timeout_seconds - self.idle_time
        return max(0, remaining)

    @property
    def is_running(self) -> bool:
        """Whether the daemon is running."""
        return self._running

    @property
    def client_count(self) -> int:
        """Number of connected clients."""
        if self._ipc_server:
            return self._ipc_server.client_count
        return 0

    @property
    def serial_state(self) -> PortState:
        """Current serial port state."""
        return self._serial_manager.state

    @property
    def serial_device(self) -> Optional[str]:
        """Current serial device path."""
        return self._serial_manager.device_path

    def _on_serial_state_change(self, state: PortState, device: Optional[str]) -> None:
        """Called when serial port state changes."""
        logger.info(f"Serial state: {state.value}, device: {device}")
        # Update tracked device in config
        if device:
            self._config.device = device

    def _write_pid_file(self) -> bool:
        """Write PID file."""
        try:
            with open(self._pid_path, "w") as f:
                f.write(str(os.getpid()))
            return True
        except Exception as e:
            logger.error(f"Failed to write PID file: {e}")
            return False

    def _remove_pid_file(self) -> None:
        """Remove PID file."""
        try:
            if os.path.exists(self._pid_path):
                os.unlink(self._pid_path)
        except Exception as e:
            logger.warning(f"Failed to remove PID file: {e}")

    def _check_existing_daemon(self) -> Optional[int]:
        """
        Check if another daemon is already running.

        Returns:
            PID of existing daemon, or None
        """
        if not os.path.exists(self._pid_path):
            return None

        try:
            with open(self._pid_path, "r") as f:
                pid = int(f.read().strip())

            # Check if process is running
            os.kill(pid, 0)
            return pid

        except (ValueError, ProcessLookupError, PermissionError):
            # PID file is stale
            self._remove_pid_file()
            return None

    def _on_client_connect(self, session: ClientSession) -> None:
        """Called when a client connects."""
        self._last_client_time = time.time()
        logger.info(f"Client {session.client_id} connected, {self.client_count} total")

    def _on_client_disconnect(self, session: ClientSession) -> None:
        """Called when a client disconnects."""
        self._last_client_time = time.time()
        logger.info(f"Client {session.client_id} disconnected, {self.client_count} total")

    def _handle_status(self, session: ClientSession, cmd: StatusCommand) -> StatusMessage:
        """Handle status command."""
        return StatusMessage(
            port=self.serial_device,
            port_state=self.serial_state.value,
            clients=self.client_count,
            buffer_bytes=self._buffer.size,
            buffer_entries=self._buffer.entry_count,
            uptime=self.uptime,
            idle_timeout=self.time_until_idle_shutdown,
            vid_pid=self._config.vid_pid
        )

    def _handle_shutdown(self, session: ClientSession, cmd: ShutdownCommand) -> None:
        """Handle shutdown command."""
        logger.info(f"Shutdown requested by client {session.client_id}")
        self.stop(reason="client_request")
        return None

    def _handle_build(self, session: ClientSession, cmd: BuildCommand) -> Optional[ErrorMessage]:
        """Handle build command."""
        if self._build_manager.is_busy:
            return ErrorMessage(
                code="BUILD_IN_PROGRESS",
                message="A build is already in progress"
            )

        self._build_requester = session
        self._build_manager.build(
            args=cmd.args,
            environment=cmd.environment,
            async_=True
        )

        logger.info(f"Build started by client {session.client_id}")
        return None  # No immediate response, completion sent via callback

    def _handle_upload(self, session: ClientSession, cmd: UploadCommand) -> Optional[ErrorMessage]:
        """Handle upload command."""
        if self._build_manager.is_busy:
            return ErrorMessage(
                code="BUILD_IN_PROGRESS",
                message="A build is already in progress"
            )

        self._build_requester = session
        self._build_manager.upload(
            args=cmd.args,
            environment=cmd.environment,
            async_=True
        )

        logger.info(f"Upload started by client {session.client_id}")
        return None

    def _on_build_output(self, stream: str, line: str) -> None:
        """Called when build produces output."""
        # Send to requester if still connected
        if self._build_requester and self._build_requester.running:
            self._build_requester.send(BuildOutputMessage(stream=stream, line=line))

    def _on_build_complete(self, result: BuildResult) -> None:
        """Called when build completes."""
        logger.info(f"Build complete: {result.operation} exit_code={result.exit_code}")

        # Send to requester if still connected
        if self._build_requester and self._build_requester.running:
            self._build_requester.send(BuildCompleteMessage(
                exit_code=result.exit_code,
                success=result.success
            ))

        self._build_requester = None

    def _idle_check_loop(self) -> None:
        """Check for idle timeout and shut down if exceeded."""
        while not self._shutdown_event.wait(timeout=60):
            if self._config.idle_timeout_seconds <= 0:
                continue

            if self.client_count == 0 and self.idle_time >= self._config.idle_timeout_seconds:
                logger.info(f"Idle timeout reached ({self._config.idle_timeout_seconds}s), shutting down")
                self.stop(reason="idle_timeout")
                break

    def _setup_signal_handlers(self) -> None:
        """Set up signal handlers for graceful shutdown."""
        def handle_signal(signum, frame):
            sig_name = signal.Signals(signum).name
            logger.info(f"Received {sig_name}, shutting down")
            self.stop(reason=f"signal_{sig_name}")

        signal.signal(signal.SIGTERM, handle_signal)
        signal.signal(signal.SIGINT, handle_signal)

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

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

        # Check for existing daemon
        existing_pid = self._check_existing_daemon()
        if existing_pid:
            logger.error(f"Another daemon is already running (PID {existing_pid})")
            return False

        # Write PID file
        if not self._write_pid_file():
            return False

        # Create IPC server
        self._ipc_server = IPCServer(
            self._socket_path,
            self._buffer,
            on_client_connect=self._on_client_connect,
            on_client_disconnect=self._on_client_disconnect
        )

        # Register command handlers
        self._ipc_server.register_handler(StatusCommand, self._handle_status)
        self._ipc_server.register_handler(ShutdownCommand, self._handle_shutdown)
        self._ipc_server.register_handler(BuildCommand, self._handle_build)
        self._ipc_server.register_handler(UploadCommand, self._handle_upload)

        # Start IPC server
        self._ipc_server.start()

        # Start serial manager
        self._serial_manager.start()

        # Set up signal handlers
        self._setup_signal_handlers()

        # Start idle check thread
        self._shutdown_event.clear()
        self._idle_check_thread = threading.Thread(target=self._idle_check_loop, daemon=True)
        self._idle_check_thread.start()

        self._running = True
        self._start_time = time.time()
        self._last_client_time = time.time()

        logger.info(f"Daemon started (socket: {self._socket_path})")
        return True

    def stop(self, reason: str = "unknown") -> None:
        """
        Stop the daemon.

        Args:
            reason: Reason for shutdown (for logging/notification)
        """
        if not self._running:
            return

        self._running = False
        self._shutdown_event.set()

        # Notify clients
        if self._ipc_server:
            self._ipc_server.broadcast(ShutdownNotice(reason=reason), subscribed_only=False)

        # Stop serial manager
        self._serial_manager.stop()

        # Stop IPC server
        if self._ipc_server:
            self._ipc_server.stop()
            self._ipc_server = None

        # Wait for idle check thread
        if self._idle_check_thread and self._idle_check_thread.is_alive():
            self._idle_check_thread.join(timeout=2.0)

        # Remove PID file
        self._remove_pid_file()

        logger.info(f"Daemon stopped (reason: {reason})")

    def run(self) -> int:
        """
        Run the daemon until stopped.

        Returns:
            Exit code (0 for success)
        """
        if not self.start():
            return 1

        try:
            # Wait for shutdown
            while self._running:
                self._shutdown_event.wait(timeout=1.0)
        except KeyboardInterrupt:
            pass
        finally:
            self.stop(reason="exit")

        return 0

    def add_data(self, source: str, data: bytes) -> int:
        """
        Add data to the buffer.

        Args:
            source: Data source ("serial", "stdout", "stderr")
            data: Raw bytes

        Returns:
            Sequence number of added entry
        """
        return self._buffer.append(source, data)

    def add_marker(self, name: str) -> int:
        """
        Add a marker to the buffer.

        Args:
            name: Marker name

        Returns:
            Sequence number of marker
        """
        return self._buffer.mark(name)


def main() -> int:
    """Main entry point for esppal-daemon command."""
    parser = argparse.ArgumentParser(
        description="ESPal daemon for ESP32 serial port management"
    )

    parser.add_argument(
        "--vidpid",
        help="USB VID:PID for device detection (e.g., 10c4:ea60)"
    )
    parser.add_argument(
        "--vid",
        help="USB Vendor ID"
    )
    parser.add_argument(
        "--pid",
        help="USB Product ID"
    )
    parser.add_argument(
        "-d", "--device",
        help="Device path (e.g., /dev/ttyUSB0)"
    )
    parser.add_argument(
        "-b", "--baudrate",
        type=int,
        help="Serial baudrate"
    )
    parser.add_argument(
        "--buffer-size",
        type=int,
        default=50,
        help="Buffer size in MB (default: 50)"
    )
    parser.add_argument(
        "--idle-timeout",
        type=float,
        default=30,
        help="Idle timeout in minutes (default: 30, 0 to disable)"
    )
    parser.add_argument(
        "-v", "--verbose",
        action="store_true",
        help="Enable verbose logging"
    )
    parser.add_argument(
        "--log-file",
        help="Log to file instead of stderr"
    )

    args = parser.parse_args()

    # Set up logging
    log_level = logging.DEBUG if args.verbose else logging.INFO
    log_format = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"

    if args.log_file:
        logging.basicConfig(
            level=log_level,
            format=log_format,
            filename=args.log_file
        )
    else:
        logging.basicConfig(
            level=log_level,
            format=log_format
        )

    # Parse VID:PID
    vid = args.vid
    pid = args.pid

    if args.vidpid:
        try:
            vid, pid = parse_vidpid(args.vidpid)
        except ValueError as e:
            logger.error(str(e))
            return 1

    # Load configuration
    config = load_config(
        vid=vid,
        pid=pid,
        device=args.device,
        buffer_size_mb=args.buffer_size,
        idle_timeout_minutes=args.idle_timeout,
        baudrate=args.baudrate
    )

    # Create and run daemon
    daemon = ESPalDaemon(config)
    return daemon.run()


if __name__ == "__main__":
    sys.exit(main())
