<?php

namespace App\Models;

/**
 * Port dari application/models/Global_data.php (CI3, storefront --
 * FILE BERBEDA dari Global_data.php milik admin/, walau nama class
 * sama), alias "func" di CI3. Bukan tabel tunggal -- kumpulan helper
 * data lintas modul, jadi bukan turunan Model biasa. Ditambah method
 * baru BERTAHAP setiap kali milestone berikutnya membutuhkan, bukan
 * sekaligus -- lihat versi asli di application/models/Global_data.php
 * kalau butuh method yang belum ada di sini.
 */
class GlobalDataModel
{
    protected $db;

    public function __construct()
    {
        $this->db = db_connect();
    }

    public function encode(string $string): string
    {
        return service('encrypter')->encrypt($string);
    }

    public function decode(string $string): ?string
    {
        try {
            return service('encrypter')->decrypt($string);
        } catch (\Throwable $e) {
            return null;
        }
    }

    /**
     * Lookup field/value generik dari tabel setting-style (field, value).
     * getSetting() dan globalset() di CI3 isinya identik persis -- dua
     * nama untuk fungsi yang sama, dipertahankan keduanya supaya view/
     * controller yang dipakai ulang tidak perlu diubah semua sekaligus.
     */
    protected function keyValueLookup(string $table, string $data)
    {
        $builder = $this->db->table($table);
        if ($data !== 'semua') {
            $builder->where('field', $data);
        }
        $res = $builder->get();

        if ($data === 'semua') {
            $result = [];
            foreach ($res->getResult() as $row) {
                $result[$row->field] = $row->value;
            }

            return (object) $result;
        }

        $result = '';
        foreach ($res->getResult() as $row) {
            $result = $row->value;
        }

        return $result;
    }

    public function getSetting(string $data)
    {
        return $this->keyValueLookup('setting', $data);
    }

    public function globalset(string $data)
    {
        return $this->keyValueLookup('setting', $data);
    }

    public function getNotifSetting(string $data)
    {
        return $this->keyValueLookup('notifsetting', $data);
    }

    protected function singleRowLookup(string $table, $id, string $what, string $opo = 'id')
    {
        $row = $this->db->table($table)->where($opo, $id)->get()->getRow();

        if ($what === 'semua') {
            return $row;
        }

        return $row !== null ? ($row->{$what} ?? null) : null;
    }

    public function getKurir($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('kurir', $id, $what, $opo);
    }

    public function getKategori($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('kategori', $id, $what, $opo);
    }

    public function getProduk($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('produk', $id, $what, $opo);
    }

    public function getVariasi($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('produkvariasi', $id, $what, $opo);
    }

    public function getVoucher($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('voucher', $id, $what, $opo);
    }

    public function getPaket($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('paket', $id, $what, $opo);
    }

    public function getTransaksiProduk($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('transaksiproduk', $id, $what, $opo);
    }

    /**
     * Jumlah pesan belum dibaca dari admin (tujuan=usrid, baca=0) -- port
     * dari Apih2h::chatnotif() (CI3, API mobile). Web baru (Manage::pesan())
     * terhubung ke tabel `pesan` yang sama dgn histori mobile existing.
     */
    public function getUnreadPesanCount(int $usrid): int
    {
        return $this->db->table('pesan')->where('tujuan', $usrid)->where('baca', 0)->countAllResults();
    }

    /** Jumlah item keranjang JASA (belum checkout) milik user yang sedang login. */
    public function getKeranjangJasa(): int
    {
        $usrid = session()->get('usrid');
        if ($usrid === null) {
            return 0;
        }

        return $this->db->table('transaksiproduk')
            ->where('jasa', 1)
            ->where('idtransaksi', 0)
            ->where('usrid', $usrid)
            ->countAllResults();
    }

    /** Cache tarif kurir RajaOngkir -- port dari Global_data::getHistoryOngkir() (CI3). */
    public function getHistoryOngkir(array $where, string $what)
    {
        $row = $this->db->table('historyongkir')
            ->where('dari', $where['dari'])
            ->where('tujuan', $where['tujuan'])
            ->where('kurir', $where['kurir'])
            ->where('service', $where['service'])
            ->orderBy('id', 'DESC')
            ->get(1)
            ->getRow();

        return $row !== null ? ($row->{$what} ?? null) : 0;
    }

