<?php

declare(strict_types=1);

namespace App\Services\Admin;

use App\Models\PegawaiModel;
use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Database\BaseConnection;
use Config\Database;
use Config\Services;

/**
 * Logika API admin (/api/admin/*) — akses DB hanya dari sini, bukan dari controller web admin.
 */
class AdminApiService
{
    protected BaseConnection $db;

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

    /**
     * @return array<string, mixed>|null baris pegawai (tanpa password) atau null
     */
    public function actorFromToken(string $token): ?array
    {
        if ($token === '') {
            return null;
        }

        $row = $this->db->table('pegawai')->where('token', $token)->get()->getRowArray();
        if ($row === null) {
            return null;
        }

        unset($row['password']);

        return $row;
    }

    /**
     * @return array{status: int, pesan: string, data?: mixed}
     */
    public function references(?int $cabangKantorId = null): array
    {
        $kantor = $this->db->table('kantor')->orderBy('nama_kantor')->get()->getResultArray();
        if ($cabangKantorId !== null && $cabangKantorId > 0) {
            $kantor = array_values(array_filter(
                $kantor,
                static fn ($r) => (int) ($r['id_kantor'] ?? 0) === $cabangKantorId
            ));
        }

        return [
            'status' => 1,
            'pesan'  => 'OK',
            'data'   => [
                'jabatan'    => $this->db->table('jabatan')->orderBy('nama_jabatan')->get()->getResultArray(),
                'unit_kerja' => $this->db->table('unit_kerja')->orderBy('nama_unit_kerja')->get()->getResultArray(),
                'golongan'   => $this->db->table('golongan')->orderBy('nama_golongan')->get()->getResultArray(),
                'kantor'     => $kantor,
                'jadwal'     => $this->db->table('jadwal')->orderBy('nama_jadwal')->get()->getResultArray(),
            ],
        ];
    }

    /**
     * @return array{status: int, pesan: string, data?: mixed}
     */
    public function pegawaiList(int $page, int $perPage, string $search = '', ?int $cabangKantorId = null): array
    {
        $perPage = max(5, min(500, $perPage));
        $page    = max(1, $page);
        $offset  = ($page - 1) * $perPage;

        $countB = $this->db->table('pegawai p')
            ->join('jabatan j', 'j.id_jabatan = p.jabatan', 'left')
            ->join('unit_kerja u', 'u.id_unit_kerja = p.unit_kerja', 'left')
            ->join('golongan g', 'g.id_golongan = p.golongan_pekerjaan', 'left')
            ->join('kantor k', 'k.id_kantor = p.kantor', 'left')
            ->join('jadwal jd', 'jd.id_jadwal = p.jadwal', 'left');
        $this->applyCabangPegawaiAlias($countB, 'p', $cabangKantorId);
        $this->applyPegawaiSearchFilter($countB, $search);
        $total = (int) $countB->countAllResults();

        $b = $this->db->table('pegawai p')
            ->select('p.id_pegawai, p.nip, p.nama_lengkap, p.jenis_kelamin, p.photo, p.email, p.jabatan, p.unit_kerja, p.golongan_pekerjaan, p.kantor, p.jadwal, p.status_kepegawaian, p.super_akses')
            ->select('j.nama_jabatan, u.nama_unit_kerja, g.nama_golongan, k.nama_kantor, jd.nama_jadwal')
            ->join('jabatan j', 'j.id_jabatan = p.jabatan', 'left')
            ->join('unit_kerja u', 'u.id_unit_kerja = p.unit_kerja', 'left')
            ->join('golongan g', 'g.id_golongan = p.golongan_pekerjaan', 'left')
            ->join('kantor k', 'k.id_kantor = p.kantor', 'left')
            ->join('jadwal jd', 'jd.id_jadwal = p.jadwal', 'left');
        $this->applyCabangPegawaiAlias($b, 'p', $cabangKantorId);
        $this->applyPegawaiSearchFilter($b, $search);

        $rows = $b->orderBy('p.nama_lengkap', 'ASC')
            ->limit($perPage, $offset)
            ->get()
            ->getResultArray();

        return [
            'status' => 1,
            'pesan'  => 'OK',
            'data'   => [
                'rows'       => $rows,
                'total'      => $total,
                'page'       => $page,
                'per_page'   => $perPage,
                'total_page' => (int) ceil($total / $perPage),
            ],
        ];
    }

    /**
     * @return array{status: int, pesan: string, data?: mixed}
     */
    public function pegawaiShow(int $id, ?int $cabangKantorId = null): array
    {
        $row = $this->fetchPegawaiDetail($id);
        if ($row === null) {
            return ['status' => 0, 'pesan' => 'Data pegawai tidak ditemukan'];
        }
        if (! $this->pegawaiRowInCabang($row, $cabangKantorId)) {
            return ['status' => 0, 'pesan' => 'Data pegawai tidak ditemukan'];
        }

        return ['status' => 1, 'pesan' => 'OK', 'data' => $row];
    }

