"""
Phase 84: Distributed Locks — Distributed file locks across nodes.

Provides acquire/release/renew semantics with TTL-based expiry and admin
force-release. Locks are stored as individual JSON files keyed by resource hash.

Storage:
  state/broker/dist_locks/
    {resource_hash}.json   — lock state per resource
"""

import hashlib
import json
import logging
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from brynq.server import BROKER_DIR, _write_json_sync, _read_jsonl_sync, _append_jsonl_sync

logger = logging.getLogger(__name__)

# ── Paths ──────────────────────────────────────────────────────────────────

LOCKS_DIR = BROKER_DIR / "dist_locks"
LOCKS_DIR.mkdir(parents=True, exist_ok=True)


def reset() -> None:
    """Reset. Used by tests."""
    pass


def _now() -> str:
    return datetime.now(timezone.utc).isoformat()


def _resource_hash(resource: str) -> str:
    return hashlib.sha256(resource.encode()).hexdigest()[:16]


def _lock_file(resource: str) -> Path:
    return LOCKS_DIR / f"{_resource_hash(resource)}.json"


def _load_lock(resource: str) -> Optional[dict]:
    f = _lock_file(resource)
    if not f.exists():
        return None
    try:
        return json.loads(f.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return None


def _save_lock(resource: str, lock: dict) -> None:
    _write_json_sync(_lock_file(resource), lock)


def _delete_lock(resource: str) -> None:
    f = _lock_file(resource)
    if f.exists():
        f.unlink()


def _is_expired(lock: dict) -> bool:
    expires = lock.get("expires_at", 0)
    return time.time() > expires


# ── Public API ─────────────────────────────────────────────────────────────

def acquire_lock(
    resource: str,
    owner_id: str,
    ttl_seconds: int = 300,
) -> Tuple[bool, dict]:
    """
    Acquire a distributed lock.
    Returns (acquired, lock_info).
    If already locked by same owner, succeeds (re-entrant).
    """
    existing = _load_lock(resource)

    if existing and not _is_expired(existing):
        if existing.get("owner_id") == owner_id:
            # Re-entrant: extend the lock
            existing["expires_at"] = time.time() + ttl_seconds
            existing["renewed_at"] = _now()
            _save_lock(resource, existing)
            return (True, existing)
        return (False, existing)

    # Expired or no lock — grant it
    lock = {
        "resource": resource,
        "owner_id": owner_id,
        "acquired_at": _now(),
        "expires_at": time.time() + ttl_seconds,
        "ttl_seconds": ttl_seconds,
        "renewed_at": None,
        "force_released": False,
    }
    _save_lock(resource, lock)
    logger.info("Lock acquired: %s by %s (ttl=%ds)", resource, owner_id, ttl_seconds)
    return (True, lock)


def release_lock(resource: str, owner_id: str) -> bool:
    """Release a lock. Only the owner can release it."""
    lock = _load_lock(resource)
    if not lock:
        return False
    if lock.get("owner_id") != owner_id:
        return False
    _delete_lock(resource)
    logger.info("Lock released: %s by %s", resource, owner_id)
    return True


def check_lock(resource: str) -> Optional[dict]:
    """Check who holds the lock and when it expires."""
    lock = _load_lock(resource)
    if not lock:
        return None
    if _is_expired(lock):
        _delete_lock(resource)
        return None
    remaining = lock["expires_at"] - time.time()
    return {
        "resource": resource,
        "owner_id": lock["owner_id"],
        "acquired_at": lock.get("acquired_at"),
        "expires_at": lock["expires_at"],
        "remaining_seconds": round(remaining, 1),
        "expired": False,
    }


def force_release(resource: str, reason: str = "") -> bool:
    """Admin override to release any lock."""
    lock = _load_lock(resource)
    if not lock:
        return False
    logger.warning("Force release: %s (owner=%s, reason=%s)", resource, lock.get("owner_id"), reason)
    _delete_lock(resource)
    return True


def list_locks(owner_id: Optional[str] = None) -> List[dict]:
    """List all active (non-expired) locks."""
    locks = []
    if not LOCKS_DIR.exists():
        return locks
    for f in sorted(LOCKS_DIR.glob("*.json")):
        try:
            lock = json.loads(f.read_text("utf-8"))
            if _is_expired(lock):
                f.unlink()
                continue
            if owner_id and lock.get("owner_id") != owner_id:
                continue
            locks.append(lock)
        except (json.JSONDecodeError, OSError):
            continue
    return locks


def renew_lock(resource: str, owner_id: str, extend_seconds: int = 300) -> Tuple[bool, Optional[dict]]:
    """Extend TTL of an existing lock. Only the owner can renew."""
    lock = _load_lock(resource)
    if not lock:
        return (False, None)
    if lock.get("owner_id") != owner_id:
        return (False, None)
    if _is_expired(lock):
        _delete_lock(resource)
        return (False, None)

    lock["expires_at"] = time.time() + extend_seconds
    lock["ttl_seconds"] = extend_seconds
    lock["renewed_at"] = _now()
    _save_lock(resource, lock)
    return (True, lock)


def cleanup_expired() -> int:
    """Remove expired locks. Returns count of cleaned locks."""
    cleaned = 0
    if not LOCKS_DIR.exists():
        return cleaned
    for f in list(LOCKS_DIR.glob("*.json")):
        try:
            lock = json.loads(f.read_text("utf-8"))
            if _is_expired(lock):
                f.unlink()
                cleaned += 1
        except (json.JSONDecodeError, OSError):
            continue
    return cleaned
