"""Presence system - heartbeat files and agent discovery."""

import json
import logging
import os
import time
from pathlib import Path
from typing import Dict, List, Optional

from kollabor_agent.runtime import AgentRuntime

logger = logging.getLogger(__name__)


def get_hub_dir() -> Path:
    """Get the hub directory, creating if needed."""
    hub_dir = Path.home() / ".kollabor-cli" / "hub"
    hub_dir.mkdir(parents=True, exist_ok=True)
    return hub_dir


def get_presence_dir() -> Path:
    """Get the presence directory."""
    d = get_hub_dir() / "presence"
    d.mkdir(parents=True, exist_ok=True)
    return d


def get_messages_dir() -> Path:
    """Get the messages directory."""
    d = get_hub_dir() / "messages"
    d.mkdir(parents=True, exist_ok=True)
    return d


def get_socket_dir() -> Path:
    """Get the socket directory in /tmp (short paths for unix sockets)."""
    d = Path("/tmp/kollabor-hub")
    d.mkdir(parents=True, exist_ok=True)
    return d


def _atomic_write(path: Path, data: dict) -> None:
    """Write JSON atomically via temp file + rename."""
    tmp = path.with_suffix(".tmp")
    try:
        with open(tmp, "w") as f:
            json.dump(data, f, indent=2)
            f.flush()
            os.fsync(f.fileno())
        tmp.rename(path)
    except Exception as e:
        logger.error(f"Atomic write failed for {path}: {e}")
        if tmp.exists():
            tmp.unlink(missing_ok=True)


class PresenceManager:
    """Manages this agent's presence file and discovers other agents."""

    def __init__(self, identity: "AgentRuntime"):
        self.identity = identity
        self._presence_dir = get_presence_dir()
        self._presence_file = self._presence_dir / f"{identity.agent_id}.json"

    def publish(self) -> None:
        """Write/update this agent's presence file."""
        self.identity.last_heartbeat = time.time()
        _atomic_write(self._presence_file, self.identity.to_presence_dict())

    def heartbeat(self) -> None:
        """Update heartbeat timestamp."""
        self.identity.last_heartbeat = time.time()
        _atomic_write(self._presence_file, self.identity.to_presence_dict())

    def remove(self) -> None:
        """Remove this agent's presence file (graceful shutdown)."""
        try:
            self._presence_file.unlink(missing_ok=True)
            logger.info(f"Removed presence file for {self.identity.designation}")
        except Exception as e:
            logger.error(f"Failed to remove presence: {e}")

    def discover_agents(self, include_self: bool = False) -> List[AgentRuntime]:
        """Scan presence directory for live agents.

        Validates PID liveness AND socket connectivity.
        Both must pass - a recycled PID with a dead socket is stale.
        """
        agents = []
        for f in self._presence_dir.glob("*.json"):
            if not include_self and f.stem == self.identity.agent_id:
                continue
            try:
                with open(f) as fh:
                    data = json.load(fh)
                agent = AgentRuntime.from_presence_dict(data)

                if not agent.is_alive():
                    logger.info(f"Cleaning dead agent: {f.stem} (pid {agent.pid})")
                    f.unlink(missing_ok=True)
                    self._cleanup_agent_socket(agent)
                    continue

                # Always verify socket - catches recycled PIDs
                if agent.socket_path and not self._socket_responds(agent.socket_path):
                    logger.info(
                        f"Cleaning stale agent: {f.stem} (pid alive but socket dead)"
                    )
                    f.unlink(missing_ok=True)
                    self._cleanup_agent_socket(agent)
                    continue

                agents.append(agent)
            except (json.JSONDecodeError, Exception) as e:
                logger.debug(f"Bad presence file {f}: {e}")
        return agents

    @staticmethod
    def _socket_responds(socket_path: str) -> bool:
        """Quick synchronous check if a socket file exists and is connectable."""
        import socket

        if not Path(socket_path).exists():
            return False
        try:
            s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
            s.settimeout(1.0)
            s.connect(socket_path)
            s.close()
            return True
        except (ConnectionRefusedError, OSError, socket.timeout):
            return False

    @staticmethod
    def _cleanup_agent_socket(agent: "AgentRuntime") -> None:
        """Remove a dead agent's socket file."""
        if agent.socket_path:
            try:
                Path(agent.socket_path).unlink(missing_ok=True)
            except Exception:
                pass

    def get_agent_by_designation(self, designation: str) -> Optional[AgentRuntime]:
        """Find an agent by its designation."""
        for agent in self.discover_agents():
            if agent.designation == designation:
                return agent
        return None

    def get_project_agents(self, project: Optional[str] = None) -> List[AgentRuntime]:
        """Get all agents working in the same project directory."""
        project = project or self.identity.project
        return [a for a in self.discover_agents() if a.project == project]

    def get_roster_summary(self) -> List[Dict]:
        """Get a summary of all agents for system prompt injection."""
        agents = self.discover_agents()
        return [
            {
                "designation": a.designation,
                "state": a.state,
                "project": a.project,
                "current_task": a.current_task,
                "is_coordinator": a.is_coordinator,
            }
            for a in agents
        ]