    /**
     * @param array<string, scalar|null> $input
     *
     * @return array{status: int, pesan: string, data?: mixed}
     */
    public function pegawaiCreate(array $input, ?int $cabangKantorId = null): array
    {
        $validation = Services::validation();
        $validation->setRules([
            'nip'                => 'required|max_length[50]|is_unique[pegawai.nip]',
            'nama_lengkap'       => 'required|max_length[50]',
            'jenis_kelamin'      => 'required|in_list[Pria,Wanita]',
            'tempat_lahir'       => 'permit_empty|max_length[100]',
            'tanggal_lahir'      => 'permit_empty|valid_date',
            'email'              => 'permit_empty|max_length[50]',
            'jabatan'            => 'required|integer',
            'unit_kerja'         => 'required|integer',
            'golongan_pekerjaan' => 'required|integer',
            'kantor'             => 'required|integer',
            'status_kepegawaian' => 'required|in_list[Kontrak,Pegawai Tetap]',
            'tanggal_bergabung'  => 'required|valid_date',
            'jadwal'             => 'required|integer',
            'super_akses'        => 'permit_empty|in_list[false,true]',
            'photo'              => 'permit_empty|max_length[255]',
            'username'         => 'required|max_length[50]|is_unique[pegawai.username]',
            'password'         => 'permit_empty|max_length[50]',
        ]);

        if (! $validation->run($input)) {
            return ['status' => 0, 'pesan' => implode(' ', $validation->getErrors())];
        }

        if ($cabangKantorId !== null && $cabangKantorId > 0 && (int) $input['kantor'] !== $cabangKantorId) {
            return ['status' => 0, 'pesan' => 'Cabang pegawai harus sama dengan cabang Anda.'];
        }

        $nip      = (string) $input['nip'];
        $password = isset($input['password']) && (string) $input['password'] !== ''
            ? md5((string) $input['password'])
            : md5($nip);

        $model = new PegawaiModel();
        $kantorVal = $cabangKantorId !== null && $cabangKantorId > 0 ? $cabangKantorId : (int) $input['kantor'];
        $photoIn = trim((string) ($input['photo'] ?? ''));
        $data  = [
            'nip'                => $nip,
            'nama_lengkap'       => (string) $input['nama_lengkap'],
            'jenis_kelamin'      => (string) $input['jenis_kelamin'],
            'tempat_lahir'       => (string) ($input['tempat_lahir'] ?? ''),
            'tanggal_lahir'      => $this->normalizeDate($input['tanggal_lahir'] ?? null),
            'photo'              => $photoIn === '' || $photoIn === '-' ? '' : substr($photoIn, 0, 255),
            'email'              => (string) ($input['email'] ?? ''),
            'jabatan'            => (int) $input['jabatan'],
            'unit_kerja'         => (int) $input['unit_kerja'],
            'golongan_pekerjaan' => (int) $input['golongan_pekerjaan'],
            'kantor'             => $kantorVal,
            'status_kepegawaian' => (string) $input['status_kepegawaian'],
            'tanggal_bergabung'  => (string) $input['tanggal_bergabung'],
            'jadwal'             => (int) $input['jadwal'],
            'super_akses'        => (string) ($input['super_akses'] ?? 'false'),
            'username'           => (string) $input['username'],
            'password'           => $password,
            'token'              => '',
            'last_login'         => null,
        ];

        if ($model->insert($data, true) === false) {
            return ['status' => 0, 'pesan' => implode(' ', $model->errors())];
        }

        $id = (int) $model->getInsertID();

        return ['status' => 1, 'pesan' => 'Data berhasil disimpan', 'data' => ['id_pegawai' => $id]];
    }

    /**
     * @param array<string, scalar|null> $input
     *
     * @return array{status: int, pesan: string, data?: mixed}
     */
    public function pegawaiUpdate(int $id, array $input, ?int $cabangKantorId = null): array
    {
        $model = new PegawaiModel();
        if ($model->find($id) === null) {
            return ['status' => 0, 'pesan' => 'Data pegawai tidak ditemukan'];
        }
        if (! $this->pegawaiIdAllowedForCabang($id, $cabangKantorId)) {
            return ['status' => 0, 'pesan' => 'Data pegawai tidak ditemukan'];
        }

        $validation = Services::validation();
        $validation->setRules([
            'nip'                => "required|max_length[50]|is_unique[pegawai.nip,id_pegawai,{$id}]",
            'nama_lengkap'       => 'required|max_length[50]',
            'jenis_kelamin'      => 'required|in_list[Pria,Wanita]',
            'tempat_lahir'       => 'permit_empty|max_length[100]',
            'tanggal_lahir'      => 'permit_empty|valid_date',
            'email'              => 'permit_empty|max_length[50]',
            'jabatan'            => 'required|integer',
            'unit_kerja'         => 'required|integer',
            'golongan_pekerjaan' => 'required|integer',
            'kantor'             => 'required|integer',
            'status_kepegawaian' => 'required|in_list[Kontrak,Pegawai Tetap]',
            'tanggal_bergabung'  => 'required|valid_date',
            'jadwal'             => 'required|integer',
            'super_akses'        => 'permit_empty|in_list[false,true]',
            'photo'              => 'permit_empty|max_length[255]',
            'username'           => "required|max_length[50]|is_unique[pegawai.username,id_pegawai,{$id}]",
            'password'           => 'permit_empty|max_length[50]',
        ]);

        if (! $validation->run($input)) {
            return ['status' => 0, 'pesan' => implode(' ', $validation->getErrors())];
        }

        if ($cabangKantorId !== null && $cabangKantorId > 0 && (int) $input['kantor'] !== $cabangKantorId) {
            return ['status' => 0, 'pesan' => 'Cabang pegawai harus sama dengan cabang Anda.'];
        }

        $kantorVal = $cabangKantorId !== null && $cabangKantorId > 0 ? $cabangKantorId : (int) $input['kantor'];
        $data = [
            'nip'                => (string) $input['nip'],
            'nama_lengkap'       => (string) $input['nama_lengkap'],
            'jenis_kelamin'      => (string) $input['jenis_kelamin'],
            'tempat_lahir'       => (string) ($input['tempat_lahir'] ?? ''),
            'tanggal_lahir'      => $this->normalizeDate($input['tanggal_lahir'] ?? null),
            'email'              => (string) ($input['email'] ?? ''),
            'jabatan'            => (int) $input['jabatan'],
            'unit_kerja'         => (int) $input['unit_kerja'],
            'golongan_pekerjaan' => (int) $input['golongan_pekerjaan'],
            'kantor'             => $kantorVal,
            'status_kepegawaian' => (string) $input['status_kepegawaian'],
            'tanggal_bergabung'  => (string) $input['tanggal_bergabung'],
            'jadwal'             => (int) $input['jadwal'],
            'super_akses'        => (string) ($input['super_akses'] ?? 'false'),
            'username'           => (string) $input['username'],
        ];

        if (array_key_exists('photo', $input)) {
            $p = trim((string) $input['photo']);
            $data['photo'] = $p === '' || $p === '-' ? '' : substr($p, 0, 255);
        }

        if (isset($input['password']) && (string) $input['password'] !== '') {
            $data['password'] = md5((string) $input['password']);
        }

        if ($model->update($id, $data) === false) {
            return ['status' => 0, 'pesan' => implode(' ', $model->errors())];
        }

        return ['status' => 1, 'pesan' => 'Data berhasil diperbarui'];
    }

