<?php

declare(strict_types=1);

namespace PpPhp\Core\Auth;

use PpPhp\Core\DB;
use PpPhp\Core\Security;

/**
 * Audit Trail for a generated app.
 *
 * Enabled in Misc → Advanced (source of truth) — the build bakes PP_AUDIT_ENABLED
 * and PP_AUDIT_TABLE from there. Nothing is logged unless it is enabled. Rows match
 * the Misc `audit_log` schema: user_id, action, table_name, record_id, old_data,
 * new_data, ip_address, created_at.
 *
 * Every call is best-effort and swallows its own errors — auditing must never break
 * a user action.
 */
final class AuditLog
{
    public static function enabled(): bool
    {
        return defined('PP_AUDIT_ENABLED') && PP_AUDIT_ENABLED && defined('PP_AUDIT_TABLE') && PP_AUDIT_TABLE !== '';
    }

    /**
     * Record one event.
     *
     * @param string               $action    e.g. create|update|delete|login|logout|login_failed|account_locked|export|access_denied
     * @param string|null          $table     the affected table (null for auth/global events)
     * @param int|string|null      $recordId  the affected record id
     * @param array<string,mixed>|null $old   values before (update/delete)
     * @param array<string,mixed>|null $new    values after (create/update)
     * @param string|null          $details   free-text context for events without a record
     */
    public static function log(string $action, ?string $table = null, int|string|null $recordId = null, ?array $old = null, ?array $new = null, ?string $details = null): void
    {
        if (!self::enabled()) return;
        try {
            $uid = (int) (Security::CurrentUserData()['id'] ?? 0);
            // For auth events (login_failed etc.) the actor may be unknown; fold a
            // details string into new_data so nothing is lost under the fixed schema.
            $new = $new ?? [];
            if ($details !== null && $details !== '') $new['_details'] = $details;

            DB::Insert(constant('PP_AUDIT_TABLE'), [
                'user_id'    => $uid,
                'action'     => substr($action, 0, 100),
                'table_name' => substr((string) ($table ?? ''), 0, 100),
                'record_id'  => $recordId === null ? null : substr((string) $recordId, 0, 100),
                'old_data'   => $old !== null ? json_encode(self::sanitize($old)) : null,
                'new_data'   => $new !== [] ? json_encode(self::sanitize($new)) : null,
                'ip_address' => substr((string) ($_SERVER['REMOTE_ADDR'] ?? ''), 0, 45),
            ]);
        } catch (\Throwable $e) {
            // best effort — never surface an auditing failure to the user
        }
    }

    /** Convenience wrappers for the common record events. */
    public static function created(string $table, int|string $id, array $new): void { self::log('create', $table, $id, null, $new); }
    public static function updated(string $table, int|string $id, array $old, array $new): void { self::log('update', $table, $id, $old, $new); }
    public static function deleted(string $table, int|string $id, ?array $old = null): void { self::log('delete', $table, $id, $old, null); }

    /** Drop obviously-sensitive keys and truncate long values before storing. */
    private static function sanitize(array $data): array
    {
        $out = [];
        foreach ($data as $k => $v) {
            $lk = strtolower((string) $k);
            if (str_contains($lk, 'password') || str_contains($lk, 'secret') || $lk === 'token' || str_contains($lk, 'api_key')) {
                $out[$k] = '***';
                continue;
            }
            if (is_scalar($v) || $v === null) {
                $s = (string) ($v ?? '');
                $out[$k] = strlen($s) > 500 ? substr($s, 0, 500) . '…' : $v;
            } else {
                $out[$k] = '[' . gettype($v) . ']';
            }
        }
        return $out;
    }
}
