<?php

namespace App\Services\Mobile;

use CodeIgniter\Database\BaseConnection;
use Config\Database;
use Exception;

/**
 * Port logika bisnis dari CI3 `application/controllers/Json.php`.
 * Respons struktur disamakan dengan legacy untuk kompatibilitas aplikasi mobile.
 *
 * TODO (fase berikutnya): pecah per agregat (PresensiService, CutiService), tambahkan transaksi,
 * perbaiki keamanan (MD5 → password_hash, token → JWT) setelah cutover klien.
 */
class MobileJsonService
{
    protected BaseConnection $db;

    /** @var array<int, string> */
    private array $hariIndo = [
        1 => 'Senin',
        2 => 'Selasa',
        3 => 'Rabu',
        4 => 'Kamis',
        5 => 'Jumat',
        6 => 'Sabtu',
        7 => 'Minggu',
    ];

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

    public function loginWToken(string $token): array
    {
        $data = [
            'status' => 0,
            'pesan'  => '',
        ];

        $pegawai = $this->db->table('pegawai')->where('token', $token)->get()->getRow();
        if ($pegawai) {
            unset($pegawai->username, $pegawai->password, $pegawai->token);
            $this->db->table('pegawai')->where('id_pegawai', $pegawai->id_pegawai)->update([
                'last_login' => date('Y-m-d H:i:s'),
            ]);
            $data['status'] = 1;
        }

        return $data;
    }

    public function login(string $user, string $pass): array
    {
        $data = [
            'status' => 0,
            'pesan'  => 'Username atau Password tidak sesuai',
        ];

        $pegawai = $this->db->table('pegawai')
            ->where('username', $user)
            ->where('password', md5($pass))
            ->get()
            ->getRow();

        if ($pegawai) {
            unset($pegawai->username, $pegawai->password, $pegawai->token);
            $token = md5((string) $pegawai->id_pegawai) . $this->generateRandomString(15);
            $this->db->table('pegawai')->where('id_pegawai', $pegawai->id_pegawai)->update([
                'token'      => $token,
                'last_login' => date('Y-m-d H:i:s'),
            ]);
            $data['status'] = 1;
            $data['pesan']  = 'Selamat datang';
            $data['token']  = $token;
        }

        return $data;
    }

    /**
     * Terbitkan token API mobile untuk pegawai (id_pegawai), seperti setelah login sukses.
     * Dipakai saat login admin lewat tabel admin_users (proxy token ke pegawai tertentu).
     */
    public function issueTokenForPegawaiId(int $id): ?string
    {
        $pegawai = $this->db->table('pegawai')->where('id_pegawai', $id)->get()->getRow();
        if ($pegawai === null) {
            return null;
        }

        $token = md5((string) $pegawai->id_pegawai) . $this->generateRandomString(15);
        $this->db->table('pegawai')->where('id_pegawai', $id)->update([
            'token'      => $token,
            'last_login' => date('Y-m-d H:i:s'),
        ]);

        return $token;
    }

