<?php

/*
 *
 * File ini bagian dari:
 *
 * OpenSID
 *
 * Sistem informasi desa sumber terbuka untuk memajukan desa
 *
 * Aplikasi dan source code ini dirilis berdasarkan lisensi GPL V3
 *
 * Hak Cipta 2009 - 2015 Combine Resource Institution (http://lumbungkomunitas.net/)
 * Hak Cipta 2016 - 2025 Perkumpulan Desa Digital Terbuka (https://opendesa.id)
 *
 * Dengan ini diberikan izin, secara gratis, kepada siapa pun yang mendapatkan salinan
 * dari perangkat lunak ini dan file dokumentasi terkait ("Aplikasi Ini"), untuk diperlakukan
 * tanpa batasan, termasuk hak untuk menggunakan, menyalin, mengubah dan/atau mendistribusikan,
 * asal tunduk pada syarat berikut:
 *
 * Pemberitahuan hak cipta di atas dan pemberitahuan izin ini harus disertakan dalam
 * setiap salinan atau bagian penting Aplikasi Ini. Barang siapa yang menghapus atau menghilangkan
 * pemberitahuan ini melanggar ketentuan lisensi Aplikasi Ini.
 *
 * PERANGKAT LUNAK INI DISEDIAKAN "SEBAGAIMANA ADANYA", TANPA JAMINAN APA PUN, BAIK TERSURAT MAUPUN
 * TERSIRAT. PENULIS ATAU PEMEGANG HAK CIPTA SAMA SEKALI TIDAK BERTANGGUNG JAWAB ATAS KLAIM, KERUSAKAN ATAU
 * KEWAJIBAN APAPUN ATAS PENGGUNAAN ATAU LAINNYA TERKAIT APLIKASI INI.
 *
 * @package   OpenSID
 * @author    Tim Pengembang OpenDesa
 * @copyright Hak Cipta 2009 - 2015 Combine Resource Institution (http://lumbungkomunitas.net/)
 * @copyright Hak Cipta 2016 - 2025 Perkumpulan Desa Digital Terbuka (https://opendesa.id)
 * @license   http://www.gnu.org/licenses/gpl.html GPL V3
 * @link      https://github.com/OpenSID/OpenSID
 *
 */

namespace App\Libraries;

use App\Enums\AgamaEnum;
use App\Enums\AsuransiEnum;
use App\Enums\CacatEnum;
use App\Enums\CaraKBEnum;
use App\Enums\GolonganDarahEnum;
use App\Enums\HamilEnum;
use App\Enums\PekerjaanEnum;
use App\Enums\PendidikanKKEnum;
use App\Enums\PendidikanSedangEnum;
use App\Enums\SasaranEnum;
use App\Enums\SHDKEnum;
use App\Enums\StatusDasarEnum;
use App\Enums\StatusKawinEnum;
use App\Enums\StatusKTPEnum;
use App\Enums\StatusRekamEnum;
use App\Enums\WargaNegaraEnum;
use App\Libraries\BIP\Bip;
use App\Models\BantuanPeserta;
use App\Models\Keluarga;
use App\Models\LogKeluarga;
use App\Models\LogPenduduk;
use App\Models\Penduduk;
use App\Models\PendudukSaja;
use App\Models\Wilayah;
use Carbon\Carbon;
use DateInterval;
use DateTimeImmutable;
use DateTimeInterface;
use Exception;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use OpenSpout\Reader\XLSX\Reader;

class Import
{
    public const DAFTAR_KOLOM = [
        'alamat',
        'dusun',
        'rw',
        'rt',
        'nama',
        'no_kk',
        'nik',
        'sex',
        'tempatlahir',
        'tanggallahir',
        'agama_id',
        'pendidikan_kk_id',
        'pendidikan_sedang_id',
        'pekerjaan_id',
        'status_kawin',
        'kk_level',
        'warganegara_id',
        'ayah_nik',
        'nama_ayah',
        'ibu_nik',
        'nama_ibu',
        'golongan_darah_id',
        'akta_lahir',
        'dokumen_pasport',
        'tanggal_akhir_paspor',
        'dokumen_kitas',
        'akta_perkawinan',
        'tanggalperkawinan',
        'akta_perceraian',
        'tanggalperceraian',
        'cacat_id',
        'cara_kb_id',
        'hamil',
        'ktp_el',
        'status_rekam',
        'alamat_sekarang',
        'status_dasar',
        'suku',
        'tag_id_card',
        'id_asuransi',
        'no_asuransi',
        'lat',
        'lng',
        'ket',
    ];

    protected array $kodeSex;
    protected array $kodeHubungan;
    protected array $kodeAgama;
    protected array $kodePendidikanKK;
    protected array $kodePendidikanSedang;
    protected array $kodePekerjaan;
    protected array $kodeStatus;
    protected array $kodeGolonganDarah;
    protected array $kodeKtpEl;
    protected $kodeStatusRekam;
    protected array $kodeStatusDasar;
    protected array $kodeCacat;
    protected array $kodeCaraKb;
    protected array $kodeWargaNegara;
    protected array $kodeHamil;
    protected $kodeAsuransi;
    protected $errorTulisPenduduk;
    protected $infoTulisPenduduk;