    /**
     * @return array{status: int, pesan: string}
     */
    public function pegawaiDelete(int $id, ?int $cabangKantorId = null): array
    {
        $model = new PegawaiModel();
        if ($model->find($id) === null) {
            return ['status' => 0, 'pesan' => 'Data pegawai tidak ditemukan'];
        }
        if (! $this->pegawaiIdAllowedForCabang($id, $cabangKantorId)) {
            return ['status' => 0, 'pesan' => 'Data pegawai tidak ditemukan'];
        }

        try {
            $model->delete($id, true);
        } catch (\Throwable $e) {
            return ['status' => 0, 'pesan' => 'Tidak dapat menghapus pegawai (kemungkinan masih direferensikan data lain).'];
        }

        return ['status' => 1, 'pesan' => 'Data pegawai telah dihapus'];
    }

    /**
     * Setel ulang password ke md5(NIP) dan kosongkan token (seperti CI3 admin).
     *
     * @return array{status: int, pesan: string}
     */
    public function pegawaiResetPassword(int $id, ?int $cabangKantorId = null): array
    {
        $row = $this->db->table('pegawai')->select('id_pegawai, nip')->where('id_pegawai', $id)->get()->getRowArray();
        if ($row === null) {
            return ['status' => 0, 'pesan' => 'Data pegawai tidak ditemukan'];
        }
        if (! $this->pegawaiIdAllowedForCabang($id, $cabangKantorId)) {
            return ['status' => 0, 'pesan' => 'Data pegawai tidak ditemukan'];
        }

        $this->db->table('pegawai')->where('id_pegawai', $id)->update([
            'password' => md5((string) $row['nip']),
            'token'    => '',
        ]);

        return ['status' => 1, 'pesan' => 'Password direset ke NIP (hash MD5) dan token dikosongkan.'];
    }

