<?php
declare(strict_types=1);

namespace App\Modules\Audit\Services;

use App\Core\App;
use App\Core\Logger;

final class AuditService
{
    /**
     * Tables we NEVER audit (they'd cause infinite loops or are just noise).
     */
    private const IGNORE_TABLES = [
        'audit_trail',
        'login_attempts',
        'active_sessions',
        'migrations',
        'seeds',
        'async_event_queue',
        'cron_job_log',
        'password_history',
        'sms_log',
        'notification_queue',
    ];

    /**
     * Sensitive fields — values are masked in audit logs.
     */
    private const SENSITIVE_FIELDS = [
        'password_hash',
        'password',
    ];

    /**
     * Fields to skip in change tracking (they change on every save and add noise).
     */
    private const NOISE_FIELDS = [
        'updated_at',
    ];

    /**
     * Log an audit entry directly.
     */
    public static function log(
        string $action,
        ?string $entityType = null,
        ?int $entityId = null,
        ?string $entityLabel = null,
        ?array $before = null,
        ?array $after = null,
        ?string $notes = null
    ): void {
        try {
            $app = App::getInstance();
            $db = $app->db();
            $employee = $app->currentEmployee();
            $session = $app->session();

            // Calculate changed fields
            $changedFields = null;
            if ($before !== null && $after !== null) {
                $changed = self::computeChangedFields($before, $after);
                if (!empty($changed)) {
                    $changedFields = $changed;
                }
            }

            // Mask sensitive fields
            if ($before !== null) $before = self::maskSensitive($before);
            if ($after !== null) $after = self::maskSensitive($after);

            $data = [
                'employee_id'         => $employee ? ($employee->id ?? null) : null,
                'employee_name'       => $employee ? ($employee->full_name_ar ?? null) : null,
                'action'              => $action,
                'entity_type'         => $entityType,
                'entity_id'           => $entityId,
                'entity_label'        => $entityLabel ? mb_substr($entityLabel, 0, 255) : null,
                'before_data_json'    => $before !== null ? json_encode($before, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR) : null,
                'after_data_json'     => $after !== null ? json_encode($after, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR) : null,
                'changed_fields_json' => $changedFields !== null ? json_encode($changedFields, JSON_UNESCAPED_UNICODE) : null,
                'ip_address'          => $_SERVER['REMOTE_ADDR'] ?? null,
                'user_agent'          => isset($_SERVER['HTTP_USER_AGENT']) ? mb_substr($_SERVER['HTTP_USER_AGENT'], 0, 500) : null,
                'session_id'          => $session ? ($session->id() ?: null) : null,
                'route'               => $_SERVER['REQUEST_URI'] ?? null,
                'notes'               => $notes,
                'created_at'          => date('Y-m-d H:i:s'),
            ];

            // Direct insert — bypasses hooks to avoid infinite loop
            $columns = array_keys($data);
            $placeholders = array_fill(0, count($columns), '?');
            $sql = sprintf(
                'INSERT INTO `audit_trail` (%s) VALUES (%s)',
                implode(', ', array_map(fn($c) => "`{$c}`", $columns)),
                implode(', ', $placeholders)
            );
            $db->query($sql, array_values($data));

        } catch (\Throwable $e) {
            // Audit must NEVER crash the application
            try {
                Logger::warning('Audit log failed: ' . $e->getMessage());
            } catch (\Throwable $e2) {}
        }
    }

    /**
     * Compute which fields actually changed between two records.
     * Returns array of field names with old/new values.
     */
    private static function computeChangedFields(array $before, array $after): array
    {
        $changed = [];
        $allKeys = array_unique(array_merge(array_keys($before), array_keys($after)));

        foreach ($allKeys as $key) {
            // Skip noise fields
            if (in_array($key, self::NOISE_FIELDS, true)) {
                continue;
            }

            $oldVal = $before[$key] ?? null;
            $newVal = $after[$key] ?? null;

            // Normalize for comparison
            $oldStr = ($oldVal === null) ? '' : (string) $oldVal;
            $newStr = ($newVal === null) ? '' : (string) $newVal;

            if ($oldStr !== $newStr) {
                // Mask sensitive
                if (in_array($key, self::SENSITIVE_FIELDS, true)) {
                    $changed[$key] = ['from' => '***', 'to' => '***'];
                } else {
                    $changed[$key] = [
                        'from' => $oldVal,
                        'to'   => $newVal,
                    ];
                }
            }
        }

        return $changed;
    }

