import json
import logging
import threading
import uuid
import asyncio
from datetime import datetime, timezone
from pathlib import Path
from typing import Callable, Dict, Any, List, Optional

_log = logging.getLogger("pulse.event_bus")

from pulse.core.paths import get_state_dir

EVENTS_DIR = get_state_dir() / "events"
MAX_FILE_SIZE = 10 * 1024 * 1024  # 10MB

# Singleton registry
_subscribers: Dict[str, List[Callable]] = {}
_lock = threading.RLock()

def _get_channel_file(channel: str) -> Path:
    EVENTS_DIR.mkdir(parents=True, exist_ok=True)
    return EVENTS_DIR / f"{channel}.jsonl"

def _rotate_if_needed(file_path: Path):
    if file_path.exists() and file_path.stat().st_size >= MAX_FILE_SIZE:
        timestamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
        rotated_path = file_path.with_name(f"{file_path.stem}_{timestamp}{file_path.suffix}")
        try:
            file_path.rename(rotated_path)
        except OSError:
            pass  # Fallback if another thread just rotated it

def publish(channel: str, source: str, payload: Dict[str, Any], correlation_id: Optional[str] = None) -> str:
    """Publish an event to a channel and persist it to rotating JSONL."""
    if not correlation_id:
        correlation_id = str(uuid.uuid4())
        
    event = {
        "type": channel,
        "source": source,
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "payload": payload,
        "correlation_id": correlation_id
    }
    
    # Persist to disk atomically with rotation
    with _lock:
        file_path = _get_channel_file(channel)
        _rotate_if_needed(file_path)
        with open(file_path, "a", encoding="utf-8") as f:
            f.write(json.dumps(event) + "\n")
            
        subs = _subscribers.get(channel, [])[:]
        
    # Dispatch to subscribers
    for callback in subs:
        if asyncio.iscoroutinefunction(callback):
            try:
                loop = asyncio.get_running_loop()
                loop.create_task(callback(event))
            except RuntimeError:
                # No running event loop in this thread, run in a background thread
                threading.Thread(target=lambda c=callback, e=event: asyncio.run(c(e)), daemon=True).start()
        else:
            try:
                callback(event)
            except Exception as e:
                # Catching exceptions to prevent one subscriber from breaking the bus
                _log.warning("Error in subscriber for %s: %s", channel, e)
                
    return correlation_id

def subscribe(channel: str, callback: Callable) -> None:
    """Register a callback for a specific channel."""
    with _lock:
        if channel not in _subscribers:
            _subscribers[channel] = []
        if callback not in _subscribers[channel]:
            _subscribers[channel].append(callback)

def unsubscribe(channel: str, callback: Callable) -> None:
    """Remove a callback from a channel."""
    with _lock:
        if channel in _subscribers and callback in _subscribers[channel]:
            _subscribers[channel].remove(callback)

def get_recent_events(channel: str, limit: int = 100) -> List[Dict[str, Any]]:
    """Helper to read recent events for a channel."""
    file_path = _get_channel_file(channel)
    if not file_path.exists():
        return []
    
    events = []
    with _lock:
        with open(file_path, "r", encoding="utf-8") as f:
            lines = f.readlines()
            for line in reversed(lines[-limit:]):
                try:
                    events.append(json.loads(line.strip()))
                except json.JSONDecodeError:
                    continue
    return events