    /**
     * Batasi query ke baris presensi yang sudah ada minimal satu rekam nyata.
     * Baris "kosong" dari API mobile presensi_today (pegawai buka app → insert stub) tidak ikut.
     */
    protected function applyPresensiHasMinimalRekam(BaseBuilder $builder): void
    {
        $builder->where(
            '(
                (pr.jam_masuk IS NOT NULL AND TRIM(CAST(pr.jam_masuk AS CHAR)) NOT IN (\'\',\'00:00\',\'00:00:00\'))
                OR (pr.jam_pulang IS NOT NULL AND TRIM(CAST(pr.jam_pulang AS CHAR)) NOT IN (\'\',\'00:00\',\'00:00:00\'))
                OR (pr.mulai_istirahat IS NOT NULL AND TRIM(CAST(pr.mulai_istirahat AS CHAR)) NOT IN (\'\',\'00:00\',\'00:00:00\'))
                OR (pr.beres_istirahat IS NOT NULL AND TRIM(CAST(pr.beres_istirahat AS CHAR)) NOT IN (\'\',\'00:00\',\'00:00:00\'))
                OR (pr.photo_masuk IS NOT NULL AND TRIM(CAST(pr.photo_masuk AS CHAR)) <> \'\')
                OR (pr.photo_pulang IS NOT NULL AND TRIM(CAST(pr.photo_pulang AS CHAR)) <> \'\')
            )',
            null,
            false
        );
    }

    /**
     * Daftar pegawai (sesuai cabang) yang hari ini belum presensi minimal dan bukan cuti Approve.
     *
     * @return list<array{id_pegawai: int, nama_lengkap: string, nip: string}>
     */
    private function belumRekamPegawaiHariIni(string $tanggalHariIni, ?int $cabangKantorId): array
    {
        $hadPresensi = $this->db->table('presensi pr')
            ->select('pr.pegawai')
            ->distinct()
            ->join('pegawai pg', 'pg.id_pegawai = pr.pegawai', 'inner')
            ->where('pr.tanggal', $tanggalHariIni);
        $this->applyPresensiHasMinimalRekam($hadPresensi);
        $this->applyCabangPegawaiAlias($hadPresensi, 'pg', $cabangKantorId);
        $idsP = [];
        foreach ($hadPresensi->get()->getResultArray() as $r) {
            $id = (int) ($r['pegawai'] ?? 0);
            if ($id > 0) {
                $idsP[] = $id;
            }
        }

        $hadCuti = $this->db->table('cuti c')
            ->select('c.pegawai')
            ->distinct()
            ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'inner')
            ->where('c.tanggal_cuti', $tanggalHariIni)
            ->where('c.status_cuti', 'Approve');
        $this->applyCabangPegawaiAlias($hadCuti, 'p', $cabangKantorId);
        $idsC = [];
        foreach ($hadCuti->get()->getResultArray() as $r) {
            $id = (int) ($r['pegawai'] ?? 0);
            if ($id > 0) {
                $idsC[] = $id;
            }
        }

        $exclude = array_values(array_unique(array_merge($idsP, $idsC)));

        $qb = $this->db->table('pegawai p')
            ->select('p.id_pegawai, p.nama_lengkap, p.nip')
            ->orderBy('p.nama_lengkap', 'ASC');
        $this->applyCabangPegawaiAlias($qb, 'p', $cabangKantorId);
        if ($exclude !== []) {
            $qb->whereNotIn('p.id_pegawai', $exclude);
        }

        $out = [];
        foreach ($qb->get()->getResultArray() as $row) {
            if (! is_array($row)) {
                continue;
            }
            $out[] = [
                'id_pegawai'   => (int) ($row['id_pegawai'] ?? 0),
                'nama_lengkap' => (string) ($row['nama_lengkap'] ?? ''),
                'nip'          => (string) ($row['nip'] ?? ''),
            ];
        }

        return $out;
    }

    /**
     * @return array{status: int, pesan: string, data?: mixed}
     */
    public function presensiList(string $tanggalDari, string $tanggalSampai, int $page, int $perPage, string $search = '', ?int $cabangKantorId = null): array
    {
        $perPage = max(5, min(200, $perPage));
        $page    = max(1, $page);
        $offset  = ($page - 1) * $perPage;
        $search  = trim($search);

        $countB = $this->db->table('presensi pr')
            ->join('pegawai pg', 'pg.id_pegawai = pr.pegawai', 'left')
            ->where('pr.tanggal >=', $tanggalDari)
            ->where('pr.tanggal <=', $tanggalSampai);
        $this->applyPresensiHasMinimalRekam($countB);
        $this->applyCabangPegawaiAlias($countB, 'pg', $cabangKantorId);
        if ($search !== '') {
            $countB->groupStart()
                ->like('pg.nama_lengkap', $search)
                ->orLike('pg.nip', $search)
                ->groupEnd();
        }
        $total = (int) $countB->countAllResults();

        $b = $this->db->table('presensi pr')
            ->select('pr.*, pg.nama_lengkap, pg.nip')
            ->join('pegawai pg', 'pg.id_pegawai = pr.pegawai', 'left')
            ->where('pr.tanggal >=', $tanggalDari)
            ->where('pr.tanggal <=', $tanggalSampai);
        $this->applyPresensiHasMinimalRekam($b);
        $this->applyCabangPegawaiAlias($b, 'pg', $cabangKantorId);
        if ($search !== '') {
            $b->groupStart()
                ->like('pg.nama_lengkap', $search)
                ->orLike('pg.nip', $search)
                ->groupEnd();
        }

        $rows = $b->orderBy('pr.tanggal', 'DESC')
            ->orderBy('pg.nama_lengkap', 'ASC')
            ->limit($perPage, $offset)
            ->get()
            ->getResultArray();

        return [
            'status' => 1,
            'pesan'  => 'OK',
            'data'   => [
                'rows'           => $rows,
                'total'          => $total,
                'page'           => $page,
                'per_page'       => $perPage,
                'total_page'     => (int) ceil($total / $perPage),
                'tanggal_dari'   => $tanggalDari,
                'tanggal_sampai' => $tanggalSampai,
                'q'              => $search,
            ],
        ];
    }

    /**
     * @return array{status: int, pesan: string, data?: mixed}
     */
    public function presensiShow(int $id, ?int $cabangKantorId = null): array
    {
        $row = $this->db->table('presensi pr')
            ->select('pr.*, pg.nama_lengkap, pg.nip, pg.email, pg.kantor as pegawai_kantor')
            ->join('pegawai pg', 'pg.id_pegawai = pr.pegawai', 'left')
            ->where('pr.id_presensi', $id)
            ->get()
            ->getRowArray();

        if ($row === null) {
            return ['status' => 0, 'pesan' => 'Data presensi tidak ditemukan'];
        }
        if (! $this->pegawaiRowInCabang(['kantor' => $row['pegawai_kantor'] ?? null], $cabangKantorId)) {
            return ['status' => 0, 'pesan' => 'Data presensi tidak ditemukan'];
        }
        unset($row['pegawai_kantor']);

        return ['status' => 1, 'pesan' => 'OK', 'data' => $row];
    }

    /**
     * Ringkasan seperti dashboard admin CI3 (Home): pegawai, presensi & cuti per rentang tanggal.
     *
     * @return array{status: int, pesan: string, data?: mixed}
     */
    public function laporanSummary(string $tanggalDari, string $tanggalSampai, ?int $cabangKantorId = null): array
    {
        $pegawaiBase = $this->db->table('pegawai');
        $this->applyCabangPegawaiAlias($pegawaiBase, 'pegawai', $cabangKantorId);
        $totalPegawai = (int) $pegawaiBase->countAllResults();

        $presensiQb = $this->db->table('presensi pr')
            ->join('pegawai pg', 'pg.id_pegawai = pr.pegawai', 'inner')
            ->where('pr.tanggal >=', $tanggalDari)
            ->where('pr.tanggal <=', $tanggalSampai);
        $this->applyPresensiHasMinimalRekam($presensiQb);
        $this->applyCabangPegawaiAlias($presensiQb, 'pg', $cabangKantorId);
        $presensiQ = (int) $presensiQb->countAllResults();

        $cutiQb = $this->db->table('cuti c')
            ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'inner')
            ->where('c.tanggal_cuti >=', $tanggalDari)
            ->where('c.tanggal_cuti <=', $tanggalSampai)
            ->where('c.status_cuti', 'Approve');
        $this->applyCabangPegawaiAlias($cutiQb, 'p', $cabangKantorId);
        $cutiQ = (int) $cutiQb->countAllResults();

        $hariIni = date('Y-m-d');
        $ph = $this->db->table('presensi pr')
            ->join('pegawai pg', 'pg.id_pegawai = pr.pegawai', 'inner')
            ->where('pr.tanggal', $hariIni);
        $this->applyPresensiHasMinimalRekam($ph);
        $this->applyCabangPegawaiAlias($ph, 'pg', $cabangKantorId);
        $presensiHariIni = (int) $ph->countAllResults();

        $ch = $this->db->table('cuti c')
            ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'inner')
            ->where('c.tanggal_cuti', $hariIni)
            ->where('c.status_cuti', 'Approve');
        $this->applyCabangPegawaiAlias($ch, 'p', $cabangKantorId);
        $cutiHariIni = (int) $ch->countAllResults();

        $belumRekam = max(0, $totalPegawai - $presensiHariIni - $cutiHariIni);

        return [
            'status' => 1,
            'pesan'  => 'OK',
            'data'   => [
                'total_pegawai'        => $totalPegawai,
                'rentang'              => ['dari' => $tanggalDari, 'sampai' => $tanggalSampai],
                'presensi_rekam'       => $presensiQ,
                'cuti_approve'         => $cutiQ,
                'hari_ini'             => $hariIni,
                'presensi_hari_ini'    => $presensiHariIni,
                'cuti_hari_ini'        => $cutiHariIni,
                'belum_rekam_hari_ini' => $belumRekam,
            ],
        ];
    }

    /**
     * Dashboard beranda admin — selaras `modules/admin/controllers/Home.php` CI3.
     *
     * @param int|null $soloPegawaiId Bila diisi (login panel sebagai pegawai saja), angka & cuti
     *                                  hanya untuk pegawai tersebut — bukan agregat seluruh perusahaan.
     *
     * @return array{status: int, pesan: string, data?: mixed}
     */
    public function dashboardHome(?int $cabangKantorId = null, ?int $soloPegawaiId = null): array
    {
        $tanggalHariIni = date('Y-m-d');

        if ($soloPegawaiId !== null && $soloPegawaiId > 0) {
            return $this->dashboardHomeSoloPegawai($tanggalHariIni, $soloPegawaiId);
        }

        $pb = $this->db->table('pegawai');
        $this->applyCabangPegawaiAlias($pb, 'pegawai', $cabangKantorId);
        $totalPegawai = (int) $pb->countAllResults();

        $pbL = $this->db->table('pegawai')->where('jenis_kelamin', 'Pria');
        $this->applyCabangPegawaiAlias($pbL, 'pegawai', $cabangKantorId);
        $pegawaiLaki = (int) $pbL->countAllResults();

        $pbP = $this->db->table('pegawai')->where('jenis_kelamin', 'Wanita');
        $this->applyCabangPegawaiAlias($pbP, 'pegawai', $cabangKantorId);
        $pegawaiPerempuan = (int) $pbP->countAllResults();

        $ph = $this->db->table('presensi pr')
            ->join('pegawai pg', 'pg.id_pegawai = pr.pegawai', 'inner')
            ->where('pr.tanggal', $tanggalHariIni);
        $this->applyPresensiHasMinimalRekam($ph);
        $this->applyCabangPegawaiAlias($ph, 'pg', $cabangKantorId);
        $presensiHariIni = (int) $ph->countAllResults();

        $ch = $this->db->table('cuti c')
            ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'inner')
            ->where('c.tanggal_cuti', $tanggalHariIni)
            ->where('c.status_cuti', 'Approve');
        $this->applyCabangPegawaiAlias($ch, 'p', $cabangKantorId);
        $cutiHariIni = (int) $ch->countAllResults();

        $belumRekam = max(0, $totalPegawai - $presensiHariIni - $cutiHariIni);

        $belumRekamPegawai = $this->belumRekamPegawaiHariIni($tanggalHariIni, $cabangKantorId);

        $persenLaki        = $totalPegawai > 0 ? round(($pegawaiLaki / $totalPegawai) * 100, 1) : 0.0;
        $persenPerempuan   = $totalPegawai > 0 ? round(($pegawaiPerempuan / $totalPegawai) * 100, 1) : 0.0;
        $persenPresensi    = $totalPegawai > 0 ? round(($presensiHariIni / $totalPegawai) * 100, 1) : 0.0;
        $persenCuti        = $totalPegawai > 0 ? round(($cutiHariIni / $totalPegawai) * 100, 1) : 0.0;
        $persenBelumRekam  = $totalPegawai > 0 ? round(($belumRekam / $totalPegawai) * 100, 1) : 0.0;

        $permohonanCutiB = $this->db->table('cuti c')
            ->select('c.id_cuti, c.pegawai, DATE_FORMAT(c.tanggal_cuti, \'%Y-%m-%d\') AS tanggal_cuti, c.tipe_cuti, c.alasan_cuti, c.status_cuti, p.nama_lengkap, p.nip', false)
            ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'left')
            ->where('c.status_cuti', 'Waiting');
        $this->applyCabangPegawaiAlias($permohonanCutiB, 'p', $cabangKantorId);
        $permohonanCuti = $permohonanCutiB->orderBy('c.tanggal_cuti', 'ASC')
            ->limit(5)
            ->get()
            ->getResultArray();
        $permohonanCuti = array_map(
            fn ($r): array => is_array($r) ? $this->normalizeCutiRowArrayForApi($r) : [],
            $permohonanCuti,
        );

        $tpc = $this->db->table('cuti c')
            ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'inner')
            ->where('c.status_cuti', 'Waiting');
        $this->applyCabangPegawaiAlias($tpc, 'p', $cabangKantorId);
        $totalPermohonanCuti = (int) $tpc->countAllResults();

        return [
            'status' => 1,
            'pesan'  => 'OK',
            'data'   => [
                'tanggal_hari_ini'       => $tanggalHariIni,
                'total_pegawai'          => $totalPegawai,
                'pegawai_laki'           => $pegawaiLaki,
                'pegawai_perempuan'      => $pegawaiPerempuan,
                'presensi_hari_ini'      => $presensiHariIni,
                'cuti_hari_ini'          => $cutiHariIni,
                'belum_rekam'            => $belumRekam,
                'persen_laki'            => $persenLaki,
                'persen_perempuan'       => $persenPerempuan,
                'persen_presensi'        => $persenPresensi,
                'persen_cuti'            => $persenCuti,
                'persen_belum_rekam'     => $persenBelumRekam,
                'permohonan_cuti'        => $permohonanCuti,
                'total_permohonan_cuti'  => $totalPermohonanCuti,
                'belum_rekam_pegawai'    => $belumRekamPegawai,
            ],
        ];
    }

    /**
     * Ringkasan beranda untuk satu pegawai (sesi login mobile / non-Ion).
     *
     * @return array{status: int, pesan: string, data?: mixed}
     */
    private function dashboardHomeSoloPegawai(string $tanggalHariIni, int $pegawaiId): array
    {
        $row = $this->db->table('pegawai')->select('id_pegawai, jenis_kelamin')->where('id_pegawai', $pegawaiId)->get()->getRowArray();
        if ($row === null) {
            return ['status' => 0, 'pesan' => 'Data pegawai tidak ditemukan'];
        }

        $jk    = (string) ($row['jenis_kelamin'] ?? '');
        $laki  = $jk === 'Pria' ? 1 : 0;
        $wanit = $jk === 'Wanita' ? 1 : 0;

        $ph = $this->db->table('presensi pr')
            ->where('pr.pegawai', $pegawaiId)
            ->where('pr.tanggal', $tanggalHariIni);
        $this->applyPresensiHasMinimalRekam($ph);
        $presensiHariIni = (int) $ph->countAllResults();

        $ch = $this->db->table('cuti c')
            ->where('c.pegawai', $pegawaiId)
            ->where('c.tanggal_cuti', $tanggalHariIni)
            ->where('c.status_cuti', 'Approve');
        $cutiHariIni = (int) $ch->countAllResults();

        $belumRekam = max(0, 1 - $presensiHariIni - $cutiHariIni);

        $permohonanCutiB = $this->db->table('cuti c')
            ->select('c.id_cuti, c.pegawai, DATE_FORMAT(c.tanggal_cuti, \'%Y-%m-%d\') AS tanggal_cuti, c.tipe_cuti, c.alasan_cuti, c.status_cuti, p.nama_lengkap, p.nip', false)
            ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'left')
            ->where('c.pegawai', $pegawaiId)
            ->where('c.status_cuti', 'Waiting');
        $permohonanCuti = $permohonanCutiB->orderBy('c.tanggal_cuti', 'ASC')
            ->limit(5)
            ->get()
            ->getResultArray();
        $permohonanCuti = array_map(
            fn ($r): array => is_array($r) ? $this->normalizeCutiRowArrayForApi($r) : [],
            $permohonanCuti,
        );

        $tpc = $this->db->table('cuti c')
            ->where('c.pegawai', $pegawaiId)
            ->where('c.status_cuti', 'Waiting');
        $totalPermohonanCuti = (int) $tpc->countAllResults();

        $totalPegawai = 1;
        $persenLaki   = $laki === 1 ? 100.0 : 0.0;
        $persenWanita = $wanit === 1 ? 100.0 : 0.0;
        if ($laki === 0 && $wanit === 0) {
            $persenLaki   = 0.0;
            $persenWanita = 0.0;
        }

        $persenPresensi   = $presensiHariIni >= 1 ? 100.0 : 0.0;
        $persenCuti       = $cutiHariIni >= 1 ? 100.0 : 0.0;
        $persenBelumRekam = $belumRekam >= 1 ? 100.0 : 0.0;

        return [
            'status' => 1,
            'pesan'  => 'OK',
            'data'   => [
                'tanggal_hari_ini'       => $tanggalHariIni,
                'total_pegawai'          => $totalPegawai,
                'pegawai_laki'           => $laki,
                'pegawai_perempuan'      => $wanit,
                'presensi_hari_ini'      => $presensiHariIni,
                'cuti_hari_ini'          => $cutiHariIni,
                'belum_rekam'            => $belumRekam,
                'persen_laki'            => $persenLaki,
                'persen_perempuan'       => $persenWanita,
                'persen_presensi'        => $persenPresensi,
                'persen_cuti'            => $persenCuti,
                'persen_belum_rekam'     => $persenBelumRekam,
                'permohonan_cuti'        => $permohonanCuti,
                'total_permohonan_cuti'  => $totalPermohonanCuti,
                'belum_rekam_pegawai'    => [],
            ],
        ];
    }

    /**
     * @return array{status: int, pesan: string, data?: mixed}
     */
    public function cutiList(string $status, int $page, int $perPage, ?int $cabangKantorId = null): array
    {
        $allowed = ['', 'Waiting', 'Approve', 'Rejected', 'Cancelled'];
        if (! in_array($status, $allowed, true)) {
            $status = 'Waiting';
        }

        $perPage = max(5, min(200, $perPage));
        $page    = max(1, $page);
        $offset  = ($page - 1) * $perPage;

        $countB = $this->db->table('cuti c')->join('pegawai p', 'p.id_pegawai = c.pegawai', 'left');
        $this->applyCabangPegawaiAlias($countB, 'p', $cabangKantorId);
        if ($status !== '') {
            $countB->where('c.status_cuti', $status);
        }
        $total = (int) $countB->countAllResults();

        $b = $this->db->table('cuti c')
            ->select('c.id_cuti, c.pegawai, DATE_FORMAT(c.tanggal_cuti, \'%Y-%m-%d\') AS tanggal_cuti, c.tipe_cuti, c.alasan_cuti, c.status_cuti, c.alasan_tolak, p.nama_lengkap, p.nip', false)
            ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'left');
        $this->applyCabangPegawaiAlias($b, 'p', $cabangKantorId);
        if ($status !== '') {
            $b->where('c.status_cuti', $status);
        }

        $rows = $b->orderBy('c.tanggal_cuti', 'DESC')
            ->orderBy('c.id_cuti', 'DESC')
            ->limit($perPage, $offset)
            ->get()
            ->getResultArray();
        $rows = array_map(
            fn ($r): array => is_array($r) ? $this->normalizeCutiRowArrayForApi($r) : [],
            $rows,
        );

        return [
            'status' => 1,
            'pesan'  => 'OK',
            'data'   => [
                'rows'         => $rows,
                'total'        => $total,
                'page'         => $page,
                'per_page'     => $perPage,
                'total_page'   => (int) ceil($total / $perPage),
                'status_filter'=> $status,
            ],
        ];
    }

    /**
     * @return array{status: int, pesan: string, data?: mixed}
     */
    public function cutiShow(int $id, ?int $cabangKantorId = null): array
    {
        $row = $this->db->table('cuti c')
            ->select('c.id_cuti, c.pegawai, DATE_FORMAT(c.tanggal_cuti, \'%Y-%m-%d\') AS tanggal_cuti, c.tipe_cuti, c.alasan_cuti, c.status_cuti, c.alasan_tolak, p.nama_lengkap, p.nip, p.email, p.kantor as pegawai_kantor', false)
            ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'left')
            ->where('c.id_cuti', $id)
            ->get()
            ->getRowArray();

        if ($row === null) {
            return ['status' => 0, 'pesan' => 'Data cuti tidak ditemukan'];
        }
        $row = $this->normalizeCutiRowArrayForApi($row);
        if (! $this->pegawaiRowInCabang(['kantor' => $row['pegawai_kantor'] ?? null], $cabangKantorId)) {
            return ['status' => 0, 'pesan' => 'Data cuti tidak ditemukan'];
        }
        unset($row['pegawai_kantor']);

        $dok = $this->db->table('cuti_dokumen')->select('dokumen')->where('cuti', $id)->get()->getResultArray();

        return [
            'status' => 1,
            'pesan'  => 'OK',
            'data'   => [
                'cuti'    => $row,
                'dokumen' => $dok,
            ],
        ];
    }