    public function __construct()
    {
        // Sediakan memory paling sedikit 512M
        preg_match('/^(\d+)(M)$/', ini_get('memory_limit'), $matches);
        $memoryLimit = $matches[1] ?: 0;
        if ($memoryLimit < 512) {
            ini_set('memory_limit', '512M');
        }
        set_time_limit(3600);

        // Data referensi tambahan

        $pendidikan = [
            'Tidak/Blm Sekolah'                => 1,
            'Tidak Tamat SD/Sederajat'         => 2,
            'Akademi/Diploma III/Sarjana Muda' => 7,
            'Strata-II'                        => 9,
        ];

        $status = [
            'BK' => 1,
            'K'  => 2,
            'CH' => 3,
            'CM' => 4,
        ];

        $statusDasar = [
            'PINDAH DALAM NEGERI' => 3,
            'PINDAH LUAR NEGERI'  => 3,
        ];

        $golonganDarah = [
            'Tdk Th' => 13,
        ];

        $this->kodeSex = [
            'laki-laki' => 1,
            'perempuan' => 2,
            'l'         => 1,
            'lk'        => 1,
            'p'         => 2,
            'pr'        => 2,
        ];
        $this->kodeHubungan         = array_change_key_case(array_combine(SHDKEnum::values(), SHDKEnum::keys()));
        $this->kodeAgama            = array_change_key_case(array_combine(AgamaEnum::values(), AgamaEnum::keys()));
        $this->kodePendidikanKK     = array_change_key_case(array_merge(array_combine(PendidikanKKEnum::values(), PendidikanKKEnum::keys()), $pendidikan));
        $this->kodePendidikanSedang = array_change_key_case(array_combine(PendidikanSedangEnum::values(), PendidikanSedangEnum::keys()));
        $this->kodePekerjaan        = array_change_key_case(array_combine(PekerjaanEnum::values(), PekerjaanEnum::keys()));
        $this->kodeStatus           = array_change_key_case(array_merge(array_combine(StatusKawinEnum::values(), StatusKawinEnum::keys()), $status));
        $this->kodeGolonganDarah    = array_change_key_case(array_merge(array_combine(GolonganDarahEnum::values(), GolonganDarahEnum::keys()), $golonganDarah));
        $this->kodeKtpEl            = array_change_key_case(array_combine(StatusRekamEnum::values(), StatusRekamEnum::keys()));
        $this->kodeStatusRekam      = array_change_key_case(array_combine(StatusKTPEnum::values(), StatusKTPEnum::keys()));
        $this->kodeStatusDasar      = array_change_key_case(array_merge(array_combine(StatusDasarEnum::values(), StatusDasarEnum::keys()), $statusDasar));
        $this->kodeCacat            = array_change_key_case(array_combine(CacatEnum::values(), CacatEnum::keys()));
        $this->kodeCaraKb           = array_change_key_case(array_combine(CaraKBEnum::values(), CaraKBEnum::keys()));
        $this->kodeWargaNegara      = array_change_key_case(array_combine(WargaNegaraEnum::values(), WargaNegaraEnum::keys()));
        $this->kodeHamil            = array_change_key_case(array_combine(HamilEnum::values(), HamilEnum::keys()));
        $this->kodeAsuransi         = array_change_key_case(array_combine(AsuransiEnum::values(), AsuransiEnum::keys()));
    }

    public function imporExcel($hapus = false)
    {
        try {
            if ($this->fileImportValid() == false) {
                return null;
            }

            $reader = new Reader();
            // $reader->setShouldPreserveEmptyRows(true);
            $reader->open($_FILES['userfile']['tmp_name']);

            // Pengguna bisa menentukan apakah data penduduk yang ada dihapus dulu
            // atau tidak sebelum melakukan impor
            if ($hapus && PendudukSaja::bolehHapusPenduduk()) {
                $this->hapusDataPenduduk();
            }

            foreach ($reader->getSheetIterator() as $sheet) {
                $gagal        = 0;
                $ganda        = 0;
                $pesan        = '';
                $barisData    = 0;
                $barisPertama = false;
                $dataPenduduk = [];
                $daftarKolom  = [];

                if ($sheet->getName() === 'Data Penduduk') {

                    $dataExcel = collect($sheet->getRowIterator())->map(static fn ($row) => collect($row->getCells())->map(static fn ($cell): bool|DateInterval|DateTimeInterface|float|int|string|null => $cell->getValue()))
                        ->chunk(500)
                        ->toArray();
                    DB::statement('SET character_set_connection = utf8');
                    DB::statement('SET character_set_client = utf8');

                    foreach ($dataExcel as $row) {
                        foreach ($row as $rowData) {
                            $barisData++;

                            // Baris kedua = '###' menunjukkan telah sampai pada baris data terakhir
                            if ($rowData[1] == '###') {
                                break;
                            }

                            // Baris pertama diabaikan, berisi nama kolom
                            if (! $barisPertama) {
                                $barisPertama = true;
                                $daftarKolom  = $rowData;

                                foreach ($daftarKolom as $kolom) {
                                    if (! in_array($kolom, self::DAFTAR_KOLOM)) {
                                        return set_session('error', 'Data penduduk gagal diimpor, nama kolom ' . $kolom . ' tidak sesuai.');
                                    }
                                }

                                continue;
                            }

                            $isiBaris      = $this->getIsiBaris($daftarKolom, $rowData);
                            $errorValidasi = $this->dataImportValid($isiBaris);
                            if (empty($errorValidasi)) {
                                $this->tulisWilayah($isiBaris);
                                $this->tulisKeluarga($isiBaris);
                                // Untuk pesan jika data yang sama akan diganti
                                if ($index = array_search($isiBaris['nik'], $dataPenduduk) && $isiBaris['nik'] != '0') {
                                    $ganda++;
                                    $pesan .= $barisData . ') NIK ' . $isiBaris['nik'] . ' sama dengan baris ' . ($index + 2) . '<br>';
                                }
                                $dataPenduduk[] = $isiBaris['nik'];
                                $this->tulisPenduduk($isiBaris);
                                if ($error = $this->errorTulisPenduduk) {
                                    $gagal++;
                                    $pesan .= $barisData . ') ' . $error['message'] . '<br>';
                                }
                                if ($this->infoTulisPenduduk) {
                                    $pesan .= $barisData . ') ' . $this->infoTulisPenduduk['message'] . '<br>';
                                }
                            } else {
                                $gagal++;
                                $pesan .= $barisData . ') ' . $errorValidasi . '<br>';
                            }
                        }
                    }
                    // Hapus data lat dan lng yang null
                    DB::table('tweb_penduduk_map')->orWhereNull(['id', 'lat', 'lng'])->delete();

                    if (($barisData - 1) <= 0) {
                        return set_session('error', 'Data penduduk gagal diimpor');
                    }

                    $pesan_impor = [
                        'gagal'  => $gagal,
                        'ganda'  => $ganda,
                        'pesan'  => $pesan,
                        'sukses' => ($barisData - 1) - $gagal,
                    ];

                    set_session('pesan_impor', $pesan_impor);
                }
            }
            $reader->close();

            return set_session('success', 'Data penduduk berhasil diimpor');
        } catch (Exception $e) {
            logger()->error($e);

            return set_session('error', 'Data penduduk gagal diimpor.');
        }
    }

