<?php

declare(strict_types=1);

namespace App\Services;

use CodeIgniter\Database\BaseConnection;
use CodeIgniter\HTTP\RequestInterface;
use Config\Database;
use Config\Services;
use Throwable;

/**
 * Audit trail untuk aksi admin (API `/api/admin/*` dan keamanan).
 */
class AdminAuditService
{
    private const MAX_PAYLOAD_CHARS = 65535;

    private BaseConnection $db;

    public function __construct(?BaseConnection $db = null)
    {
        $this->db = $db ?? Database::connect();
    }

    /**
     * Catat satu baris audit. `payload` disimpan sebagai JSON (disederhanakan & dibatasi panjang).
     * Konteks sesi (auth_source, grup Ion) diambil dari session saat dipanggil dari request web.
     *
     * @param array<string, mixed> $payload
     */
    public function log(string $action, array $payload = []): void
    {
        try {
            if (! $this->db->tableExists('admin_activity_logs')) {
                log_message('debug', 'AdminAuditService: tabel admin_activity_logs belum ada — jalankan migrasi.');

                return;
            }

            $req = Services::request();
            $sess = session();

            $outcome = (string) ($payload['__outcome'] ?? 'success');
            $body    = $payload;
            unset($body['__outcome']);

            $row = [
                'admin_user'   => $this->resolveAdminUserLabel($sess, $body),
                'action'       => $this->truncate($action, 128),
                'endpoint'     => $this->truncate($req->getUri()->getPath(), 512),
                'payload'      => $this->encodePayload($this->enrichPayload($req, $sess, $body)),
                'ip_address'   => $this->truncate($req->getIPAddress(), 45),
                'user_agent'   => $this->truncate($req->getUserAgent()->__toString(), 512),
                'auth_source'  => $this->truncate((string) ($sess->get('admin_auth_source') ?? ''), 32) ?: null,
                'roles_json'   => $this->encodeRolesJson($sess),
                'outcome'      => $this->truncate($outcome, 32),
                'created_at'   => date('Y-m-d H:i:s'),
            ];

            $this->db->table('admin_activity_logs')->insert($row);
        } catch (Throwable $e) {
            log_message('error', 'AdminAuditService::log gagal: ' . $e->getMessage());
        }
    }

    /**
     * @param array<string, mixed> $sessPayload
     */
    private function resolveAdminUserLabel($sess, array $sessPayload): string
    {
        $u = $sess->get('admin_username');
        if (is_string($u) && $u !== '') {
            return $this->truncate($u, 191);
        }

        $actor = $sessPayload['actor'] ?? null;
        if (is_array($actor)) {
            $nip = (string) ($actor['nip'] ?? '');
            $id  = (string) ($actor['id_pegawai'] ?? '');
            if ($nip !== '') {
                return $this->truncate('pegawai:' . $nip, 191);
            }
            if ($id !== '') {
                return $this->truncate('pegawai:id:' . $id, 191);
            }
        }

        return 'unknown';
    }

    /**
     * @param array<string, mixed> $payload
     *
     * @return array<string, mixed>
     */
    private function enrichPayload(RequestInterface $req, $sess, array $payload): array
    {
        $out = $this->sanitizeForStorage($payload);

        $out['_http'] = [
            'method' => $req->getMethod(),
            'uri'    => (string) $req->getUri(),
        ];

        $out['_session_panel'] = [
            'has_admin_token' => is_string($sess->get('admin_mobile_token')) && $sess->get('admin_mobile_token') !== '',
            'auth_source'     => $sess->get('admin_auth_source'),
            'ion_groups'      => $sess->get('admin_ion_groups'),
        ];

        return $out;
    }

    /**
     * @param array<string, mixed> $data
     *
     * @return array<string, mixed>
     */
    private function sanitizeForStorage(array $data): array
    {
        $redactKeys = ['password', 'token', 'admin_mobile_token'];

        return $this->stripSensitiveRecursive($data, $redactKeys);
    }

    /**
     * @param array<string, mixed> $data
     * @param list<string>         $redactKeys
     *
     * @return array<string, mixed>
     */
    private function stripSensitiveRecursive(array $data, array $redactKeys): array
    {
        $out = [];
        foreach ($data as $k => $v) {
            $key = (string) $k;
            if (in_array(strtolower($key), $redactKeys, true)) {
                $out[$key] = '[redacted]';

                continue;
            }
            if (is_array($v)) {
                $out[$key] = $this->stripSensitiveRecursive($v, $redactKeys);
            } elseif (is_scalar($v) || $v === null) {
                $out[$key] = $v;
            }
        }

        return $out;
    }

    /**
     * @param array<string, mixed> $payload
     */
    private function encodePayload(array $payload): ?string
    {
        $json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
        if ($json === false) {
            return null;
        }

        if (strlen($json) > self::MAX_PAYLOAD_CHARS) {
            $json = substr($json, 0, self::MAX_PAYLOAD_CHARS - 40) . '…[truncated]';
        }

        return $json;
    }

    private function encodeRolesJson($sess): ?string
    {
        $g = $sess->get('admin_ion_groups');
        if (! is_array($g)) {
            return null;
        }

        $enc = json_encode(array_values($g), JSON_UNESCAPED_UNICODE);
        if ($enc === false) {
            return null;
        }

        return strlen($enc) > 16000 ? substr($enc, 0, 16000) : $enc;
    }

    private function truncate(string $s, int $max): string
    {
        if (strlen($s) <= $max) {
            return $s;
        }

        return substr($s, 0, max(0, $max - 3)) . '...';
    }
}