    /**
     * @return array{status: int, pesan: string}
     */
    public function cutiApprove(int $id, ?int $cabangKantorId = null): array
    {
        $exists = $this->db->table('cuti')->select('id_cuti')->where('id_cuti', $id)->get()->getRowArray();
        if ($exists === null) {
            return ['status' => 0, 'pesan' => 'Data cuti tidak ditemukan'];
        }
        if (! $this->cutiAllowedForCabang($id, $cabangKantorId)) {
            return ['status' => 0, 'pesan' => 'Data cuti tidak ditemukan'];
        }

        $this->db->table('cuti')->where('id_cuti', $id)->update([
            'status_cuti'  => 'Approve',
            'alasan_tolak' => '',
        ]);

        return ['status' => 1, 'pesan' => 'Cuti disetujui.'];
    }

    /**
     * @return array{status: int, pesan: string}
     */
    public function cutiReject(int $id, string $alasanTolak, ?int $cabangKantorId = null): array
    {
        $exists = $this->db->table('cuti')->select('id_cuti')->where('id_cuti', $id)->get()->getRowArray();
        if ($exists === null) {
            return ['status' => 0, 'pesan' => 'Data cuti tidak ditemukan'];
        }
        if (! $this->cutiAllowedForCabang($id, $cabangKantorId)) {
            return ['status' => 0, 'pesan' => 'Data cuti tidak ditemukan'];
        }

        $alasanTolak = trim($alasanTolak);
        if ($alasanTolak === '') {
            return ['status' => 0, 'pesan' => 'Alasan penolakan wajib diisi.'];
        }

        $this->db->table('cuti')->where('id_cuti', $id)->update([
            'status_cuti'  => 'Rejected',
            'alasan_tolak' => $alasanTolak,
        ]);

        return ['status' => 1, 'pesan' => 'Cuti ditolak.'];
    }