    public function profil(string $token): array
    {
        $data = [
            'status' => 0,
            'pesan'  => '',
        ];

        $pegawai = $this->db->table('pegawai')
            ->select('*, COALESCE(tanggal_lahir, \'0000-00-00\') as tanggal_lahir', false)
            ->where('token', $token)
            ->limit(1)
            ->get()
            ->getRow();

        if (! $pegawai) {
            return $data;
        }

        unset($pegawai->username, $pegawai->password, $pegawai->token);

        $pegawai->kantor     = $this->db->table('kantor')->where('id_kantor', $pegawai->kantor)->limit(1)->get()->getRow();
        $pegawai->jabatan    = $this->db->table('jabatan')->where('id_jabatan', $pegawai->jabatan)->limit(1)->get()->getRow();
        $pegawai->unit_kerja = $this->db->table('unit_kerja')->where('id_unit_kerja', $pegawai->unit_kerja)->limit(1)->get()->getRow();
        $pegawai->lembur     = $this->db->table('lembur')->where('pegawai', $pegawai->id_pegawai)->where('tanggal_lembur', date('Y-m-d'))->limit(1)->get()->getRow();

        $dilapangan          = $this->db->table('dilapangan')->where('pegawai', $pegawai->id_pegawai)->limit(1)->get()->getRow();
        $pegawai->dilapangan = $dilapangan ? true : false;

        $hari    = (int) date('N');
        $pre_in  = $hari . '_in';
        $pre_out = $hari . '_out';

        $jadwal_ar = [
            'hari'             => $this->hariIndo[$hari],
            'masuk'            => '',
            'pulang'           => '',
            'istirahat'        => '',
            'toleransi_masuk'  => '0',
            'toleransi_pulang' => '0',
            'libur'            => true,
        ];

        $libur_perusahaan = $this->db->table('libur')->where('tanggal_libur', date('Y-m-d'))->limit(1)->get()->getRow();
        if ($libur_perusahaan) {
            $jadwal_ar['ket_libur'] = 'Libur: ' . $libur_perusahaan->keterangan_libur;
        } else {
            $cuti = $this->db->table('cuti')
                ->where('pegawai', $pegawai->id_pegawai)
                ->where('tanggal_cuti', date('Y-m-d'))
                ->where('status_cuti', 'Approve')
                ->limit(1)
                ->get()
                ->getRow();
            if ($cuti) {
                $jadwal_ar['ket_libur'] = 'Cuti: ' . $cuti->alasan_cuti;
            } else {
                $jadwal = $this->db->table('jadwal')->where('id_jadwal', $pegawai->jadwal)->limit(1)->get()->getRow();
                if ($jadwal) {
                    $jadwal_ar['masuk']            = $jadwal->{$pre_in};
                    $jadwal_ar['pulang']           = $jadwal->{$pre_out};
                    $jadwal_ar['istirahat']        = '12:00';
                    $jadwal_ar['toleransi_masuk']  = $jadwal->toleransi_terlambat;
                    $jadwal_ar['toleransi_pulang'] = $jadwal->toleransi_pulang_cepat;
                    $jadwal_ar['libur']            = false;
                    $jadwal_ar['ket_libur']        = '';
                } else {
                    $jadwal_ar['ket_libur'] = 'Tidak ada jadwal. Hubungi Petugas';
                }
            }
        }

        $pegawai->jadwal = $jadwal_ar;
        $data['status']  = 1;
        $data['pegawai'] = $pegawai;

        return $data;
    }

    public function saveCuti(string $token, string $nama_photo, string $img, string $tanggal, string $alasan, string $tipe): array
    {
        $data = [
            'status' => 0,
            'pesan'  => '',
        ];

        $pegawai = $this->db->table('pegawai')->where('token', $token)->limit(1)->get()->getRow();
        if (! $pegawai) {
            return $data;
        }

        $dir = $this->ensureUploadDir('dokcuti');
        $image      = base64_decode($img);
        $image_name = uniqid((string) mt_rand(), true);
        $filename   = $image_name . '-' . $nama_photo;

        if ($image === false || $image === '' || file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $image) === false) {
            $data['pesan'] = 'Photo Dokumen GAGAL upload';

            return $data;
        }

        $this->db->table('cuti')->insert([
            'pegawai'      => $pegawai->id_pegawai,
            'tanggal_cuti' => $tanggal,
            'tipe_cuti'    => $tipe,
            'alasan_cuti'  => $alasan,
            'status_cuti'  => 'Waiting',
            'alasan_tolak' => '',
        ]);

        $id = (int) $this->db->insertID();
        $this->db->table('cuti_dokumen')->insert([
            'cuti'    => $id,
            'dokumen' => $filename,
        ]);

        $data['status'] = 1;
        $data['pesan']  = 'Photo Dokumen berhasil di upload';

