"""
Garantiza que solo una instancia de un script corra a la vez.

Implementación basada en fcntl.flock (lock advisory del kernel Linux):
  - Si el proceso que tiene el lock muere por cualquier motivo (crash,
    excepción no capturada, kill -9, reboot), el kernel libera el lock
    automáticamente al cerrar el file descriptor. NO quedan locks
    huérfanos que bloqueen ejecuciones futuras.
  - Si una segunda instancia intenta arrancar mientras la primera está
    viva, sale inmediatamente sin tocar nada.

Uso típico:

    from helpers.singleton import single_instance

    if __name__ == "__main__":
        with single_instance("procesar_output"):
            main()

Para inspeccionar a mano qué scripts están corriendo:

    ls -la .locks/
    cat  .locks/procesar_output.lock     # muestra PID + hora de inicio

Para ver el proceso vivo:

    ps -p $(awk -F= '{print $2}' .locks/procesar_output.lock | awk '{print $1}')
"""

import fcntl
import os
import sys
import time
from contextlib import contextmanager
from datetime import datetime
from pathlib import Path

# Carpeta donde se guardan los locks. Se usa una ruta del repo (no /tmp)
# para que sobreviva limpiezas de /tmp y sea fácil de inspeccionar.
LOCK_DIR = Path(__file__).resolve().parent.parent / ".locks"


def _ensure_lock_dir() -> None:
    LOCK_DIR.mkdir(parents=True, exist_ok=True)


def _read_lock_info(lock_path: Path) -> str:
    try:
        return lock_path.read_text(encoding="utf-8").strip()
    except Exception:
        return "(no se pudo leer)"


@contextmanager
def single_instance(name: str, exit_if_running: bool = True):
    """
    Adquiere un lock exclusivo identificado por `name`.

    Si ya hay otra instancia corriendo:
      - exit_if_running=True (default): imprime mensaje y hace sys.exit(0)
      - exit_if_running=False: lanza RuntimeError
    """
    _ensure_lock_dir()
    lock_path = LOCK_DIR / f"{name}.lock"

    # Abrimos en modo a+ para no truncar contenido si otro proceso ya lo
    # tiene tomado. Si conseguimos el lock, después truncamos nosotros.
    fp = open(lock_path, "a+")
    try:
        fcntl.flock(fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
    except BlockingIOError:
        info = _read_lock_info(lock_path)
        msg = (
            f"[{name}] Ya hay una instancia corriendo. "
            f"Lock: {lock_path} | Info: {info}. Saliendo."
        )
        fp.close()
        if exit_if_running:
            print(msg, file=sys.stderr)
            sys.exit(0)
        raise RuntimeError(msg)

    # Tenemos el lock. Escribimos PID + timestamp + script para diagnóstico.
    fp.seek(0)
    fp.truncate()
    fp.write(
        f"pid={os.getpid()} "
        f"started={datetime.now().isoformat(timespec='seconds')} "
        f"script={Path(sys.argv[0]).name}\n"
    )
    fp.flush()

    started = time.time()
    try:
        yield
    finally:
        elapsed = time.time() - started
        try:
            fcntl.flock(fp.fileno(), fcntl.LOCK_UN)
        finally:
            fp.close()
            try:
                lock_path.unlink()
            except FileNotFoundError:
                pass
        print(f"[{name}] lock liberado ({elapsed:.1f}s)")