    /**
     * @return array<string, mixed>|null
     */
    private function fetchPegawaiDetail(int $id): ?array
    {
        $row = $this->db->table('pegawai p')
            ->select('p.*, j.nama_jabatan, u.nama_unit_kerja, g.nama_golongan, k.nama_kantor, jd.nama_jadwal')
            ->join('jabatan j', 'j.id_jabatan = p.jabatan', 'left')
            ->join('unit_kerja u', 'u.id_unit_kerja = p.unit_kerja', 'left')
            ->join('golongan g', 'g.id_golongan = p.golongan_pekerjaan', 'left')
            ->join('kantor k', 'k.id_kantor = p.kantor', 'left')
            ->join('jadwal jd', 'jd.id_jadwal = p.jadwal', 'left')
            ->where('p.id_pegawai', $id)
            ->get()
            ->getRowArray();

        if ($row !== null) {
            unset($row['password'], $row['token']);
        }

        return $row;
    }

    private function applyCabangPegawaiAlias(BaseBuilder $b, string $alias, ?int $cabangKantorId): void
    {
        if ($cabangKantorId !== null && $cabangKantorId > 0) {
            $b->where("{$alias}.kantor", $cabangKantorId);
        }
    }

    private function pegawaiIdAllowedForCabang(int $pegawaiId, ?int $cabangKantorId): bool
    {
        if ($cabangKantorId === null || $cabangKantorId <= 0) {
            return true;
        }

        return $this->db->table('pegawai')->where('id_pegawai', $pegawaiId)->where('kantor', $cabangKantorId)->countAllResults() > 0;
    }