    /**
     * Mask sensitive field values.
     */
    private static function maskSensitive(array $data): array
    {
        foreach (self::SENSITIVE_FIELDS as $field) {
            if (array_key_exists($field, $data)) {
                $data[$field] = '***MASKED***';
            }
        }
        return $data;
    }

    /**
     * Get a human-readable label for a record.
     */
    private static function extractLabel(string $table, array $data): ?string
    {
        // Try common label fields in priority order
        $labelFields = [
            'full_name_ar', 'name_ar', 'username', 'membership_number',
            'form_number', 'branch_code', 'role_code', 'carnet_number',
            'receipt_number', 'rule_code', 'template_code', 'service_code',
            'form_code', 'workflow_code', 'report_code', 'discount_code',
        ];

        foreach ($labelFields as $field) {
            if (!empty($data[$field])) {
                return mb_substr((string) $data[$field], 0, 255);
            }
        }

        return null;
    }

    /**
     * Log login events.
     */
    public static function logLogin(string $username, bool $success, ?string $reason = null): void
    {
        self::log(
            $success ? 'login' : 'login_failed',
            'employee',
            null,
            $username,
            null,
            null,
            $success ? "تسجيل دخول ناجح: {$username}" : "فشل تسجيل الدخول: {$username} - {$reason}"
        );
    }

    /**
     * Log logout events.
     */
    public static function logLogout(): void
    {
        $employee = App::getInstance()->currentEmployee();
        $name = $employee ? ($employee->full_name_ar ?? 'unknown') : 'unknown';
        self::log('logout', 'employee', $employee ? (int) $employee->id : null, $name);
    }

    /**
     * Register database hooks for AUTOMATIC audit logging of ALL operations.
     * This is the nuclear option — every INSERT, UPDATE, DELETE gets logged.
     */
    public static function registerDatabaseHooks(): void
    {
        try {
            $db = App::getInstance()->db();

            if (!$db->tableExists('audit_trail')) {
                return;
            }

            // ══════════════════════════════════════
            // HOOK: After INSERT — log creation
            // ══════════════════════════════════════
            $db->onAfterInsert(function (string $table, array $data, ?int $id) {
                if (in_array($table, self::IGNORE_TABLES, true)) {
                    return;
                }

                $label = self::extractLabel($table, $data);

                self::log(
                    'create',
                    $table,
                    $id,
                    $label,
                    null,   // no before (it's a creation)
                    $data,  // after = the inserted data
                    null
                );
            });

            // ══════════════════════════════════════
            // HOOK: After UPDATE — log changes with before/after
            // ══════════════════════════════════════
            $db->onAfterUpdate(function (string $table, array $changedData, ?int $entityId, ?array $oldRecord, ?array $newRecord) {
                if (in_array($table, self::IGNORE_TABLES, true)) {
                    return;
                }

                // Don't log if nothing actually changed
                if ($oldRecord && $newRecord) {
                    $changed = self::computeChangedFields($oldRecord, $newRecord);
                    if (empty($changed)) {
                        return; // No real changes
                    }
                }

                $label = null;
                if ($newRecord) {
                    $label = self::extractLabel($table, $newRecord);
                } elseif ($oldRecord) {
                    $label = self::extractLabel($table, $oldRecord);
                }

                self::log(
                    'update',
                    $table,
                    $entityId,
                    $label,
                    $oldRecord,  // full record before
                    $newRecord,  // full record after
                    null
                );
            });

            // ══════════════════════════════════════
            // HOOK: After DELETE — log deletion with old data
            // ══════════════════════════════════════
            $db->onAfterDelete(function (string $table, array $oldRecord, ?int $entityId) {
                if (in_array($table, self::IGNORE_TABLES, true)) {
                    return;
                }

                $label = !empty($oldRecord) ? self::extractLabel($table, $oldRecord) : null;

                self::log(
                    'delete',
                    $table,
                    $entityId,
                    $label,
                    $oldRecord,  // what was deleted
                    null,        // nothing after (it's gone)
                    "Deleted from {$table}" . ($entityId ? " ID: {$entityId}" : '')
                );
            });

        } catch (\Throwable $e) {
            try {
                Logger::warning('Failed to register audit hooks: ' . $e->getMessage());
            } catch (\Throwable $e2) {}
        }
    }
}