"""Admin/scheduler audit trail: an append-only record of who changed what and when.

Design (locked): log at ACTION / job-run granularity, NEVER per-row — a scheduler run that
touches thousands of videos is ONE audit row summarising the outcome. Automatic job runs are
only logged when they actually changed something (or errored); all admin actions and all manual
runs are always logged. Secrets are never stored (config changes log the key name only).

`record()` only db.add()s the row — the caller commits it in the SAME transaction as the
mutation it describes, so the trail can't drift from reality (and a rolled-back mutation drops
its audit row too).
"""
from datetime import datetime, timedelta, timezone

from sqlalchemy import delete
from sqlalchemy.orm import Session

from app.models import AuditLog


class AuditAction:
    """Canonical `<entity>_<action>` audit keys — the single source of truth. User-facing labels
    are resolved on the client via i18n `auditActions.<key>` (mirrors quota.QuotaAction)."""

    # User / roles / access
    USER_ROLE_CHANGE = "user_role_change"
    USER_SUSPEND = "user_suspend"
    USER_UNSUSPEND = "user_unsuspend"
    USER_DELETE = "user_delete"
    INVITE_APPROVE = "invite_approve"
    INVITE_DENY = "invite_deny"
    INVITE_ADD = "invite_add"
    DEMO_ADD = "demo_add"
    DEMO_REMOVE = "demo_remove"
    DEMO_RESET = "demo_reset"
    DISCOVERY_PURGE = "discovery_purge"
    # Config
    CONFIG_SET = "config_set"
    CONFIG_RESET = "config_reset"
    # Scheduler / sync
    SCHEDULER_INTERVAL = "scheduler_interval"
    MAINTENANCE_TUNE = "maintenance_tune"
    SYNC_PAUSE = "sync_pause"
    SYNC_RESUME = "sync_resume"
    # Scheduler job outcome / run-summary (actor_id set = a manual "run now"; NULL = automatic)
    JOB_RUN = "job_run"
    # The log itself was cleared (a single row survives the clear as tamper-evidence)
    AUDIT_CLEAR = "audit_clear"


def record(
    db: Session,
    actor_id: int | None,
    action: str,
    *,
    target_type: str | None = None,
    target_id: str | int | None = None,
    summary: str | None = None,
    before: dict | None = None,
    after: dict | None = None,
) -> None:
    """Append an audit row (NOT committed — the caller commits it with the mutation)."""
    db.add(
        AuditLog(
            actor_id=actor_id,
            action=action,
            target_type=target_type,
            # Truncate to the column widths so an over-long target/summary can never abort the
            # mutation this row is committed alongside (an audit row must not break what it observes).
            target_id=None if target_id is None else str(target_id)[:128],
            summary=(summary or None) and summary[:255],
            before=before,
            after=after,
        )
    )


def gc(db: Session, retention_days: int) -> int:
    """Delete audit rows older than `retention_days`. Returns the number removed. A
    retention_days <= 0 disables pruning (keep forever)."""
    if retention_days <= 0:
        return 0
    cutoff = datetime.now(timezone.utc) - timedelta(days=retention_days)
    result = db.execute(delete(AuditLog).where(AuditLog.created_at < cutoff))
    db.commit()
    return result.rowcount or 0