    /*
     * ====================
     * Selesai IMPOR EXCEL
     * ====================
    */

    public function imporBip($hapus = false)
    {
        try {
            if ($this->fileImportValid() == false) {
                return null;
            }

            $data = new SpreadsheetExcelReader($_FILES['userfile']['tmp_name']);

            DB::statement('SET character_set_connection = utf8');
            DB::statement('SET character_set_client = utf8');

            // Pengguna bisa menentukan apakah data penduduk yang ada dihapus dulu
            // atau tidak sebelum melakukan impor
            if ($hapus) {
                $this->hapusDataPenduduk();
            }

            $bip = new Bip($data);
            $bip->imporBip();
        } catch (Exception $e) {
            log_message('error', $e->getMessage());

            return set_session('error', 'Data penduduk gagal diimpor.');
        }

        return null;
    }

    /**
     * Konversi tulisan menjadi kode angka
     *
     * @param array		tulisan => kode angka
     * @param string	tulisan yang akan dikonversi
     *
     * @return int kode angka, -1 kalau tidak ada kodenya
     */
    protected function getKode(mixed $daftar_kode, mixed $nilai)
    {
        /*
         *
         * Hapus spasi pada daftar kode
         * Contoh:
         * SLTA / SEDERAJAT => SLTA/SEDERAJAT
         *
         */
        $daftar_kode = array_combine(str_replace(' ', '', array_keys($daftar_kode)), array_values($daftar_kode));

        $nilai = str_replace(' ', '', strtolower((string) $nilai));
        $nilai = preg_replace('/\\s*\\/\\s*/', '/', $nilai);

        if (! empty($nilai) && $nilai != '-' && ! array_key_exists($nilai, $daftar_kode)) {
            return $nilai;
        } // kode salah

        return $daftar_kode[$nilai];
    }

    protected function konversiKode($daftar_kode, $nilai)
    {
        $nilai = trim((string) $nilai);

        if (ctype_digit($nilai)) {
            return $nilai;
        }

        return $this->getKode($daftar_kode, $nilai);
    }

