import dataclasses
import threading
import time
from dataclasses import dataclass
from typing import Callable, Dict, List, Optional

from abstra_internals.controllers.codebase_events import CodebaseEventController
from abstra_internals.logger import AbstraLogger

DEFAULT_HEARTBEAT_TTL = 90.0
DEFAULT_SWEEP_INTERVAL = 5.0


@dataclass(frozen=True)
class PresenceState:
    session_id: str
    email: str
    name: str
    current_file_path: Optional[str]
    last_heartbeat_at: float

    def to_dict(self) -> dict:
        return {
            "sessionId": self.session_id,
            "email": self.email,
            "name": self.name,
            "currentFilePath": self.current_file_path,
            "lastHeartbeatAt": self.last_heartbeat_at,
        }


class PresenceController:
    """Who is currently editing the project, for the editor's presence avatars.

    In-memory, heartbeat-kept, TTL-swept — an awareness signal only. There is no
    file locking: concurrent edits are allowed and reconciled by the write
    plane's mtime CAS, not prevented here.
    """

    _presence: Dict[str, PresenceState] = {}
    _lock = threading.Lock()
    _sweeper_started: bool = False

    @classmethod
    def reset_state(cls) -> None:
        with cls._lock:
            cls._presence.clear()

    @classmethod
    def update_presence(
        cls,
        session_id: str,
        email: str,
        name: str,
        current_file_path: Optional[str],
    ) -> PresenceState:
        now = time.time()
        is_new = False

        with cls._lock:
            existing = cls._presence.get(session_id)
            if existing is None:
                is_new = True
                state = PresenceState(
                    session_id=session_id,
                    email=email,
                    name=name,
                    current_file_path=current_file_path,
                    last_heartbeat_at=now,
                )
            else:
                state = dataclasses.replace(
                    existing,
                    email=email,
                    name=name,
                    current_file_path=current_file_path,
                    last_heartbeat_at=now,
                )
            cls._presence[session_id] = state

        cls._broadcast_presence_event(
            "presence_joined" if is_new else "presence_update", state
        )
        return state

    @classmethod
    def remove_presence(cls, session_id: str) -> bool:
        removed_state: Optional[PresenceState] = None

        with cls._lock:
            existing = cls._presence.get(session_id)
            if existing is None:
                return False
            del cls._presence[session_id]
            removed_state = existing

        cls._broadcast_presence_event("presence_left", removed_state)
        return True

    @classmethod
    def get_all_presence(cls) -> List[PresenceState]:
        with cls._lock:
            return list(cls._presence.values())

    @classmethod
    def sweep_once(cls, ttl: float = DEFAULT_HEARTBEAT_TTL) -> List[PresenceState]:
        now = time.time()
        expired_presence: List[PresenceState] = []

        with cls._lock:
            for session_id, presence in list(cls._presence.items()):
                if now - presence.last_heartbeat_at > ttl:
                    expired_presence.append(presence)
                    del cls._presence[session_id]

        for presence in expired_presence:
            cls._broadcast_presence_event("presence_left", presence)

        return expired_presence

    @classmethod
    def _broadcast_presence_event(
        cls, event_type: str, presence: PresenceState
    ) -> None:
        try:
            CodebaseEventController.broadcast_raw(
                {
                    "filepath": presence.current_file_path or "",
                    "event": event_type,
                    "presence": presence.to_dict(),
                }
            )
        except Exception as e:
            AbstraLogger.capture_exception(e)


def start_sweeper(
    interval: float = DEFAULT_SWEEP_INTERVAL,
    ttl: float = DEFAULT_HEARTBEAT_TTL,
    log_exception: Optional[Callable[[Exception], None]] = None,
) -> Optional[threading.Thread]:
    if PresenceController._sweeper_started:
        return None

    def _loop() -> None:
        while True:
            try:
                time.sleep(interval)
                PresenceController.sweep_once(ttl=ttl)
            except Exception as exc:
                if log_exception is not None:
                    log_exception(exc)
                else:
                    AbstraLogger.capture_exception(exc)

    thread = threading.Thread(target=_loop, daemon=True, name="PresenceSweeper")
    thread.start()
    PresenceController._sweeper_started = True
    return thread