    public function getProdukDigital($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('produkdigital', $id, $what, $opo);
    }

    public function getWarna($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('variasiwarna', $id, $what, $opo);
    }

    public function getSaldo($id, string $what, string $opo = 'id')
    {
        $result = $this->singleRowLookup('saldo', $id, $what, $opo);

        return $what === 'semua' ? $result : ($result ?? 0);
    }

    public function getBank($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('rekeningbank', $id, $what, $opo);
    }

    public function getKel($id, string $what, string $opo = 'id')
    {
        $result = $this->singleRowLookup('kel', $id, $what, $opo);

        return $result ?? 'kelurahan tidak ditemukan';
    }

    public function getKec($id, string $what, string $opo = 'id')
    {
        $result = $this->singleRowLookup('kec', $id, $what, $opo);

        return $result ?? 'kecamatan tidak ditemukan';
    }

    public function getKab($id, string $what, string $opo = 'id')
    {
        $result = $this->singleRowLookup('kab', $id, $what, $opo);

        return $result ?? 'kabupaten tidak ditemukan';
    }

    public function getProv($id, string $what, string $opo = 'id')
    {
        $result = $this->singleRowLookup('prov', $id, $what, $opo);

        return $result ?? 'provinsi tidak ditemukan';
    }

    /** List kab/kota dalam satu provinsi, untuk dropdown cascading alamat. */
    public function getKabByProv($idprov): array
    {
        return $this->db->table('kab')->where('idprov', $idprov)->orderBy('tipe', 'DESC')->get()->getResult();
    }

    /** List kecamatan dalam satu kab/kota, untuk dropdown cascading alamat. */
    public function getKecByKab($idkab): array
    {
        return $this->db->table('kec')->where('idkab', $idkab)->get()->getResult();
    }

    /** List kelurahan dalam satu kecamatan, untuk dropdown cascading alamat. */
    public function getKelByKec($idkec): array
    {
        return $this->db->table('kel')->where('idkec', $idkec)->get()->getResult();
    }

    public function getAlamat($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('alamat', $id, $what, $opo);
    }

    public function getRekening($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('rekening', $id, $what, $opo);
    }

    public function getSaldotarik($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('saldotarik', $id, $what, $opo);
    }

    public function getKonfirmasi($id, string $what, string $opo = 'idbayar')
    {
        return $this->singleRowLookup('konfirmasi', $id, $what, $opo);
    }

    /**
     * Konfigurasi SDK Midtrans -- port dari constructor Home.php/Assync.php
     * (CI3) yang menyalakan ulang \Midtrans\Config setiap request. Dipanggil
     * manual (bukan dari constructor) supaya cuma controller yang benar2
     * perlu Midtrans yang memuatnya.
     */
    public function configureMidtrans(): void
    {
        $set = $this->getSetting('semua');
        \Midtrans\Config::$serverKey    = $set->midtrans_server ?? '';
        \Midtrans\Config::$isProduction = stripos($set->midtrans_snap ?? '', 'sandbox') === false;
        \Midtrans\Config::$isSanitized  = true;
        \Midtrans\Config::$is3ds        = true;
    }

    public function getBayar($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('pembayaran', $id, $what, $opo);
    }

    public function getBayardigital($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('pembayarandigital', $id, $what, $opo);
    }

    public function getTransaksi($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('transaksi', $id, $what, $opo);
    }

    public function getTransaksidigital($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('transaksidigital', $id, $what, $opo);
    }

    public function getPengiriman($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('pengiriman', $id, $what, $opo);
    }

    public function getSize($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('variasisize', $id, $what, $opo);
    }

    /**
     * Nomor WA acak (round-robin sederhana: ambil yang paling lama
     * dipakai) untuk tombol "Hubungi Penjual via WA" -- port dari
     * Global_data::getRandomWasap() (CI3).
     */
    public function getRandomWasap()
    {
        $row = $this->db->table('wasap')->orderBy('tgl', 'ASC')->limit(1)->get()->getRow();
        if ($row === null) {
            return 0;
        }

        if (substr($row->wasap, 0, 1) === '0') {
            return '+62' . substr($row->wasap, 1);
        }
        if (substr($row->wasap, 0, 2) === '62') {
            return '+' . $row->wasap;
        }
        if (substr($row->wasap, 0, 1) === '+') {
            return $row->wasap;
        }

        return 0;
    }