    protected function dataImportValid(array $isiBaris)
    {
        $validator = Validator::make($isiBaris, [
            'nik'                  => ['required', 'regex:/^(0|\d{16})$/'],
            'no_kk'                => ['required', 'digits:16', 'regex:/^\d+$/'],
            'sex'                  => ['required', Rule::in([1, 2])],
            'agama_id'             => ['required', 'integer', 'between:1,7'],
            'pendidikan_kk_id'     => ['required', 'integer', 'between:1,10'],
            'pendidikan_sedang_id' => ['nullable', 'integer', 'between:1,18'],
            'pekerjaan_id'         => ['required', 'integer', 'between:1,89'],
            'status_kawin'         => ['required', 'integer', 'between:1,4'],
            'kk_level'             => ['required', 'integer', 'between:1,11'],
            'warganegara_id'       => ['required', 'integer', 'between:1,3'],
            'golongan_darah_id'    => ['required', 'integer', 'between:1,13'],
            'cacat_id'             => ['nullable', 'integer', 'between:1,7'],
            'cara_kb_id'           => ['nullable', static function ($attribute, $value, $fail): void {
                if (! in_array($value, array_merge(range(1, 8), ['99']))) {
                    $fail("kode cara_kb {$value}  tidak dikenal");
                }
            }],
            'hamil'        => ['nullable', Rule::in([1, 2])],
            'ktp_el'       => ['nullable', Rule::in([1, 2])],
            'status_rekam' => ['nullable', 'integer', 'between:1,8'],
            'status_dasar' => ['nullable', Rule::in([1, 2, 3, 4, 6, 9])],
            'id_asuransi'  => ['nullable', function ($attribute, $value, $fail): void {
                if (! in_array((int) $value, $this->kodeAsuransi)) {
                    $fail('kode asuransi tidak dikenal');
                }
            }],
            'tag_id_card'       => ['nullable', 'min:10', 'max:17'],
            'lat'               => ['nullable', 'min:2', 'max:24'],
            'lng'               => ['nullable', 'min:2', 'max:24'],
            'tanggallahir'      => ['required', 'date_format:Y-m-d'],
            'tanggalperkawinan' => ['nullable', 'date_format:Y-m-d'],
            'tanggalperceraian' => ['nullable', 'date_format:Y-m-d'],
            'ayah_nik'          => ['nullable', 'regex:/^\d+$/', 'size:16'],
            'ibu_nik'           => ['nullable', 'regex:/^\d+$/', 'size:16'],
            'nama'              => ['required', static function ($attribute, $value, $fail): void {
                if (cekNama($value)) {
                    $fail('Nama hanya boleh berisi karakter alpha, spasi, titik, koma, tanda petik dan strip');
                }
            }],
            'nama_ayah' => ['required', static function ($attribute, $value, $fail): void {
                if (cekNama($value)) {
                    $fail('Nama ayah hanya boleh berisi karakter alpha, spasi, titik, koma, tanda petik dan strip');
                }
            }],
            'nama_ibu' => ['required', static function ($attribute, $value, $fail): void {
                if (cekNama($value)) {
                    $fail('Nama ibu hanya boleh berisi karakter alpha, spasi, titik, koma, tanda petik dan strip');
                }
            }],
            // tambahan validasi yang belum ada sebelumnya - fields yang wajib diisi berdasarkan issue #9931
            'alamat'               => 'required|string|max:255',
            'dusun'                => 'required|string|max:50',
            'rw'                   => 'required|string|max:3',
            'rt'                   => 'required|string|max:3',
            'tempatlahir'          => 'required|string|max:100',
            'akta_lahir'           => 'nullable|string|max:50',
            'dokumen_pasport'      => 'required|string|max:50',
            'tanggal_akhir_paspor' => 'nullable|date',
            'dokumen_kitas'        => 'required|string|max:50',
            'akta_perkawinan'      => 'nullable|string|max:50',
            'akta_perceraian'      => 'nullable|string|max:50',
            'alamat_sekarang'      => 'nullable|string|max:255',
            'suku'                 => 'nullable|string|max:50',
            'no_asuransi'          => 'nullable|string|max:50',
            'ket'                  => 'nullable|string',
        ], [
            'nik.required'                  => 'NIK tidak boleh kosong',
            'nik.regex'                     => 'NIK harus berupa 16 digit angka atau 0 untuk NIK sementara',
            'no_kk.digits'                  => 'Nomor KK salah',
            'no_kk.regex'                   => 'Nomor KK salah',
            'tanggallahir.required'         => 'Tanggal lahir tidak boleh kosong',
            'tanggallahir.date_format'      => 'Tanggal lahir (' . ($isiBaris['tanggallahir'] ?? '') . ') tidak valid. Format tanggal harus yyyy-mm-dd',
            'tanggalperkawinan.date_format' => 'Tanggal perkawinan (' . ($isiBaris['tanggalperkawinan'] ?? '') . ') tidak valid. Format tanggal harus yyyy-mm-dd',
            'tanggalperceraian.date_format' => 'Tanggal perceraian (' . ($isiBaris['tanggalperceraian'] ?? '') . ') tidak valid. Format tanggal harus yyyy-mm-dd',
            'sex.required'                  => 'Jenis kelamin wajib diisi',
            'sex.in'                        => 'kode jenis kelamin ' . ($isiBaris['sex'] ?? '') . '  tidak dikenal',
            'agama_id.required'             => 'Agama wajib diisi',
            'agama_id.between'              => 'kode agama ' . ($isiBaris['agama_id'] ?? '') . '  tidak dikenal',
            'pendidikan_kk_id.required'     => 'Pendidikan dalam KK wajib diisi',
            'pendidikan_kk_id.between'      => 'kode pendidikan ' . ($isiBaris['pendidikan_kk_id'] ?? '') . '  tidak dikenal',
            'pendidikan_sedang_id.between'  => 'kode pendidikan_sedang ' . ($isiBaris['pendidikan_sedang_id'] ?? '') . '  tidak dikenal',
            'pekerjaan_id.required'         => 'Pekerjaan wajib diisi',
            'pekerjaan_id.between'          => 'kode pekerjaan ' . ($isiBaris['pekerjaan_id'] ?? '') . '  tidak dikenal',
            'status_kawin.required'         => 'Status kawin wajib diisi',
            'status_kawin.between'          => 'kode status_kawin ' . ($isiBaris['status_kawin'] ?? '') . ' tidak dikenal',
            'kk_level.required'             => 'Status hubungan dalam keluarga (SHDK) wajib diisi',
            'kk_level.between'              => 'kode status hubungan ' . ($isiBaris['kk_level'] ?? '') . '  tidak dikenal',
            'warganegara_id.required'       => 'Kewarganegaraan wajib diisi',
            'warganegara_id.between'        => 'kode warganegara ' . ($isiBaris['warganegara_id'] ?? '') . '  tidak dikenal',
            'golongan_darah_id.required'    => 'Golongan darah wajib diisi',
            'golongan_darah_id.between'     => 'kode golongan_darah ' . ($isiBaris['golongan_darah_id'] ?? '') . '  tidak dikenal',
            'cacat_id.between'              => 'kode cacat ' . ($isiBaris['cacat_id'] ?? '') . '  tidak dikenal',
            'hamil.in'                      => 'kode hamil ' . ($isiBaris['hamil'] ?? '') . '  tidak dikenal',
            'ktp_el.in'                     => 'kode ktp_el ' . ($isiBaris['ktp_el'] ?? '') . ' tidak dikenal',
            'status_rekam.between'          => 'kode status_rekam ' . ($isiBaris['status_rekam'] ?? '') . ' tidak dikenal',
            'status_dasar.in'               => 'kode status_dasar ' . ($isiBaris['status_dasar'] ?? '') . ' tidak dikenal',
            'tag_id_card.min'               => 'Panjang karakter tag id card minimal 10 karakter dan maksimal 17 karakter',
            'tag_id_card.max'               => 'Panjang karakter tag id card minimal 10 karakter dan maksimal 17 karakter',
            'lat.min'                       => 'Panjang karakter lat minimal 2 karakter dan maksimal 24 karakter',
            'lat.max'                       => 'Panjang karakter lat minimal 2 karakter dan maksimal 24 karakter',
            'lng.min'                       => 'Panjang karakter lng minimal 2 karakter dan maksimal 24 karakter',
            'lng.max'                       => 'Panjang karakter lng minimal 2 karakter dan maksimal 24 karakter',
            'ayah_nik.regex'                => 'NIK ayah salah',
            'ayah_nik.size'                 => 'NIK ayah salah',
            'ibu_nik.regex'                 => 'NIK ibu salah',
            'ibu_nik.size'                  => 'NIK ibu salah',
            'nama.required'                 => 'Nama wajib diisi',
            'nama_ayah.required'            => 'Nama ayah wajib diisi',
            'alamat.required'               => 'Alamat wajib diisi',
            'dusun.required'                => 'Dusun wajib diisi',
            'rw.required'                   => 'RW wajib diisi',
            'rt.required'                   => 'RT wajib diisi',
            'tempatlahir.required'          => 'Tempat lahir wajib diisi',
            'dokumen_pasport.required'      => 'Dokumen pasport wajib diisi',
            'dokumen_kitas.required'        => 'Dokumen kitas wajib diisi',
        ]);

        if ($validator->fails()) {
            return $validator->errors()->first();
        }

        return null;
    }

