<?php

declare(strict_types=1);

namespace App\Controllers\Api\Admin;

use App\Controllers\BaseController;
use App\Services\Admin\AdminApiService;
use App\Services\Admin\AdminExtraApiService;
use App\Services\AdminAuditService;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface;

/**
 * Dasar API `/api/admin/*`: wajib **sesi panel admin** + token yang sama dengan sesi;
 * RBAC per fitur (`canAccess`). Tanpa cookie sesi, panggilan hanya dengan `?token=` ditolak.
 *
 * Audit: setelah otorisasi sukses, controller anak memanggil {@see auditAuthorized()}.
 * Percobaan ditolak dicatat ke DB + `log_message`.
 */
abstract class BaseAdminApiController extends BaseController
{
    protected AdminApiService $adminApi;

    protected AdminExtraApiService $adminExtra;

    private AdminAuditService $audit;

    public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger): void
    {
        parent::initController($request, $response, $logger);
        helper('rbac');
        $this->adminApi   = new AdminApiService();
        $this->adminExtra = new AdminExtraApiService();
        $this->audit      = new AdminAuditService();
    }

    /**
     * @param array<string, mixed> $payload
     */
    protected function auditAuthorized(string $action, array $actor, array $payload = []): void
    {
        $payload['actor'] = $this->actorAuditSummary($actor);
        $this->audit->log($action, $payload);
    }

    /**
     * @param array<string, mixed> $actor
     *
     * @return array<string, mixed>
     */
    private function actorAuditSummary(array $actor): array
    {
        return [
            'id_pegawai'   => $actor['id_pegawai'] ?? null,
            'nip'          => $actor['nip'] ?? null,
            'nama_lengkap' => $actor['nama_lengkap'] ?? null,
        ];
    }

    /**
     * Ringkasan GET/POST untuk payload audit (dibatasi ukuran).
     *
     * @return array<string, mixed>
     */
    protected function auditRequestParams(): array
    {
        $get  = $this->request->getGet();
        $post = $this->request->getPost();

        return [
            'query' => is_array($get) ? $this->truncateParamMap($get, 40) : [],
            'post'  => is_array($post) ? $this->truncateParamMap($post, 40) : [],
        ];
    }

    /**
     * @param array<string, mixed> $map
     *
     * @return array<string, mixed>
     */
    private function truncateParamMap(array $map, int $maxKeys): array
    {
        $i   = 0;
        $out = [];
        foreach ($map as $k => $v) {
            if ($i++ >= $maxKeys) {
                $out['_truncated'] = true;

                break;
            }
            if (is_scalar($v) || $v === null) {
                $out[(string) $k] = $v;
            } elseif (is_array($v)) {
                $out[(string) $k] = '[array]';
            }
        }

        return $out;
    }

    /**
     * @param array<string, mixed> $payload
     */
    protected function respond(array $payload, int $code = 200): ResponseInterface
    {
        return $this->response->setStatusCode($code)->setJSON($payload);
    }

    protected function extractToken(): string
    {
        $t = $this->request->getGet('token')
            ?? $this->request->getPost('token')
            ?? $this->request->getHeaderLine('X-Admin-Token');

        return is_string($t) ? trim($t) : '';
    }

    /**
     * Sesi admin panel aktif + token permintaan sama dengan `admin_mobile_token` di sesi.
     *
     * @return array{actor: array<string, mixed>|null, response: null}|array{actor: null, response: ResponseInterface}
     */
    protected function requireAdminSessionBoundToken(): array
    {
        $sess = session();
        $sessTok = $sess->get('admin_mobile_token');
        if (! is_string($sessTok) || $sessTok === '') {
            $this->logAdminSecurityDenied('no_admin_session', 'unauthorized');

            return [
                'actor'    => null,
                'response' => $this->respond(['status' => 0, 'pesan' => 'Unauthorized'], 401),
            ];
        }

        $reqTok = $this->extractToken();
        if ($reqTok === '' || $reqTok !== $sessTok) {
            $this->logAdminSecurityDenied('token_not_bound_to_session', 'unauthorized');

            return [
                'actor'    => null,
                'response' => $this->respond(['status' => 0, 'pesan' => 'Unauthorized'], 401),
            ];
        }

        $actor = $this->adminApi->actorFromToken($reqTok);
        if ($actor === null) {
            $this->logAdminSecurityDenied('invalid_token_actor', 'unauthorized');

            return [
                'actor'    => null,
                'response' => $this->respond(['status' => 0, 'pesan' => 'Token tidak valid'], 403),
            ];
        }

        return ['actor' => $actor, 'response' => null];
    }

    /**
     * RBAC fitur — memakai `admin_ion_groups` + `Config\AdminAccess` (sama lapisan web).
     */
    protected function requireAdminFeature(string $feature): ?ResponseInterface
    {
        if (canAccess($feature)) {
            return null;
        }

        $this->logAdminSecurityDenied('forbidden_feature:' . $feature, 'forbidden');

        return $this->respond(['status' => 0, 'pesan' => 'Forbidden'], 403);
    }

    /**
     * Gabungan autentikasi sesi + RBAC satu pintu untuk endpoint admin API.
     *
     * @return array{actor: array<string, mixed>|null, response: null}|array{actor: null, response: ResponseInterface}
     */
    protected function requireAdminApiAccess(string $feature): array
    {
        $auth = $this->requireAdminSessionBoundToken();
        if ($auth['response'] !== null) {
            return $auth;
        }

        $deny = $this->requireAdminFeature($feature);
        if ($deny !== null) {
            return ['actor' => $auth['actor'], 'response' => $deny];
        }

        return ['actor' => $auth['actor'], 'response' => null];
    }

    /**
     * Filter cabang untuk grup Ion `supervisor`: data pegawai/cuti/presensi dibatasi ke `pegawai.kantor`
     * yang sama dengan baris pegawai pemegang token (bukan dari jabatan).
     *
     * @return array{scoped: bool, kantor_id: int|null} scoped=true & kantor_id=null → pegawai supervisor belum punya kantor
     */
    protected function cabangScopeFromActor(?array $actor): array
    {
        if ($actor === null || ! rbac_enforce_ion()) {
            return ['scoped' => false, 'kantor_id' => null];
        }
        if (hasRole('webmaster') || hasRole('hrd')) {
            return ['scoped' => false, 'kantor_id' => null];
        }
        if (! hasRole('supervisor')) {
            return ['scoped' => false, 'kantor_id' => null];
        }
        $kid = (int) ($actor['kantor'] ?? 0);
        if ($kid <= 0) {
            return ['scoped' => true, 'kantor_id' => null];
        }

        return ['scoped' => true, 'kantor_id' => $kid];
    }

    /**
     * Supervisor wajib punya `kantor` pada baris pegawai (token) agar scope cabang terdefinisi.
     */
    protected function denyIfSupervisorCabangInvalid(array $scope): ?ResponseInterface
    {
        if ($scope['scoped'] && $scope['kantor_id'] === null) {
            return $this->respond([
                'status' => 0,
                'pesan'  => 'Akun supervisor belum memiliki cabang (kantor) pada data pegawai. Hubungi HRD untuk mengisi kolom kantor.',
            ], 403);
        }

        return null;
    }

    /** @return int|null null = tanpa filter cabang */
    protected function cabangKantorIdForQueries(array $scope): ?int
    {
        return $scope['scoped'] ? $scope['kantor_id'] : null;
    }

    /**
     * Setelah {@see requireAdminApiAccess()} sukses: validasi kantor supervisor & siapkan filter cabang.
     *
     * @return array{kid: int|null, response: ResponseInterface|null}
     */
    protected function cabangKantorAfterAuth(?array $actor): array
    {
        $scope = $this->cabangScopeFromActor($actor);
        if (($deny = $this->denyIfSupervisorCabangInvalid($scope)) !== null) {
            return ['kid' => null, 'response' => $deny];
        }

        return ['kid' => $this->cabangKantorIdForQueries($scope), 'response' => null];
    }

    private function logAdminSecurityDenied(string $reason, string $outcome): void
    {
        $ip   = $this->request->getIPAddress();
        $path = $this->request->getUri()->getPath();
        $when = date('c');
        log_message('warning', "[api/admin] denied={$reason} outcome={$outcome} ip={$ip} path={$path} at={$when}");

        $action = $outcome === 'forbidden' ? 'api.admin.security.forbidden' : 'api.admin.security.unauthorized';
        $this->audit->log($action, [
            'reason'    => $reason,
            '__outcome' => $outcome,
        ]);
    }
}