    public function updateViewer(string $url): void
    {
        $row = $this->db->table('produk')->where('url', $url)->get()->getRow();
        if ($row === null) {
            return;
        }

        $this->db->table('produk')->where('id', $row->id)->update(['dilihat' => (int) $row->dilihat + 1]);
    }

    public function formUang($format): string
    {
        return number_format((float) $format, 0, ',', '.');
    }

    public function potong(string $str, int $max, string $after = ''): string
    {
        if (strlen($str) > $max) {
            $str = rtrim(substr($str, 0, $max)) . $after;
        }

        return $str;
    }

    public function ubahTgl(string $format, string $tanggal = 'now', string $bahasa = 'id'): string
    {
        $en = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
        $id = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', "Jum'at", 'Sabtu', 'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];

        $target = $bahasa === 'id' ? $id : $en;

        return str_replace($en, $target, date($format, strtotime($tanggal)));
    }

    public function getBintang($idproduk = 0): array
    {
        $rows = $this->db->table('review')->where('idproduk', $idproduk)->get()->getResult();
        $total = 0;
        foreach ($rows as $r) {
            $total += $r->nilai;
        }
        $jml = count($rows);
        $nilai = ($total > 0 || $jml > 0) ? round($total / max($jml, 1), 0, PHP_ROUND_HALF_DOWN) : 0;

        return ['star' => $nilai, 'jml' => $jml];
    }

    public function getLaku($idproduk = 0): array
    {
        $rows = $this->db->table('transaksiproduk')->where('idproduk', $idproduk)->get()->getResult();
        $total = 0;
        foreach ($rows as $r) {
            $total += $r->jumlah;
        }

        return ['terjual' => $total];
    }

    /**
     * Price-list PASCABAYAR dari Digiflazz, di-cache 5 menit (file cache,
     * sama seperti CI3 asli) supaya tidak memanggil API live di setiap
     * request -- ini cuma BACA daftar harga, bukan transaksi.
     */
    public function HargaPascabayar()
    {
        $cacheKey = 'digiflazz_pricelist_pasca';
        $cached   = cache($cacheKey);
        if ($cached !== null) {
            return json_decode($cached);
        }

        $set      = $this->getSetting('semua');
        $username = $set->mp_username ?? '';
        $apiKey   = $set->mp_apikey ?? '';
        if ($username === '' || $apiKey === '') {
            return null;
        }

        $data = json_encode([
            'cmd'      => 'pasca',
            'username' => $username,
            'sign'     => md5($username . $apiKey . 'pricelist'),
        ]);

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, 'https://api.digiflazz.com/v1/price-list');
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
        curl_setopt($ch, CURLOPT_TIMEOUT, 15);
        $result = curl_exec($ch);
        curl_close($ch);

        $hasil = json_decode((string) $result);

        if ($hasil !== null && isset($hasil->data) && is_array($hasil->data)) {
            cache()->save($cacheKey, $result, 300);
        }