    protected function formatTanggal(string|DateTimeInterface|null $kolom_tanggal)
    {
        if ($kolom_tanggal === null || $kolom_tanggal === '' || $kolom_tanggal === '0') {
            return null;
        }

        try {
            return Carbon::parse($kolom_tanggal)->format('Y-m-d');
        } catch (Exception) {
            log_message('error', 'Format tanggal (' . $kolom_tanggal . ') tidak valid. Format tanggal harus dd-mm-yyyy');

            return false;
        }
    }

    protected function cekValidasiTanggal($tanggal)
    {
        try {
            $date = Carbon::createFromFormat('Y-m-d', $tanggal);

            return $date && $date->format('Y-m-d') === $tanggal;
        } catch (Exception) {
            return false;
        }
    }

    protected function tulisWilayah(array &$isiBaris)
    {
        // Masukkan wilayah administratif ke tabel tweb_wil_clusterdesa apabila
        // wilayah administratif ini belum ada

        // --- Masukkan dusun apabila belum ada
        $belumAdaDusun = Wilayah::where('dusun', $isiBaris['dusun'])->doesntExist();
        if ($belumAdaDusun) {
            $dusun = [
                [
                    'dusun'     => $isiBaris['dusun'],
                    'rw'        => 0,
                    'rt'        => 0,
                    'config_id' => identitas('id'),
                ],
                [
                    'dusun'     => $isiBaris['dusun'],
                    'rw'        => '-',
                    'rt'        => 0,
                    'config_id' => identitas('id'),
                ],
                [
                    'dusun'     => $isiBaris['dusun'],
                    'rw'        => '-',
                    'rt'        => '-',
                    'config_id' => identitas('id'),
                ],
            ];

            $hasil = Wilayah::insert($dusun);
        }

        // --- Masukkan rw apabila belum ada
        $belumAdaRw = Wilayah::where('dusun', $isiBaris['dusun'])->where('rw', $isiBaris['rw'])->doesntExist();
        if ($belumAdaRw) {
            $rw = [
                [
                    'dusun'     => $isiBaris['dusun'],
                    'rw'        => $isiBaris['rw'],
                    'rt'        => 0,
                    'config_id' => identitas('id'),
                ],
                [
                    'dusun'     => $isiBaris['dusun'],
                    'rw'        => $isiBaris['rw'],
                    'rt'        => '-',
                    'config_id' => identitas('id'),
                ],
            ];

            $hasil = Wilayah::insert($rw);
        }

        // --- Masukkan rt apabila belum ada
        $cekRt = Wilayah::where('dusun', $isiBaris['dusun'])->where('rw', $isiBaris['rw'])->where('rt', $isiBaris['rt'])->first();
        if (! $cekRt) {
            $rt = [
                'dusun'     => $isiBaris['dusun'],
                'rw'        => $isiBaris['rw'],
                'rt'        => $isiBaris['rt'],
                'config_id' => identitas('id'),
            ];

            $rt                     = Wilayah::create($rt);
            $isiBaris['id_cluster'] = $rt->id;
        } else {
            $isiBaris['id_cluster'] = $cekRt->id;
        }
    }

    protected function tulisKeluarga(array &$isiBaris)
    {
        // Penduduk dengan no_kk kosong adalah penduduk lepas
        if ($isiBaris['no_kk'] == '') {
            return false;
        }
        // Masukkan keluarga ke tabel tweb_keluarga apabila
        // keluarga ini belum ada
        $keluargaBaru = false;

        $keluarga = Keluarga::select('id')->where(['no_kk' => $isiBaris['no_kk']])->first();

        $data['updated_by'] = ci_auth()->id;
        $data['id_cluster'] = $isiBaris['id_cluster'];
        $data['config_id']  = identitas('id');

        if ($keluarga) {
            // Update keluarga apabila sudah ada
            $isiBaris['id_kk'] = $keluarga->id;
            // Hanya update apabila alamat kosong
            // karena alamat keluarga akan diupdate menggunakan data kepala keluarga di tulis_tweb_pendududk
            if (! $keluarga->alamat) {
                $keluarga->alamat = $isiBaris['alamat'];
                $keluarga->save();
            }
        } else {
            $data['no_kk']     = $isiBaris['no_kk'];
            $data['alamat']    = $isiBaris['alamat'];
            $keluarga          = Keluarga::create($data);
            $isiBaris['id_kk'] = $keluarga->id;
            $keluargaBaru      = true;

            // Tulis Log Keluarga Baru
            $log_keluarga = [
                'id_kk'           => $isiBaris['id_kk'],
                'id_peristiwa'    => 1,
                'tgl_peristiwa'   => date('Y-m-d H:i:s'),
                'id_pend'         => null,
                'id_log_penduduk' => null,
                'updated_by'      => auth()->id,
            ];

            LogKeluarga::create($log_keluarga);
        }

        return $keluargaBaru;
    }

