"""PeerDrop Daemon — Lifecycle management using trio.

Handles daemon startup, shutdown, PID file, and health checks.
"""

from __future__ import annotations

import logging
import os
import signal
from pathlib import Path
from typing import Any

import trio

from peerdrop.core.engine import EngineConfig, PeerEngine
from peerdrop.core.events import (
    EventBus,
    MessageReceived,
    TransferCompleted,
    TransferFailed,
    TransferProgress,
    TransferStarted,
)
from peerdrop.core.service import ServiceInterface
from peerdrop.daemon.server import DaemonServer
from peerdrop.interfaces.rest.app import RestServer

logger = logging.getLogger(__name__)

DEFAULT_PID_PATH = Path("~/.peerdrop/peerdrop.pid").expanduser()
DEFAULT_SOCK_PATH = Path("~/.peerdrop/peerdrop.sock").expanduser()

# Map event types to IPC event names
_EVENT_TYPE_MAP: dict[type, str] = {
    TransferStarted: "transfer_started",
    TransferProgress: "transfer_progress",
    TransferCompleted: "transfer_completed",
    TransferFailed: "transfer_failed",
    MessageReceived: "message_received",
}


class Daemon:
    """Manages the daemon lifecycle using trio."""

    def __init__(
        self,
        config: EngineConfig | None = None,
        sock_path: Path | str | None = None,
        rest_port: int | None = None,
    ) -> None:
        self._config = config or EngineConfig()
        self._engine = PeerEngine(self._config)
        self._service = ServiceInterface(self._engine)
        self._server: DaemonServer | None = None
        self._rest_server: RestServer | None = None
        self._rest_port = rest_port
        self._sock_path = Path(sock_path) if sock_path else DEFAULT_SOCK_PATH
        self._pid_path = self._sock_path.with_suffix(".pid")
        self._cancel_scope: trio.CancelScope | None = None

    @property
    def service(self) -> ServiceInterface:
        """Access the service interface."""
        return self._service

    async def start(self) -> None:
        """Start the daemon."""
        logger.info("Starting PeerDrop daemon...")

        # Write PID file
        self._pid_path.parent.mkdir(parents=True, exist_ok=True)
        self._pid_path.write_text(str(os.getpid()))
        logger.info(f"PID: {os.getpid()}")

        # Start engine
        logger.info("Initializing libp2p...")
        await self._engine.start()
        logger.info(f"Engine started, peer ID: {self._engine.get_peer_id()}")

        # Create server with service interface
        self._server = DaemonServer(self._service, sock_path=self._sock_path)

        # Start REST server if port specified
        if self._rest_port:
            self._rest_server = RestServer(sock_path=str(self._sock_path), port=self._rest_port)
            self._rest_server.start()
            logger.info(f"REST API: {self._rest_server.url}")

        logger.info("Daemon ready")

    async def run(self) -> None:
        """Run the daemon until interrupted. Blocks forever."""
        await self.start()

        async with trio.open_nursery() as nursery:
            self._cancel_scope = nursery.cancel_scope

            # Give the server's handler access to the nursery for background tasks
            if self._server:
                self._server.set_nursery(nursery)

            # Start messaging manager with the nursery
            if self._engine._messaging:
                await self._engine._messaging.start(nursery)

            # Set up signal handlers for graceful shutdown
            signal_received = trio.Event()

            async def watch_signals():
                with trio.open_signal_receiver(signal.SIGINT, signal.SIGTERM) as signals:
                    async for _sig in signals:
                        logger.info("Signal received, shutting down...")
                        signal_received.set()
                        nursery.cancel_scope.cancel()
                        return

            async def bridge_events():
                """Subscribe to EventBus and forward events to IPC clients."""
                bus = self._engine.event_bus
                # Subscribe to all transfer and messaging event types
                channels = {}
                for event_type, event_name in _EVENT_TYPE_MAP.items():
                    channels[event_name] = bus.subscribe(event_type)

                try:
                    async with trio.open_nursery() as sub_nursery:
                        for event_name, channel in channels.items():
                            async def _forward(ch: trio.MemoryReceiveChannel, name: str) -> None:
                                async with ch:
                                    async for event in ch:
                                        data = {}
                                        # Transfer events
                                        if hasattr(event, "transfer_id"):
                                            data["transfer_id"] = event.transfer_id
                                        if hasattr(event, "progress"):
                                            data["progress"] = event.progress
                                            data["bytes_sent"] = event.bytes_sent
                                        if hasattr(event, "file_name"):
                                            data["file_name"] = event.file_name
                                        if hasattr(event, "error"):
                                            data["error"] = event.error
                                        if hasattr(event, "sender_peer_id"):
                                            data["sender_peer_id"] = event.sender_peer_id
                                            data["receiver_peer_id"] = event.receiver_peer_id
                                        # Message events
                                        if hasattr(event, "topic"):
                                            data["topic"] = event.topic
                                        if hasattr(event, "sender"):
                                            data["sender"] = event.sender
                                        if hasattr(event, "data"):
                                            data["data"] = event.data

                                        if self._server:
                                            await self._server.broadcast_event(name, data)

                            sub_nursery.start_soon(_forward, channel, event_name)
                except trio.Cancelled:
                    pass

            nursery.start_soon(watch_signals)
            nursery.start_soon(self._server.serve)
            nursery.start_soon(bridge_events)

            # Wait for signal
            await signal_received.wait()

        await self.stop()

    async def stop(self) -> None:
        """Stop the daemon gracefully."""
        logger.info("Stopping daemon...")

        if self._rest_server:
            self._rest_server.stop()

        await self._engine.stop()

        # Remove PID file
        if self._pid_path.exists():
            self._pid_path.unlink()

        logger.info("Daemon stopped")

    def is_running(self) -> bool:
        """Check if another daemon is already running."""
        if not self._pid_path.exists():
            return False

        try:
            pid = int(self._pid_path.read_text().strip())
            os.kill(pid, 0)  # Check if process exists
            return True
        except (ValueError, ProcessLookupError, PermissionError):
            # Stale PID file
            self._pid_path.unlink(missing_ok=True)
            return False

    def get_status(self) -> dict[str, Any]:
        """Get daemon status."""
        running = self.is_running()
        return {
            "running": running,
            "pid": os.getpid() if running else None,
            "peer_id": self._engine.get_peer_id() if self._engine._started else None,
            "sock_path": str(self._sock_path),
        }


def write_pid_file(path: Path = DEFAULT_PID_PATH) -> None:
    """Write current process PID to file."""
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(str(os.getpid()))


def read_pid_file(path: Path = DEFAULT_PID_PATH) -> int | None:
    """Read PID from file, returns None if not found or invalid."""
    try:
        return int(path.read_text().strip())
    except (FileNotFoundError, ValueError):
        return None


def remove_pid_file(path: Path = DEFAULT_PID_PATH) -> None:
    """Remove PID file."""
    path.unlink(missing_ok=True)


def kill_daemon(path: Path = DEFAULT_PID_PATH, sock_path: Path | str | None = None) -> bool:
    """Send SIGTERM to daemon process. Returns True if signal was sent."""
    if sock_path:
        path = Path(sock_path).with_suffix(".pid")
    pid = read_pid_file(path)
    if pid is None:
        return False

    try:
        os.kill(pid, signal.SIGTERM)
        return True
    except (ProcessLookupError, PermissionError):
        remove_pid_file(path)
        return False