        return $hasil;
    }

    /**
     * Rating rata-rata + jumlah ulasan sebuah produk.
     */
    public function getReviewProduk($id): array
    {
        $rows = $this->db->table('review')->where('idproduk', $id)->get()->getResult();

        $count = 0;
        foreach ($rows as $r) {
            $count += $r->nilai;
        }
        $nilai = $count > 0 ? round($count / count($rows), 1) : 0;

        return ['nilai' => $nilai, 'ulasan' => count($rows)];
    }

    public function createPagination(int $rows, int $page, int $perpage = 10, string $function = 'refreshTabel'): string
    {
        $tpages = (int) ceil($rows / $perpage);
        $adjacents = 2;
        $prevlabel = '&lsaquo;';
        $nextlabel = '&rsaquo;';
        $out = '<div class="flex-c flex-w w-full p-t-10 m-lr--7">';

        if ($page != 1) {
            $out .= "<a href=\"javascript:void(0)\" class='flex-c-m how-pagination1 trans-04 m-all-7' onclick=\"{$function}(1)\">&laquo;</a>\n";
            $out .= "<a href=\"javascript:void(0)\" class='flex-c-m how-pagination1 trans-04 m-all-7' onclick=\"{$function}(" . ($page - 1) . ")\">{$prevlabel}</a>\n";
        }

        $pmin = ($page > $adjacents) ? ($page - $adjacents) : 1;
        $pmax = ($page < ($tpages - $adjacents)) ? ($page + $adjacents) : $tpages;
        for ($i = $pmin; $i <= $pmax; $i++) {
            if ($i == $page) {
                $out .= "<a href=\"javascript:void(0)\" class='flex-c-m how-pagination1 trans-04 m-all-7 active-pagination1'>{$i}</a>\n";
            } else {
                $out .= "<a href=\"javascript:void(0)\" class='flex-c-m how-pagination1 trans-04 m-all-7' onclick=\"{$function}({$i})\">{$i}</a>\n";
            }
        }

        if ($page < $tpages) {
            $out .= "<a href=\"javascript:void(0)\" onclick=\"{$function}(" . ($page + 1) . ")\" class='flex-c-m how-pagination1 trans-04 m-all-7'>{$nextlabel}</a>\n";
        }
        if ($page < ($tpages - $adjacents)) {
            $out .= "<a href=\"javascript:void(0)\" onclick=\"{$function}({$tpages})\" class='flex-c-m how-pagination1 trans-04 m-all-7'>&raquo;</a>\n";
        }
        $out .= '</div>';

        return $out;
    }

    /**
     * URL foto utama produk, fallback ke favicon kalau belum ada upload --
     * sama seperti CI3 asli.
     */
    public function getFoto($id, string $kat = 'utama'): string
    {
        $builder = $this->db->table('upload')->where('idproduk', $id);
        if ($kat === 'utama') {
            $builder->where('jenis', 1);
        }
        $row = $builder->get()->getRow();

        return $row !== null ? base_url('admin/uploads/' . $row->nama) : base_url('assets/images/favicon.png');
    }

    /**
     * TRUE (usrid) kalau sesi customer masih valid & akun aktif, 0 kalau
     * tidak -- dipakai di head/menu untuk tahu sedang login atau tidak.
     * Efek samping (sama seperti CI3 asli): bump `userdata.tgl` setiap
     * dipanggil, dipakai sebagai indikator "terakhir aktif".
     */
    public function cekLogin(): int
    {
        $usrid = session()->get('usrid');
        $lvl   = session()->get('lvl');
        if ($usrid === null || $lvl === null) {
            return 0;
        }

        $this->db->table('userdata')->where('id', $usrid)->update(['tgl' => date('Y-m-d H:i:s')]);
        $usr = $this->getUser($usrid, 'semua');

        return ($usr !== null && (int) $usr->status === 1) ? (int) $usrid : 0;
    }

    /**
     * Jumlah item keranjang (belum checkout) milik user yang sedang
     * login -- ditampilkan sebagai badge di header.
     */
    public function getKeranjang(): int
    {
        $usrid = session()->get('usrid');
        if ($usrid === null) {
            return 0;
        }

        return $this->db->table('transaksiproduk')
            ->where('jasa', 0)
            ->where('idtransaksi', 0)
            ->where('usrid', $usrid)
            ->countAllResults();
    }

    /**
     * Jumlah item keranjang PPOB (belum checkout) milik user yang sedang
     * login -- port dari Global_data::getKeranjangppob() (CI3).
     */
    public function getKeranjangppob(): int
    {
        $usrid = session()->get('usrid');
        if ($usrid === null) {
            return 0;
        }

        return $this->db->table('transaksiprodukdigital')
            ->where('idtransaksi', 0)
            ->where('usrid', $usrid)
            ->countAllResults();
    }

    /**
     * Hitung ulang harga jual satu SKU PPOB di server (anti tamper harga
     * dari client) -- port dari logika harga yang diulang di
     * Ppob::tambahkeranjangppob()/Home::keranjangppob() (CI3). Sumber data
     * sama seperti getNominalPrabayar(): tabel margin (jalur Digiflazz)
     * atau h2h (jalur H2H, sekarang nonaktif tapi tetap diport untuk
     * paritas perilaku).
     */
    public function hargaJualSku(string $buyerSkuCode, int $h2h): ?float
    {
        $set   = $this->getSetting('semua');
        $usrid = session()->get('usrid');
        $level = (int) (session()->get('lvl') ?? 0);

        $markup       = 0;
        $level_anggota = $level > 1 ? $level : 0;

        if ($usrid !== null) {
            $user = $this->getUser($usrid, 'semua');
            if ($user !== null && (int) $user->upline > 0) {
                $upline = $this->getUser($user->upline, 'semua');
                if ($upline !== null && (int) $upline->level > 0) {
                    $markup = (float) $upline->markup;
                }
            }
        }

        $perLevel = (float) ($set->marginpulsa_per_level ?? 0);
        $table    = $h2h === 1 ? 'h2h' : 'margin';

        $r = $this->db->table($table)
            ->where('buyer_sku_code', $buyerSkuCode)
            ->get()
            ->getRow();

        if ($r === null) {
            return null;
        }

        return (float) $r->price + (float) $r->margin
            - round((float) $r->margin * $level_anggota * $perLevel / 100)
            + $markup;
    }

    public function getUser($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('userdata', $id, $what, $opo);
    }

    public function getProfil($id, string $what, string $opo = 'id')
    {
        return $this->singleRowLookup('profil', $id, $what, $opo);
    }

    /**
     * Daftar nominal produk prabayar (Pulsa/Data/dst.) untuk satu brand,
     * dikelompokkan per "type" (Reguler/Combo Data/dst., dipakai sebagai
     * tab nominal di halaman).
     *
     * BUKAN port literal dari Global_data::ppobdataajax view (CI3) --
     * versi asli memanggil API Digiflazz LANGSUNG setiap kali nomor HP
     * diketik (HargaPrabayar(), di-cache 5 menit) lalu menambahkan margin
     * dari tabel `margin`. Di sini harga DASAR-nya diambil dari tabel
     * `margin` itu sendiri (kolom `price` sudah disinkronkan rutin lewat
     * tombol "Update Semua" di admin) -- jadi sumber data sama persis,
     * cuma tanpa panggilan API tambahan yang tidak perlu di setiap
     * ketikan nomor HP. Formula harga akhir & markup upline TETAP identik
     * dengan versi asli.
     */
    public function getNominalPrabayar(int $idproduk, string $brand): array
    {
        $kategori = $this->getProdukDigital($idproduk, 'deskripsi');
        if ($kategori === null) {
            return [];
        }

        $set   = $this->getSetting('semua');
        $usrid = session()->get('usrid');
        $level = (int) (session()->get('lvl') ?? 0);

        $markup       = 0;
        $level_anggota = $level > 1 ? $level : 0;

        if ($usrid !== null) {
            $user = $this->getUser($usrid, 'semua');
            if ($user !== null && (int) $user->upline > 0) {
                $upline = $this->getUser($user->upline, 'semua');
                if ($upline !== null && (int) $upline->level > 0) {
                    $markup = (float) $upline->markup;
                }
            }
        }

        $perLevel = (float) ($set->marginpulsa_per_level ?? 0);

        $rows = $this->db->table('margin')
            ->where('category', $kategori)
            ->where('brand', $brand)
            ->where('idproduk', $idproduk)
            ->where('type !=', 'Cek Hutang')
            ->notLike('product_name', 'Cek Nama')
            ->notLike('product_name', 'Cek Hutang')
            ->orderBy('price', 'ASC')
            ->get()
            ->getResult();

        $grup = [];
        $typeLabel = ['Umum' => 'Reguler', 'Combo Data' => 'Pulsa + Data'];

        foreach ($rows as $r) {
            $hargaJual = (float) $r->price + (float) $r->margin
                - round((float) $r->margin * $level_anggota * $perLevel / 100)
                + $markup;

            $label = $typeLabel[$r->type] ?? $r->type;
            $grup[$label][] = (object) [
                'buyer_sku_code'       => $r->buyer_sku_code,
                'category'             => $r->category,
                'product_name'         => $r->product_name,
                'desc'                 => $r->desc,
                'buyer_product_status' => (int) $r->buyer_product_status,
                'harga'                => (int) round($hargaJual),
                'idproduk'             => $idproduk,
            ];
        }

        return $grup;
    }

    /**
     * Cek nama pelanggan PLN dari ID pelanggan/no meter -- port dari
     * Ppob::TokenPln() (CI3), jalur Digiflazz saja (cek_token=h2h tidak
     * diport, setting live saat ini memang "digiflazz"). BUKAN data
     * cache -- ini panggilan live ke Digiflazz (inquiry-pln) karena
     * sifatnya validasi nomor pelanggan real-time, bukan daftar harga.
     */
    public function inquiryPln(string $customerNo): ?object
    {
        $set      = $this->getSetting('semua');
        $username = $set->mp_username ?? '';
        $apiKey   = $set->mp_apikey ?? '';
        if ($username === '' || $apiKey === '') {
            return null;
        }

        $data = json_encode([
            'username'    => $username,
            'customer_no' => $customerNo,
            'sign'        => md5($username . $apiKey . $customerNo),
        ]);

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, 'https://api.digiflazz.com/v1/inquiry-pln');
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
        curl_setopt($ch, CURLOPT_TIMEOUT, 15);
        $result = curl_exec($ch);
        $err    = curl_error($ch);
        curl_close($ch);

        if ($err !== '') {
            return null;
        }

        return json_decode((string) $result);
    }

    public function hashPassword(string $plain): string
    {
        return password_hash($plain, PASSWORD_DEFAULT);
    }

    /**
     * Cocok dengan password hash baru (bcrypt) maupun password lama yang
     * masih reversible-encrypted (encode()/decode() CI3).
     */
    public function verifyPassword(string $input, ?string $stored): bool
    {
        if ($stored === null || $stored === '') {
            return false;
        }

        if (password_verify($input, $stored)) {
            return true;
        }

        $legacy = $this->decode($stored);

        return is_string($legacy) && $legacy !== '' && hash_equals($legacy, $input);
    }

    public function isLegacyPassword(?string $stored): bool
    {
        return is_string($stored) && $stored !== '' && $stored[0] !== '$';
    }

    public function randomPassword(): string
    {
        $alphabet     = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
        $alphaLength  = strlen($alphabet) - 1;
        $pass         = '';
        for ($i = 0; $i <= 16; $i++) {
            $pass .= $alphabet[random_int(0, $alphaLength)];
        }

        return $pass;
    }

    /**
     * Catat aktivitas user (login/logout web, dst.) ke tabel `log` --
     * tabel yang SAMA dibaca tab "Aktivitas User" di admin-v4, jadi
     * bentuk $params harus tetap persis: usrid, ip, type, ket, created.
     */
    public function addlog(array $params): void
    {
        $this->db->table('log')->insert($params);
    }

    /**
     * Antrekan email/WA -- BUKAN kirim langsung. Sama seperti CI3 asli:
     * insert ke tabel `notifikasi` (status=0 = belum terkirim), ada
     * proses lain (worker/cron) yang mengirimkannya. Beda dengan
     * sendEmailOK()/sendWAOK() di sisi admin yang kirim langsung lewat
     * curl -- storefront pakai pola antre supaya request user tidak
     * menunggu proses kirim email/WA selesai.
     */
    public function sendEmail(string $tujuan, string $judul, string $pesan, string $subyek, ?string $pengirim = null): bool
    {
        $this->db->table('notifikasi')->insert([
            'jenis'    => 1,
            'tujuan'   => $tujuan,
            'judul'    => $judul,
            'pesan'    => $pesan,
            'subyek'   => $subyek,
            'pengirim' => $pengirim,
            'tgl'      => date('Y-m-d H:i:s'),
            'status'   => 0,
        ]);

        return true;
    }

    public function sendWA(string $nomer, string $pesan): bool
    {
        $this->db->table('notifikasi')->insert([
            'jenis'  => 2,
            'tujuan' => $nomer,
            'pesan'  => $pesan,
            'tgl'    => date('Y-m-d H:i:s'),
            'status' => 0,
        ]);

        return true;
    }

    /**
     * Token terenkripsi untuk link verifikasi email/WA. BUKAN port literal
     * dari arrEnc() CI3 (yang pakai base64(serialize()) lalu unserialize()
     * data dari query string -- celah PHP Object Injection karena
     * unserialize() menerima input dari URL). Diganti json + encrypter
     * (AES + HMAC, sudah byte-compatible CI3 -- lihat decode()) supaya
     * tidak bisa dipalsukan/disisipi objek, perilaku ke pemanggil tetap
     * sama: array masuk, token string keluar, dan sebaliknya.
     */
    public function arrEnc(array $arr): string
    {
        return $this->encode(json_encode($arr));
    }

    public function arrDec(string $token): ?array
    {
        $json = $this->decode($token);
        if ($json === null) {
            return null;
        }

        $arr = json_decode($json, true);

        return is_array($arr) ? $arr : null;
    }

    public function verifEmail(int $id): bool
    {
        if ($id <= 0) {
            return false;
        }

        $profil  = $this->getProfil($id, 'semua', 'usrid');
        $user    = $this->getUser($id, 'semua');
        $nama    = $profil->nama ?? ($user->nama ?? '');
        $toko    = $this->getSetting('nama');
        $verifid = $this->arrEnc(['id' => $id, 'time' => date('YmdHis')]);
        $link    = site_url('home/signup?verify=' . $verifid);

        $pesan = '
            <html>
            <body>
                <div style="padding:20px;border-radius:3px;margin:auto;">
                    <h3>Halo, ' . esc($nama) . '</h3>
                    <p>Terima kasih sudah mendaftar di <b>' . esc((string) $toko) . '</b>, untuk mengaktifkan akun Anda, silahkan klik link berikut:</p>
                    <p><a href="' . $link . '">VERIFIKASI AKUN ' . esc(strtoupper((string) $toko)) . '</a></p>
                    <p>atau link dibawah ini<br>' . $link . '</p>
                </div>
            </body>
            </html>
        ';

        return $this->sendEmail((string) ($user->username ?? ''), 'Verifikasi Pendaftaran ' . $toko, $pesan, 'Verifikasi Pendaftaran ' . $toko);
    }

    public function verifWA(int $id): bool
    {
        if ($id <= 0) {
            return false;
        }

        $profil  = $this->getProfil($id, 'semua', 'usrid');
        $nama    = $profil->nama ?? '';
        $toko    = $this->getSetting('nama');
        $verifid = $this->arrEnc(['id' => $id, 'time' => date('YmdHis')]);
        $link    = site_url('home/signup?verify=' . $verifid);

        $pesan = "Halo, *{$nama}*\n\n" .
            "Terima kasih sudah mendaftar di *{$toko}*\n" .
            "*untuk mengaktifkan akun Anda, silahkan klik link berikut:*\n\n" .
            "{$link}\n\n" .
            " _*Apabila link tidak bisa di klik, simpan nomer whatsapp ini terlebih dahulu_";

        return $this->sendWA((string) ($profil->nohp ?? ''), $pesan);
    }

    /**
     * Reset password jadi acak & antre email+WA pemberitahuan -- port
     * dari Global_data::resetPass() (CI3).
     */
    public function resetPass(string $email): bool
    {
        $usrid = $this->getUser($email, 'id', 'username');
        if (! $usrid || (int) $usrid <= 0) {
            return false;
        }

        $user    = $this->getUser($usrid, 'semua');
        $profil  = $this->getProfil($usrid, 'semua', 'usrid');
        $nama    = $profil->nama ?? ($user->nama ?? '');
        $generated = $this->randomPassword();

        $this->db->table('userdata')->where('id', $usrid)->update(['password' => $this->hashPassword($generated)]);

        $toko  = $this->getSetting('nama');
        $pesan = '
            <html>
            <body>
                <div style="width:90%;padding:20px;border:1px solid #ccc;border-radius:3px;margin:auto;">
                    <h3>Halo, ' . esc($nama) . '</h3>
                    <p>Selamat, reset password Anda berhasil dan untuk login ke akun Anda, silahkan menggunakan password dibawah:</p>
                    <p>Pass: ' . esc($generated) . '</p>
                    <p>Segera masuk dan ganti password Anda untuk meningkatkan keamanan akun Anda kembali.</p>
                    <p><a href="' . site_url('home/signin') . '">LOGIN DISINI</a></p>
                </div>
            </body>
            </html>
        ';
        $pesanWA = "Halo, *{$nama}*\n\nSelamat, reset password Anda berhasil dan untuk login ke akun Anda, silahkan menggunakan password dibawah:\nPass: {$generated}\n\nSegera masuk dan ganti password Anda untuk meningkatkan keamanan akun Anda kembali.";

        $this->sendEmail($user->username, (string) $toko, $pesan, 'Reset password');
        $this->sendWA($profil->nohp ?? '', $pesanWA);

        return true;
    }
}