    protected function tulisPenduduk($isiBaris)
    {
        $this->errorTulisPenduduk = null;
        $this->infoTulisPenduduk  = [];

        $data = [];

        // Siapkan data penduduk
        $kolomBaris = DB::connection()->getSchemaBuilder()->getColumnListing('tweb_penduduk');

        foreach ($kolomBaris as $kolom) {
            if (! empty($isiBaris[$kolom])) {
                $data[$kolom] = $isiBaris[$kolom];
            }
        }

        $data['status'] = '1';  // penduduk impor dianggap aktif

        // Jangan masukkan atau update isian yang kosong
        foreach ($data as $key => $value) {
            if (empty($value)) {
                if (! ($key == 'nik' && $value == '0')) {
                    unset($data[$key]);
                } // Kecuali untuk kolom NIk boleh 0
            }
        }
        // jika kk_level adalah kepala keluarga maka periksa,
        // apakah sudah ada kepala_keluarga untuk keluarga tersebut ?
        // maka ganti menjadi lainnya
        if ($data['kk_level'] == SHDKEnum::KEPALA_KELUARGA) {
            $adaKepalaKeluarga = PendudukSaja::where(['id_kk' => $isiBaris['id_kk'], 'kk_level' => SHDKEnum::KEPALA_KELUARGA])->first();
            if ($adaKepalaKeluarga) {
                $data['kk_level']                   = SHDKEnum::LAINNYA;
                $this->infoTulisPenduduk['message'] = 'Status SHDK pada NIK : ' . $data['nik'] . ' diubah menjadi ' . SHDKEnum::valueOf(SHDKEnum::LAINNYA) . ' karena dalam keluarga tersebut sudah ada kepala keluarga';
            }
        }
        // Masukkan penduduk ke tabel tweb_penduduk apabila
        // penduduk ini belum ada
        // Penduduk dianggap baru apabila NIK tidak diketahui (nilai 0)
        $pendudukBaru = false;
        if ($isiBaris['nik'] == 0) {
            // Update penduduk NIK sementara dengan ketentuan
            // 1. Cek nama
            // 2. Cek tempat lahir
            // 3. Cek tgl lahir
            // Jika ke 3 data tsb sama, maka data sebelumnya dianggap sama, selain itu dianggap penduduk yg berbeda/baru
            $cekData         = PendudukSaja::where(['nama' => $isiBaris['nama'], 'tempatlahir' => $isiBaris['tempatlahir'], 'tanggallahir' => $isiBaris['tanggallahir']])->first();
            $isiBaris['nik'] = $cekData->nik ?? Penduduk::nikSementara();
        }

        // Hamil hanya untuk jenis kelamin perempuan (2)
        if ($data['sex'] == '1') {
            unset($data['hamil']);
        }

        $res = PendudukSaja::where(['nik' => $isiBaris['nik']])->first();
        if ($res) {
            // Abaikan status dasar
            if ($data['status_dasar'] != '' && $data['status_dasar'] != $res['status_dasar']) {
                return $this->errorTulisPenduduk['message'] = 'Tidak dapat mengubah status dasar dengan nik ' . $data['nik'] . ' karena telah terdaftar';
            }

            // Abaikan shdk
            if ($data['kk_level'] != '' && $data['kk_level'] != $res['kk_level']) {
                return $this->errorTulisPenduduk['message'] = 'Tidak dapat mengubah status hubungan dengan nik ' . $data['nik'] . ' karena telah terdaftar';
            }

            // Abaikan no kk
            $keluarga = Keluarga::where(['id' => $res['id_kk']])->first();
            if ($isiBaris['no_kk'] != '' && $isiBaris['no_kk'] != $keluarga['no_kk']) {
                return $this->errorTulisPenduduk['message'] = 'Tidak dapat mengubah nomor kk dengan nik ' . $data['nik'] . ' karena telah terdaftar';
            }

            // Abaikan alamat
            $keluarga = Keluarga::where(['id' => $res['id_kk']])->first();
            if ($isiBaris['alamat'] != '' && $isiBaris['alamat'] != $keluarga['alamat']) {
                return $this->errorTulisPenduduk['message'] = 'Tidak dapat mengubah alamat dengan nik ' . $data['nik'] . ' karena telah terdaftar';
            }

            // Abaikan dusun
            $cluster = Wilayah::where(['id' => $keluarga['id_cluster']])->first();
            if ($isiBaris['dusun'] != '' && $isiBaris['dusun'] != $cluster['dusun']) {
                return $this->errorTulisPenduduk['message'] = 'Tidak dapat mengubah dusun dengan nik ' . $data['nik'] . ' karena telah terdaftar';
            }

            // Abaikan rw
            $cluster = Wilayah::where(['id' => $keluarga['id_cluster']])->first();
            if ($isiBaris['rw'] != '' && $isiBaris['rw'] != $cluster['rw']) {
                return $this->errorTulisPenduduk['message'] = 'Tidak dapat mengubah rw dengan nik ' . $data['nik'] . ' karena telah terdaftar';
            }

            // Abaikan rt
            $cluster = Wilayah::where(['id' => $keluarga['id_cluster']])->first();
            if ($isiBaris['rt'] != '' && $isiBaris['rt'] != $cluster['rt']) {
                return $this->errorTulisPenduduk['message'] = 'Tidak dapat mengubah rt dengan nik ' . $data['nik'] . ' karena telah terdaftar';
            }

            if ($data['status_dasar'] != -1) {
                if (PendudukSaja::cekTagIdCard($data['tag_id_card'], $res['id'])) {
                    return $this->errorTulisPenduduk['message'] = 'Tag ID Card ' . $data['tag_id_card'] . ' sudah digunakan pada NIK : ' . $data['nik'];
                }

                $data['nik'] = $res['nik'];

                // Hanya update apabila status dasar valid (data SIAK)
                $data['updated_at'] = date('Y-m-d H:i:s');
                $data['updated_by'] = ci_auth()->id;

                try {
                    PendudukSaja::where('id', $res['id'])->update($data);
                } catch (Exception $e) {
                    $this->errorTulisPenduduk = $e->getMessage();
                }
            }
            $pendudukBaru = $res['id'];
        } else {
            if (setting('tgl_data_lengkap_aktif') != 0) {
                return $this->errorTulisPenduduk['message'] = 'Tidak dapat menambahkan penduduk dengan nik ' . $data['nik'] . ' karena data sudah ditetapkan lengkap, <a href="#" data-remote="false" data-toggle="modal" data-target="#pengaturan" data-title="Pengaturan Penduduk">klik disini</a> untuk mengubah pengaturan penduduk menjadi belum lengkap.';
            }

            if ($data['nama'] == '' || $isiBaris['no_kk'] == '' || $data['kk_level'] == '' || $isiBaris['dusun'] == '' || $isiBaris['rt'] == '' || $isiBaris['rw'] == '') {
                return $this->errorTulisPenduduk['message'] = 'nama, nomor kk, shdk, dusun, rt, rw harus diisi untuk penduduk baru';
            }

            if (PendudukSaja::cekTagIdCard($data['tag_id_card'])) {
                return $this->errorTulisPenduduk['message'] = 'Tag ID Card ' . $data['tag_id_card'] . ' sudah digunakan pada NIK : ' . $data['nik'];
            }

            // Konfersi nik 0 sesuai format nik sementara
            $data['nik'] = $isiBaris['nik'];

            if ($data['status_dasar'] == -1) {
                $data['status_dasar'] = 9;
            } // Tidak Valid
            $data['created_at'] = date('Y-m-d H:i:s');
            $data['created_by'] = ci_auth()->id;
            $data['config_id']  = identitas('id');

            try {
                $pendudukBaru = PendudukSaja::create($data)->id;
            } catch (Exception $e) {
                $this->errorTulisPenduduk['message'] = $e->getMessage();
            }

            // Insert ke log_penduduk pada penduduk baru
            $kode_peristiwa = $data['status_dasar'];
            if ($data['status_dasar'] == 1 || $data['status_dasar'] == 9) {
                $kode_peristiwa = 5;
            }

            $log['tgl_peristiwa']  = $data['created_at'];
            $log['kode_peristiwa'] = $kode_peristiwa;
            $log['tgl_lapor']      = $data['created_at'];
            $log['id_pend']        = $pendudukBaru;
            $log['created_by']     = $data['created_by'];
            $log['config_id']      = identitas('id');
            LogPenduduk::upsert($log, ['config_id', 'id_pend', 'kode_peristiwa', 'tgl_peristiwa']);
        }

        // Tambah atau perbarui lokasi penduduk
        $this->pendudukMap($pendudukBaru, $isiBaris['lat'], $isiBaris['lng']);

        // Update nik_kepala dan id_cluster di keluarga apabila baris ini kepala keluarga
        // dan sudah ada NIK
        if ($data['kk_level'] == SHDKEnum::KEPALA_KELUARGA) {
            Keluarga::where('id', $data['id_kk'])
                ->update([
                    'nik_kepala' => $pendudukBaru,
                    'id_cluster' => $isiBaris['id_cluster'],
                    'alamat'     => $isiBaris['alamat'],
                ]);
        }

        return $pendudukBaru;
    }