    /**
     * @param array<string, mixed> $row baris pegawai (key `kantor` = id_kantor)
     */
    private function pegawaiRowInCabang(array $row, ?int $cabangKantorId): bool
    {
        if ($cabangKantorId === null || $cabangKantorId <= 0) {
            return true;
        }

        return (int) ($row['kantor'] ?? 0) === $cabangKantorId;
    }

    private function cutiAllowedForCabang(int $cutiId, ?int $cabangKantorId): bool
    {
        if ($cabangKantorId === null || $cabangKantorId <= 0) {
            return true;
        }
        $row = $this->db->table('cuti c')
            ->select('p.kantor')
            ->join('pegawai p', 'p.id_pegawai = c.pegawai', 'left')
            ->where('c.id_cuti', $cutiId)
            ->get()
            ->getRowArray();

        return $row !== null && (int) ($row['kantor'] ?? 0) === $cabangKantorId;
    }

    private function applyPegawaiSearchFilter(BaseBuilder $b, string $search): void
    {
        if ($search === '') {
            return;
        }

        $b->groupStart()
            ->like('p.nama_lengkap', $search)
            ->orLike('p.nip', $search)
            ->orLike('p.username', $search)
            ->groupEnd();
    }

    private function normalizeDate(mixed $v): ?string
    {
        if ($v === null || $v === '' || $v === '0000-00-00') {
            return null;
        }

        $s = (string) $v;

        return $s;
    }

    /**
     * Kunci baris cuti ke huruf kecil + rapikan bentuk tanggal_cuti setelah JSON (objek/array).
     *
     * @param array<string, mixed> $row
     *
     * @return array<string, mixed>
     */
    private function normalizeCutiRowArrayForApi(array $row): array
    {
        $row = array_change_key_case($row, CASE_LOWER);
        $tc  = $row['tanggal_cuti'] ?? null;
        if (is_array($tc)) {
            $row['tanggal_cuti'] = $tc['date'] ?? $tc['value'] ?? null;
        }

        return $row;
    }
}
