from __future__ import annotations

import json
import threading
from datetime import datetime, date
from pathlib import Path
from typing import Callable, IO


class AuditLog:
    def __init__(
        self,
        base_dir: Path,
        clock: Callable[[], datetime] | None = None,
    ) -> None:
        self._base_dir = Path(base_dir)
        self._clock: Callable[[], datetime] = clock or datetime.now
        self._current_date: date | None = None
        self._fh: IO[str] | None = None
        self._lock = threading.Lock()

    def log(self, event: dict) -> None:
        now = self._clock()
        today = now.date()
        if "ts" not in event:
            event = {**event, "ts": now.isoformat()}
        with self._lock:
            if today != self._current_date:
                self._open(today)
            assert self._fh is not None
            self._fh.write(json.dumps(event, separators=(",", ":")) + "\n")

    def close(self) -> None:
        with self._lock:
            self._close_locked()

    def _close_locked(self) -> None:
        """Close file handle; must be called while holding self._lock."""
        if self._fh is not None:
            try:
                self._fh.close()
            except Exception:
                pass
            finally:
                self._fh = None

    @property
    def current_path(self) -> Path:
        if self._current_date is None:
            today = self._clock().date()
            return self._base_dir / f"{today}.jsonl"
        return self._base_dir / f"{self._current_date}.jsonl"

    def _open(self, today: date) -> None:
        self._close_locked()  # already holding self._lock
        self._base_dir.mkdir(parents=True, exist_ok=True)
        path = self._base_dir / f"{today}.jsonl"
        self._fh = open(path, "a", buffering=1, encoding="utf-8")
        self._current_date = today