    /**
     * ========================================================
     * IMPOR EXCEL
     * ========================================================
     */
    private function fileImportValid(): bool
    {
        // error 1 = UPLOAD_ERR_INI_SIZE; lihat Upload.php
        // TODO: pakai cara upload yg disediakan Codeigniter
        if ($_FILES['userfile']['error'] == 1) {
            $upload_mb = max_upload();
            set_session('error', ' -> Ukuran file melebihi batas ' . $upload_mb . ' MB');

            return false;
        }
        $mime_type_excel = ['application/octet-stream', 'application/vnd.ms-excel', 'application/x-csv', 'text/x-csv', 'text/csv', 'application/csv', 'application/excel', 'application/vnd.msexcel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel.sheet.macroenabled.12', 'application/wps-office.xlsx'];
        if (! in_array(strtolower((string) $_FILES['userfile']['type']), $mime_type_excel)) {
            set_session('error', ' -> Jenis file salah: ' . $_FILES['userfile']['type']);

            return false;
        }

        return true;
    }

    private function cekKosong($isi)
    {
        if ($isi instanceof DateTimeImmutable) {
            return $isi->format('Y-m-d');
        }

        $isi = trim((string) $isi);

        return (in_array($isi, ['', '-'])) ? null : $isi;
    }

    private function cekKosongDenganDefault($isi, $default = '-')
    {
        if ($isi instanceof DateTimeImmutable) {
            return $isi->format('Y-m-d');
        }

        $isi = trim((string) $isi);

        return $isi === '' || $isi === '0' ? $default : $isi;
    }