        return $data;
    }

    /**
     * @param int|string|null $id
     */
    public function batalkanCuti(string $token, $id): array
    {
        $data = [
            'status' => 0,
            'pesan'  => '',
        ];

        $pegawai = $this->db->table('pegawai')->where('token', $token)->limit(1)->get()->getRow();
        if (! $pegawai) {
            return $data;
        }

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

        $data['status'] = 1;
        $data['pesan']  = "Ajuan di batalkan {$id} - {$pegawai->id_pegawai}";

        return $data;
    }

    public function saveAktifitas(string $token, string $nama_photo, string $img, string $tanggal, string $deksripsi): array
    {
        $data = [
            'status' => 0,
            'pesan'  => '',
        ];

        $pegawai = $this->db->table('pegawai')->where('token', $token)->limit(1)->get()->getRow();
        if (! $pegawai) {
            return $data;
        }

        $dir = $this->ensureUploadDir('aktifitas');
        $image      = base64_decode($img);
        $image_name = uniqid((string) mt_rand(), true);
        $filename   = $image_name . '-' . $nama_photo;

        if ($image === false || $image === '' || file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $image) === false) {
            $data['pesan'] = 'Photo Kegiatan GAGAL upload';

            return $data;
        }

        $this->db->table('aktifitas_harian')->insert([
            'pegawai'         => $pegawai->id_pegawai,
            'image'           => $filename,
            'deskripsi'       => $deksripsi,
            'waktu_aktifitas' => $tanggal,
        ]);

        $data['status'] = 1;
        $data['pesan']  = 'Photo Kegiatan berhasil di upload';

        return $data;
    }

    public function saveMasuk(string $token, string $nama_photo, string $img, string $lat, string $lng, string $jarak): array
    {
        $data = [
            'status' => 0,
            'pesan'  => 'Tidak ada jadwal kerja',
        ];

        $pegawai = $this->db->table('pegawai')->where('token', $token)->limit(1)->get()->getRow();
        if (! $pegawai) {
            return $data;
        }

        $jadwal = $this->db->table('jadwal')->where('id_jadwal', $pegawai->jadwal)->limit(1)->get()->getRow();
        if (! $jadwal) {
            return $data;
        }

        $dir = $this->ensureUploadDir('absen' . DIRECTORY_SEPARATOR . 'masuk');
        $image      = base64_decode($img);
        $image_name = uniqid((string) mt_rand(), true);
        $filename   = $image_name . '-' . $nama_photo;

        if ($image === false || $image === '' || file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $image) === false) {
            $data['pesan'] = 'Photo Kehadiran  GAGAL upload';

            return $data;
        }

        $hari   = (int) date('N');
        $pre_in = $hari . '_in';

        $masuk_jadwal  = date('Y-m-d') . ' ' . $jadwal->{$pre_in};
        $jam_masuk     = strtotime($masuk_jadwal);
        $jam_toleransi = strtotime($masuk_jadwal) + ((int) $jadwal->toleransi_terlambat * 60);
        $jam_sekarang  = time();

        if ($jam_sekarang < $jam_masuk) {
            $ket_masuk = 'Sesuai jadwal';
        } elseif ($jam_sekarang >= $jam_masuk && $jam_sekarang <= $jam_toleransi) {
            $ket_masuk = 'Terlambat';
        } else {
            $ket_masuk = 'Sangat terlambat';
        }

        $this->db->table('presensi')->where('tanggal', date('Y-m-d'))->where('pegawai', $pegawai->id_pegawai)->update([
            'jam_masuk'   => date('Y-m-d H:i:s'),
            'ket_masuk'   => $ket_masuk,
            'photo_masuk' => $filename,
            'lat_masuk'   => $lat,
            'lng_masuk'   => $lng,
            'jarak_masuk' => $jarak,
        ]);

        $data['status'] = 1;
        $data['pesan']  = 'Photo Kehadiran berhasil di upload';

        return $data;
    }

    public function savePulang(string $token, string $nama_photo, string $img, string $lat, string $lng, string $jarak): array
    {
        $data = [
            'status' => 0,
            'pesan'  => 'Tidak ada jadwal kerja',
        ];

        $pegawai = $this->db->table('pegawai')->where('token', $token)->limit(1)->get()->getRow();
        if (! $pegawai) {
            return $data;
        }

        $jadwal = $this->db->table('jadwal')->where('id_jadwal', $pegawai->jadwal)->limit(1)->get()->getRow();
        if (! $jadwal) {
            return $data;
        }

        $dir = $this->ensureUploadDir('absen' . DIRECTORY_SEPARATOR . 'pulang');
        $image      = base64_decode($img);
        $image_name = uniqid((string) mt_rand(), true);
        $filename   = $image_name . '-' . $nama_photo;

        if ($image === false || $image === '' || file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $image) === false) {
            $data['pesan'] = 'Photo Kehadiran  GAGAL upload';

            return $data;
        }

        $wibNow = new \DateTimeImmutable('now', new \DateTimeZone('Asia/Jakarta'));
        $hari    = (int) $wibNow->format('N');
        $pre_out = $hari . '_out';

        $jadwal_pulang = $wibNow->format('Y-m-d') . ' ' . $jadwal->{$pre_out};
        $jam_pulang    = strtotime($jadwal_pulang);
        $jam_toleransi = strtotime($jadwal_pulang) - ((int) $jadwal->toleransi_terlambat * 60);
        $jam_sekarang  = $wibNow->getTimestamp();

        if ($jam_sekarang < $jam_toleransi) {
            $data['pesan'] = 'Belum waktunya Pulang, waktu pulang anda adalah pukul ' . $jadwal->{$pre_out} . '. Jika anda memerlukan untuk pulang cepat, silahkan hubungi Operator';

            return $data;
        }

        if ($jam_sekarang >= $jam_toleransi && $jam_sekarang <= $jam_pulang) {
            $ket_pulang = 'Pulang Cepat';
        } else {
            $ket_pulang = 'Sesuai jadwal';
        }

        $this->db->table('presensi')->where('tanggal', $wibNow->format('Y-m-d'))->where('pegawai', $pegawai->id_pegawai)->update([
            'jam_pulang'   => $wibNow->format('Y-m-d H:i:s'),
            'ket_pulang'   => $ket_pulang,
            'photo_pulang' => $filename,
            'lat_pulang'   => $lat,
            'lng_pulang'   => $lng,
            'jarak_pulang' => $jarak,
        ]);

        $data['status'] = 1;
        $data['pesan']  = 'Photo Kehadiran berhasil di upload';

        return $data;
    }

    public function saveIstirahat(string $token, string $mulai, string $selesai): array
    {
        $data = [
            'status' => 0,
            'pesan'  => 'Tidak ada jadwal kerja',
        ];

        $pegawai = $this->db->table('pegawai')->where('token', $token)->limit(1)->get()->getRow();
        if (! $pegawai) {
            return $data;
        }

        $jadwal = $this->db->table('jadwal')->where('id_jadwal', $pegawai->jadwal)->limit(1)->get()->getRow();
        if (! $jadwal) {
            return $data;
        }

        $ist = null;
        if ($mulai !== '') {
            $ist = [
                'mulai_istirahat' => $mulai,
                'beres_istirahat' => $mulai,
            ];
        }

        if ($selesai !== '') {
            $ist = [
                'beres_istirahat' => $selesai,
                'is_istirahat'    => 1,
            ];
        }

        if ($ist === null) {
            return $data;
        }

        $this->db->table('presensi')->where('tanggal', date('Y-m-d'))->where('pegawai', $pegawai->id_pegawai)->update($ist);

        $data['status'] = 1;
        $data['pesan']  = 'berhasil disimpan';

        return $data;
    }

    public function presensiToday(string $token): array
    {
        $data = [
            'status' => 0,
            'pesan'  => '',
        ];

        $pegawai = $this->db->table('pegawai')->select('id_pegawai, jadwal')->where('token', $token)->limit(1)->get()->getRow();
        if (! $pegawai) {
            return $data;
        }

        $presensi = $this->db->table('presensi')->where('tanggal', date('Y-m-d'))->where('pegawai', $pegawai->id_pegawai)->limit(1)->get()->getRow();

        if (! $presensi) {
            $jadwal = $this->db->table('jadwal')->where('id_jadwal', $pegawai->jadwal)->get()->getRow();
            $ins    = [
                'pegawai'         => $pegawai->id_pegawai,
                'tanggal'         => date('Y-m-d'),
                'jadwal'          => serialize($jadwal),
                'jam_masuk'       => null,
                'ket_masuk'       => '',
                'photo_masuk'     => '',
                'jam_pulang'      => null,
                'ket_pulang'      => '',
                'photo_pulang'    => '',
                'mulai_istirahat' => null,
                'beres_istirahat' => null,
                'is_istirahat'    => '0',
                'lat_masuk'       => '0',
                'lng_masuk'       => '0',
                'lat_pulang'      => '0',
                'lng_pulang'      => '0',
                'jarak_masuk'     => '0',
                'jarak_pulang'    => '0',
            ];
            $this->db->table('presensi')->insert($ins);
            $id = (int) $this->db->insertID();

            $ins['id_presensi'] = $id . '.';

            $presensi = (object) $ins;
        }

        $data['status'] = 1;
        $data['data']   = $presensi;

        return $data;
    }

    public function presensi(string $token): array
    {
        $data = [
            'status' => 0,
            'pesan'  => '',
        ];

        $pegawai = $this->db->table('pegawai')->select('id_pegawai, jadwal')->where('token', $token)->limit(1)->get()->getRow();
        if (! $pegawai) {
            return $data;
        }

        $presensi = $this->db->table('presensi')->where('pegawai', $pegawai->id_pegawai)->orderBy('tanggal', 'DESC')->limit(20)->get()->getResult();
        if ($presensi) {
            $data['status'] = 1;
            $data['data']   = $presensi;
        }

        return $data;
    }

    public function daftarToday(string $token): array
    {
        $data = [
            'status' => 0,
            'pesan'  => '',
        ];

        $pegawai = $this->db->table('pegawai')->select('id_pegawai, jadwal')->where('token', $token)->limit(1)->get()->getRow();
        if (! $pegawai) {
            return $data;
        }

        $today = date('Y-m-d');
        $presensi = $this->db->table('pegawai')
            ->join('jabatan', 'id_jabatan=jabatan', 'left')
            ->join('presensi', 'id_pegawai=pegawai AND tanggal = ' . $this->db->escape($today), 'left', false)
            ->where('id_pegawai !=', $pegawai->id_pegawai)
            ->where('id_presensi IS NOT NULL', null, false)
            ->orderBy('jam_masuk', 'DESC')
            ->orderBy('nama_lengkap')
            ->get()
            ->getResult();

        if ($presensi) {
            $data['status'] = 1;
            $data['data']   = $presensi;
        }

        return $data;
    }

    public function berita(string $token, $dari, $jumlah): array
    {
        if ($dari === '' || $dari === null) {
            $dari = 0;
        }
        if ($jumlah === '' || $jumlah === null) {
            $jumlah = 10;
        }
        $dari   = (int) $dari;
        $jumlah = (int) $jumlah;

        $data = [
            'status' => 0,
            'pesan'  => '',
        ];

        $pegawai = $this->db->table('pegawai')->select('id_pegawai')->where('token', $token)->limit(1)->get()->getRow();
        if (! $pegawai) {
            return $data;
        }

        $berita = $this->db->table('berita')->orderBy('tanggal', 'DESC')->limit($jumlah, $dari)->get()->getResult();

        $data['status'] = 1;
        $data['data']   = $berita;

        return $data;
    }

    public function cuti(string $token, $dari, $jumlah): array
    {
        if ($dari === '' || $dari === null) {
            $dari = 0;
        }
        if ($jumlah === '' || $jumlah === null) {
            $jumlah = 25;
        }
        $dari   = (int) $dari;
        $jumlah = (int) $jumlah;

        $data = [
            'status' => 0,
            'pesan'  => '',
        ];

        $pegawai = $this->db->table('pegawai')->select('id_pegawai')->where('token', $token)->limit(1)->get()->getRow();
        if (! $pegawai) {
            return $data;
        }

        $cuti = $this->db->table('cuti')->where('pegawai', $pegawai->id_pegawai)->orderBy('tanggal_cuti', 'DESC')->limit($jumlah, $dari)->get()->getResult();

        if ($cuti) {
            foreach ($cuti as $v) {
                $v->dokumen = $this->db->table('cuti_dokumen')->select('dokumen')->where('cuti', $v->id_cuti)->get()->getResult();
            }
        }

        $data['status'] = 1;
        $data['data']   = $cuti;

        return $data;
    }

    public function lembur(string $token, $dari, $jumlah): array
    {
        if ($dari === '' || $dari === null) {
            $dari = 0;
        }
        if ($jumlah === '' || $jumlah === null) {
            $jumlah = 25;
        }
        $dari   = (int) $dari;
        $jumlah = (int) $jumlah;

        $data = [
            'status' => 0,
            'pesan'  => '',
        ];

        $pegawai = $this->db->table('pegawai')->select('id_pegawai')->where('token', $token)->limit(1)->get()->getRow();
        if (! $pegawai) {
            return $data;
        }

        $rows = $this->db->table('lembur')->where('pegawai', $pegawai->id_pegawai)->orderBy('tanggal_lembur', 'DESC')->limit($jumlah, $dari)->get()->getResult();

        $data['status'] = 1;
        $data['data']   = $rows;

        return $data;
    }

    public function libur(string $token): array
    {
        $data = [
            'status' => 0,
            'pesan'  => '',
        ];

        $pegawai = $this->db->table('pegawai')->select('id_pegawai')->where('token', $token)->limit(1)->get()->getRow();
        if (! $pegawai) {
            return $data;
        }

        $rows = $this->db->table('libur')->orderBy('tanggal_libur', 'DESC')->get()->getResult();

        $data['status'] = 1;
        $data['data']   = $rows;

        return $data;
    }

    public function aktifitas(string $token, $dari, $jumlah): array
    {
        if ($dari === '' || $dari === null) {
            $dari = 0;
        }
        if ($jumlah === '' || $jumlah === null) {
            $jumlah = 25;
        }
        $dari   = (int) $dari;
        $jumlah = (int) $jumlah;

        $data = [
            'status' => 0,
            'pesan'  => '',
        ];

        $pegawai = $this->db->table('pegawai')->select('id_pegawai')->where('token', $token)->limit(1)->get()->getRow();
        if (! $pegawai) {
            return $data;
        }

        $rows = $this->db->table('aktifitas_harian')->where('pegawai', $pegawai->id_pegawai)->orderBy('waktu_aktifitas', 'DESC')->limit($jumlah, $dari)->get()->getResult();

        $data['status'] = 1;
        $data['data']   = $rows;

        return $data;
    }

    public function savePp(string $token, string $nama_photo, string $img): array
    {
        $data = [
            'status' => 0,
            'pesan'  => 'Tidak ada jadwal kerja',
        ];

        $pegawai = $this->db->table('pegawai')->where('token', $token)->limit(1)->get()->getRow();
        if (! $pegawai) {
            return $data;
        }

        $dir = $this->ensureUploadDir('pengguna');
        $image      = base64_decode($img);
        $image_name = uniqid((string) mt_rand(), true);
        $filename   = $image_name . '-' . $nama_photo;

        if ($image === false || $image === '' || file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $image) === false) {
            $data['pesan'] = 'Photo Kehadiran  GAGAL upload';

            return $data;
        }

        if ($pegawai->photo !== '' && $pegawai->photo !== null) {
            try {
                $photoPath = $dir . DIRECTORY_SEPARATOR . $pegawai->photo;
                if (is_file($photoPath)) {
                    unlink($photoPath);
                }
            } catch (Exception $e) {
            }
        }

        $this->db->table('pegawai')->where('id_pegawai', $pegawai->id_pegawai)->update([
            'photo' => $filename,
        ]);

        $data['status'] = 1;
        $data['pesan']  = 'Photo Profil berhasil di upload';

        return $data;
    }

    public function savePassword(string $token, string $passlama, string $passbaru): array
    {
        $data = [
            'status' => 0,
            'pesan'  => '-',
        ];

        $pegawai = $this->db->table('pegawai')->where('token', $token)->limit(1)->get()->getRow();
        if (! $pegawai) {
            return $data;
        }

        if (md5($passlama) == $pegawai->password) {
            $this->db->table('pegawai')->where('id_pegawai', $pegawai->id_pegawai)->update([
                'password' => md5($passbaru),
            ]);
            $data['status'] = 1;
            $data['pesan']  = 'Password berhasil di ubah';
        } else {
            $data['pesan'] = 'Password lama tidak sesuai, silahkan coba lagi';
        }

        return $data;
    }

    private function generateRandomString(int $length = 10): string
    {
        $characters   = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
        $randomString = '';
        for ($i = 0; $i < $length; $i++) {
            // Sengaja memakai rand() seperti CI3 agar pola token kompatibel.
            $randomString .= $characters[rand(0, strlen($characters) - 1)];
        }

        return $randomString;
    }

    private function ensureUploadDir(string $relativeUnderUploads): string
    {
        $base = FCPATH . 'assets' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $relativeUnderUploads);
        if (! is_dir($base)) {
            mkdir($base, 0755, true);
        }

        return $base;
    }
}
