"""
Structured audit log for administrative and security-critical actions.

Writes append-only JSON-lines to ~/.soosef/audit.jsonl.  Each line is a
self-contained JSON object so the file can be tailed, grepped, or ingested
by any log aggregator without a parser.

Entry schema
------------
{
    "timestamp": "2026-04-01T12:34:56.789012+00:00",  # ISO-8601 UTC
    "actor":     "alice",          # username or "cli" for CLI invocations
    "action":    "user.delete",    # dotted hierarchical action name
    "target":    "user:3",         # affected resource (id, fingerprint, path …)
    "outcome":   "success",        # "success" | "failure"
    "source":    "web",            # "web" | "cli"
    "detail":    "optional extra"  # omitted when None
}

Actions used by SooSeF
----------------------
    user.create          Admin created a new user account
    user.delete          Admin deleted a user account
    user.password_reset  Admin issued a temporary password for a user
    key.channel.generate New channel key generated
    key.identity.generate New Ed25519 identity generated
    killswitch.fire      Emergency purge executed

The log is intentionally destroyed by the killswitch (AUDIT_LOG is under
BASE_DIR).  That is correct for this threat model: if you're wiping the
device you want the log gone too.
"""

from __future__ import annotations

import json
import logging
import threading
from datetime import UTC, datetime
from pathlib import Path
from typing import Literal

import soosef.paths as paths

logger = logging.getLogger(__name__)

# Serialisation lock — multiple Gunicorn workers write to the same file;
# the lock only protects within a single process, but append writes to a
# local filesystem are atomic at the OS level for small payloads, so the
# worst case across workers is interleaved bytes within a single line,
# which is extremely unlikely given the small line sizes here.  A proper
# multi-process solution would use a logging socket handler; this is
# acceptable for the offline-first threat model.
_lock = threading.Lock()

Outcome = Literal["success", "failure"]
Source = Literal["web", "cli"]


def log_action(
    actor: str,
    action: str,
    target: str,
    outcome: Outcome,
    source: Source,
    detail: str | None = None,
) -> None:
    """
    Append one audit entry to ~/.soosef/audit.jsonl.

    This function never raises — a failed write is logged to stderr and
    silently swallowed so that audit log failures do not block user-facing
    operations.

    Args:
        actor:   Username performing the action, or ``"cli"`` for CLI calls.
        action:  Dotted action name, e.g. ``"user.delete"``.
        target:  Affected resource identifier, e.g. ``"user:3"`` or a key
                 fingerprint prefix.
        outcome: ``"success"`` or ``"failure"``.
        source:  ``"web"`` or ``"cli"``.
        detail:  Optional free-text annotation (avoid PII where possible).
    """
    entry: dict[str, str] = {
        "timestamp": datetime.now(tz=UTC).isoformat(),
        "actor": actor,
        "action": action,
        "target": target,
        "outcome": outcome,
        "source": source,
    }
    if detail is not None:
        entry["detail"] = detail

    line = json.dumps(entry, ensure_ascii=False) + "\n"

    try:
        log_path: Path = paths.AUDIT_LOG
        # Ensure parent directory exists (BASE_DIR should already exist after
        # ensure_dirs(), but be defensive in case audit is called early).
        log_path.parent.mkdir(parents=True, exist_ok=True)

        with _lock:
            with log_path.open("a", encoding="utf-8") as fh:
                fh.write(line)
    except OSError as exc:
        # Never crash a user-facing request because audit logging failed.
        logger.error("audit: failed to write entry — %s", exc)