    private function getIsiBaris(array $kolom, array $rowData)
    {
        $kolom              = array_flip(array_filter($kolom, 'strlen'));
        $isiBaris['alamat'] = trim((string) $rowData[$kolom['alamat']]);
        $dusun              = ltrim(trim((string) $rowData[$kolom['dusun']]), "'");
        $dusun              = str_replace('_', ' ', $dusun);
        $dusun              = strtoupper($dusun);
        $dusun              = str_replace('DUSUN ', '', $dusun);
        $isiBaris['dusun']  = $dusun;

        $isiBaris['rw']        = ltrim(trim((string) $rowData[$kolom['rw']]), "'");
        $isiBaris['rt']        = ltrim(trim((string) $rowData[$kolom['rt']]), "'");
        $isiBaris['nama']      = trim((string) $rowData[$kolom['nama']]);
        $isiBaris['nama_ayah'] = trim((string) $rowData[$kolom['nama']]);
        $isiBaris['nama_ibu']  = trim((string) $rowData[$kolom['nama']]);

        // Data Disdukcapil adakalanya berisi karakter tambahan pada no_kk dan nik
        // yang tidak tampak (non-printable characters),
        // jadi perlu dibuang
        $no_kk             = trim((string) $rowData[$kolom['no_kk']]);
        $no_kk             = preg_replace('/[^0-9]/', '', $no_kk);
        $isiBaris['no_kk'] = $no_kk;

        $nik             = trim((string) $rowData[$kolom['nik']]);
        $nik             = preg_replace('/[^0-9]/', '', $nik);
        $isiBaris['nik'] = $nik;

        $isiBaris['sex']                  = $this->konversiKode($this->kodeSex, $rowData[$kolom['sex']]);
        $isiBaris['tempatlahir']          = $this->cekKosong($rowData[$kolom['tempatlahir']]);
        $isiBaris['tanggallahir']         = $this->cekKosong($this->formatTanggal($rowData[$kolom['tanggallahir']]));
        $isiBaris['agama_id']             = $this->konversiKode($this->kodeAgama, $rowData[$kolom['agama_id']]);
        $isiBaris['pendidikan_kk_id']     = $this->konversiKode($this->kodePendidikanKK, $rowData[$kolom['pendidikan_kk_id']]);
        $isiBaris['pendidikan_sedang_id'] = $this->konversiKode($this->kodePendidikanSedang, $rowData[$kolom['pendidikan_sedang_id']]);
        $isiBaris['pekerjaan_id']         = $this->konversiKode($this->kodePekerjaan, $rowData[$kolom['pekerjaan_id']]);
        $isiBaris['status_kawin']         = $this->konversiKode($this->kodeStatus, $rowData[$kolom['status_kawin']]);
        $isiBaris['kk_level']             = $this->konversiKode($this->kodeHubungan, $rowData[$kolom['kk_level']]);
        $isiBaris['warganegara_id']       = $this->konversiKode($this->kodeWargaNegara, $rowData[$kolom['warganegara_id']]);
        $isiBaris['nama_ayah']            = $this->cekKosong($rowData[$kolom['nama_ayah']]);
        $isiBaris['nama_ibu']             = $this->cekKosong($rowData[$kolom['nama_ibu']]);
        $isiBaris['golongan_darah_id']    = $this->konversiKode($this->kodeGolonganDarah, $rowData[$kolom['golongan_darah_id']]);
        $isiBaris['akta_lahir']           = $this->cekKosong($rowData[$kolom['akta_lahir']]);
        $isiBaris['dokumen_pasport']      = $this->cekKosongDenganDefault($rowData[$kolom['dokumen_pasport']]);
        $isiBaris['tanggal_akhir_paspor'] = $this->cekKosong($this->formatTanggal($rowData[$kolom['tanggal_akhir_paspor']]));
        $isiBaris['dokumen_kitas']        = $this->cekKosongDenganDefault($rowData[$kolom['dokumen_kitas']]);
        $isiBaris['ayah_nik']             = $this->cekKosong($rowData[$kolom['ayah_nik']]);
        $isiBaris['ibu_nik']              = $this->cekKosong($rowData[$kolom['ibu_nik']]);
        $isiBaris['akta_perkawinan']      = $this->cekKosong($rowData[$kolom['akta_perkawinan']]);
        $isiBaris['tanggalperkawinan']    = $this->cekKosong($this->formatTanggal($rowData[$kolom['tanggalperkawinan']]));
        $isiBaris['akta_perceraian']      = $this->cekKosong($rowData[$kolom['akta_perceraian']]);
        $isiBaris['tanggalperceraian']    = $this->cekKosong($this->formatTanggal($rowData[$kolom['tanggalperceraian']]));
        $isiBaris['cacat_id']             = $this->konversiKode($this->kodeCacat, $rowData[$kolom['cacat_id']]);
        $isiBaris['cara_kb_id']           = $this->konversiKode($this->kodeCaraKb, $rowData[$kolom['cara_kb_id']]);
        $isiBaris['hamil']                = $this->konversiKode($this->kodeHamil, $rowData[$kolom['hamil']]);
        $isiBaris['ktp_el']               = $this->konversiKode($this->kodeKtpEl, $rowData[$kolom['ktp_el']]);
        $isiBaris['status_rekam']         = $this->konversiKode($this->kodeStatusRekam, $rowData[$kolom['status_rekam']]);
        $isiBaris['alamat_sekarang']      = $this->cekKosong($rowData[$kolom['alamat_sekarang']]);
        $isiBaris['status_dasar']         = $this->konversiKode($this->kodeStatusDasar, $rowData[$kolom['status_dasar']]);
        $isiBaris['suku']                 = $this->cekKosong($rowData[$kolom['suku']]);
        $isiBaris['tag_id_card']          = $this->cekKosong($rowData[$kolom['tag_id_card']]);
        $isiBaris['id_asuransi']          = $this->konversiKode($this->kodeAsuransi, $rowData[$kolom['id_asuransi']]);
        $isiBaris['no_asuransi']          = $this->cekKosong($rowData[$kolom['no_asuransi']]);
        $isiBaris['lat']                  = $this->cekKosong($rowData[$kolom['lat']]);
        $isiBaris['lng']                  = $this->cekKosong($rowData[$kolom['lng']]);
        $isiBaris['ket']                  = $this->cekKosong($rowData[$kolom['ket']]);

        return $isiBaris;
    }

    private function pendudukMap($id = 0, $lat = null, $lng = null): ?bool
    {
        if ($lat === null || $lng === null) {
            return false;
        }

        // Ubah data penduduk map
        DB::table('tweb_penduduk_map')->updateOrInsert([
            'id' => $id,
        ], [
            'lat' => $lat,
            'lng' => $lng,
        ]);

        return null;
    }

    private function hapusDataPenduduk(): void
    {
        $tabelPenduduk = ['tweb_wil_clusterdesa', 'tweb_keluarga', 'tweb_penduduk', 'log_keluarga', 'log_penduduk', 'log_perubahan_penduduk', 'log_surat', 'tweb_rtm'];

        foreach ($tabelPenduduk as $tabel) {
            DB::table($tabel)->where('config_id', identitas('id'))->delete();
        }

        // Hapus peserta bantuan dengan sasaran penduduk, keluarga, rumah tangga, kelompok
        BantuanPeserta::whereIn('program_id', static fn ($q) => $q->select(['id'])->from('program')->whereIn('sasaran', [SasaranEnum::PENDUDUK, SasaranEnum::KELUARGA, SasaranEnum::RUMAH_TANGGA, SasaranEnum::KELOMPOK]))->delete();
    }
}
