"""task-2962: .done.notified O_EXCL 원자적 선점 공용 모듈.

성공 알림을 보내는 4개 소비자(done-watcher.sh, activity-watcher.py,
done-watcher.py, notify-completion.py)가 동일한 `{task_id}.done.notified`
마커를 놓고 경쟁하던 문제(체크 → 전송 → 마커 순서로는 TOCTOU 경쟁창이 생김)를
해소하기 위한 공용 원자적 선점 유틸리티.

사용 패턴 (모든 소비자 공통):
    if claim_notification(marker_path):
        ok = send_telegram(...)
        if ok:
            finalize_notification(marker_path, payload={...})
        else:
            release_notification(marker_path)
    else:
        # 이미 다른 프로세스가 선점(또는 이미 전송 완료) → 스킵
        pass
"""

from __future__ import annotations

import json
import os
from pathlib import Path


def claim_notification(marker_path: str | os.PathLike) -> bool:
    """O_EXCL 로 마커를 원자적 선점한다.

    부모 디렉토리는 존재하지 않으면 자동 생성한다.

    Returns:
        선점에 성공하면 True. 마커가 이미 존재하면(다른 프로세스가 선점했거나
        이미 알림 완료됨) False. FileExistsError 이외의 OSError 가 발생하면
        전송을 금지하는 쪽(fail-closed)으로 False 를 반환한다.
    """
    path = Path(marker_path)
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        fd = os.open(str(path), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
        os.close(fd)
        return True
    except FileExistsError:
        return False
    except OSError:
        # fail-closed: 알 수 없는 이유로 선점 여부를 확신할 수 없으면
        # 중복 전송을 막기 위해 전송하지 않는 쪽으로 판정한다.
        return False


def release_notification(marker_path: str | os.PathLike) -> None:
    """전송 실패 시 선점을 롤백한다(마커 삭제).

    마커가 없으면 조용히 무시한다. 삭제 중 예외는 전파하지 않는다.
    """
    path = Path(marker_path)
    try:
        path.unlink()
    except FileNotFoundError:
        pass
    except OSError:
        pass


def finalize_notification(marker_path: str | os.PathLike, payload: dict | None = None) -> None:
    """전송 성공을 확정한다. payload 를 JSON 으로 마커에 기록한다.

    marker_path 는 claim_notification 으로 이미 생성돼 있는 것을 전제로 하지만,
    없더라도(레이스 등으로 유실된 경우) 새로 생성한다. 기록 실패는 예외를
    전파하지 않는다(알림 전송 자체는 이미 성공했으므로).
    """
    path = Path(marker_path)
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        data = payload if payload is not None else {}
        path.write_text(json.dumps(data, ensure_ascii=False, default=str), encoding="utf-8")
    except OSError:
        pass
