"""Daemon process lifecycle — double-fork daemonization, PID file, signals.

Unix daemonization pattern:
  1. First fork — parent exits (detach from controlling terminal)
  2. setsid() — become session leader
  3. Second fork — can no longer acquire a controlling terminal
  4. Redirect stdio to log file, write PID file
  5. Wire SIGTERM → graceful shutdown, SIGHUP → log reload notice
"""

from __future__ import annotations

import asyncio
import logging
import os
import signal
import sys
import time
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import Any

log = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# PID file helpers
# ---------------------------------------------------------------------------


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


def read_pid(path: Path) -> int | None:
    """Read PID from file, returning None if missing or invalid."""
    try:
        return int(path.read_text().strip())
    except (FileNotFoundError, ValueError):
        return None


def remove_pid(path: Path) -> None:
    """Remove PID file if it exists."""
    try:
        path.unlink()
    except FileNotFoundError:
        pass


def is_daemon_running(path: Path) -> int | None:
    """Check if daemon is running. Returns PID if alive, None otherwise.

    Cleans up stale PID file if the process is gone.
    """
    pid = read_pid(path)
    if pid is None:
        return None
    try:
        os.kill(pid, 0)  # signal 0 = existence check
        return pid
    except ProcessLookupError:
        remove_pid(path)
        return None
    except PermissionError:
        # Process exists but we can't signal it (different user)
        return pid


# ---------------------------------------------------------------------------
# Logging setup
# ---------------------------------------------------------------------------


def setup_daemon_logging(log_file: Path, level: str = "INFO") -> None:
    """Configure file-based logging with rotation for the daemon process."""
    log_file.parent.mkdir(parents=True, exist_ok=True)

    handler = RotatingFileHandler(
        log_file,
        maxBytes=10 * 1024 * 1024,  # 10 MB
        backupCount=5,
    )
    handler.setFormatter(logging.Formatter(
        "%(asctime)s %(levelname)-8s %(name)s: %(message)s",
        datefmt="%Y-%m-%dT%H:%M:%S",
    ))

    root = logging.getLogger()
    root.setLevel(getattr(logging, level.upper(), logging.INFO))
    # Clear any existing handlers (inherited from pre-fork)
    root.handlers.clear()
    root.addHandler(handler)


# ---------------------------------------------------------------------------
# Double-fork daemonization
# ---------------------------------------------------------------------------


def _daemonize(log_file: Path) -> None:
    """Double-fork to fully detach from terminal.

    After this call, the process is a proper Unix daemon:
    - No controlling terminal
    - Session leader's child (can't reacquire terminal)
    - stdin closed, stdout/stderr → log file
    """
    # First fork
    pid = os.fork()
    if pid > 0:
        os._exit(0)  # parent exits

    os.setsid()  # new session

    # Second fork
    pid = os.fork()
    if pid > 0:
        os._exit(0)  # first child exits

    # Redirect file descriptors
    sys.stdout.flush()
    sys.stderr.flush()

    devnull = os.open(os.devnull, os.O_RDWR)
    os.dup2(devnull, sys.stdin.fileno())
    os.close(devnull)

    # stdout/stderr go to log file (for any non-logging output)
    log_file.parent.mkdir(parents=True, exist_ok=True)
    log_fd = os.open(str(log_file), os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
    os.dup2(log_fd, sys.stdout.fileno())
    os.dup2(log_fd, sys.stderr.fileno())
    os.close(log_fd)


# ---------------------------------------------------------------------------
# Signal handling
# ---------------------------------------------------------------------------


def _wire_signals(shutdown: asyncio.Event, loop: asyncio.AbstractEventLoop) -> None:
    """Wire SIGTERM and SIGHUP to the shutdown event."""
    def _handle_term(sig: int, frame: Any) -> None:
        log.info("Received SIGTERM — initiating graceful shutdown")
        loop.call_soon_threadsafe(shutdown.set)

    def _handle_hup(sig: int, frame: Any) -> None:
        log.info("Received SIGHUP — config reload not yet implemented")

    signal.signal(signal.SIGTERM, _handle_term)
    signal.signal(signal.SIGHUP, _handle_hup)


# ---------------------------------------------------------------------------
# Daemon entry point
# ---------------------------------------------------------------------------


async def _run_daemon(config: Any, project_dir: Path) -> None:
    """Async entry point: create pools, wire signals, run loop."""
    import asyncpg
    import redis.asyncio as aioredis

    from loom.daemon.loop import daemon_loop

    pid_path = project_dir / config.daemon.pid_file_path
    write_pid(pid_path)
    log.info("Daemon started (PID %d)", os.getpid())

    shutdown = asyncio.Event()
    loop = asyncio.get_running_loop()
    _wire_signals(shutdown, loop)

    pool = await asyncpg.create_pool(
        config.database.url,
        min_size=config.database.pool_min_size,
        max_size=config.database.pool_max_size,
    )
    redis_conn = aioredis.from_url(config.redis.url, decode_responses=True)

    # Optional Weft integration
    weft_pool = None
    weft_embedder = None
    if config.integrations.weft.enabled:
        try:
            from loom.integrations.weft import create_weft_pool, create_weft_embedder
            weft_pool = await create_weft_pool(config)
            weft_embedder = await create_weft_embedder(config)
            log.info("Weft integration enabled")
        except Exception:
            log.warning("Weft integration configured but failed to init", exc_info=True)

    try:
        await daemon_loop(
            pool, redis_conn, config, shutdown,
            weft_pool=weft_pool, weft_embedder=weft_embedder,
        )
    finally:
        log.info("Daemon shutting down — closing connections")
        await redis_conn.aclose()
        await pool.close()
        if weft_pool:
            await weft_pool.close()
        remove_pid(pid_path)
        log.info("Daemon stopped.")


def start_daemon(config: Any, project_dir: Path, *, foreground: bool = False) -> None:
    """Start the daemon process.

    Args:
        config: LoomConfig instance
        project_dir: Project root directory (PID/log paths are relative to this)
        foreground: If True, run in foreground (no fork). Useful for debugging.
    """
    pid_path = project_dir / config.daemon.pid_file_path
    log_path = project_dir / config.daemon.log_file_path

    # Check not already running
    existing_pid = is_daemon_running(pid_path)
    if existing_pid:
        print(f"Daemon already running (PID {existing_pid})")
        sys.exit(1)

    if not foreground:
        _daemonize(log_path)

    # Set up logging after fork (so handlers point to correct FDs)
    setup_daemon_logging(log_path, config.log_level)

    # Change working directory to project root
    os.chdir(project_dir)

    asyncio.run(_run_daemon(config, project_dir))


def stop_daemon(config: Any, project_dir: Path, *, timeout: int = 10) -> bool:
    """Stop the daemon by sending SIGTERM and waiting for exit.

    Returns True if daemon was stopped, False if it wasn't running.
    """
    pid_path = project_dir / config.daemon.pid_file_path
    pid = is_daemon_running(pid_path)

    if pid is None:
        return False

    os.kill(pid, signal.SIGTERM)

    # Wait for process to exit
    for _ in range(timeout * 10):  # check every 100ms
        try:
            os.kill(pid, 0)
            time.sleep(0.1)
        except ProcessLookupError:
            remove_pid(pid_path)
            return True

    # Still running after timeout — leave it, don't force kill
    return False
