<?php
defined('BASEPATH') or exit('No direct script access allowed');

use VisualAppeal\AutoUpdate;
use Curl\Curl;

class Model_update extends CI_Model
{
    /** @var Curl */
    protected $curl;

    /** @var bool */
    protected $ssl_verify = true;

    /** @var string */
    protected $url_checker;

    /** @var string */
    protected $url_api;

    /** @var string */
    protected $api_key;

    /** @var string */
    protected $versi;

    /** @var string */
    protected $pro;

    /** @var string */
    protected $custom;

    /** @var string */
    protected $patch;

    /** @var string */
    protected $title;

    /** @var string */
    protected $perusahaan;

    /** @var string */
    protected $email;

    /** @var string */
    protected $phone;

    public function __construct()
    {
        parent::__construct();

        $this->curl = new Curl();

        // SSL verify for update server (true on production by default)
        $this->ssl_verify = defined('UPDATE_SSL_VERIFY')
            ? (bool) UPDATE_SSL_VERIFY
            : (ENVIRONMENT === 'production');

        $this->curl->setOpt(CURLOPT_SSL_VERIFYPEER, $this->ssl_verify);
        $this->curl->setOpt(CURLOPT_SSL_VERIFYHOST, $this->ssl_verify ? 2 : 0);

        $this->url_checker = defined('URL_CHECKER') ? URL_CHECKER : '';
        $this->url_api     = (ENVIRONMENT === 'production')
            ? (defined('URL_API') ? URL_API : '')
            : (defined('URL_API_SANDBOX') ? URL_API_SANDBOX : (defined('URL_API') ? URL_API : ''));

        $info = info();
        $this->api_key     = isset($info['api_key']) ? $info['api_key'] : '';
        $this->versi       = isset($info['version']) ? $info['version'] : '0.0.0';
        $this->pro         = isset($info['versi_pro']) ? $info['versi_pro'] : '';
        $this->custom      = isset($info['versi_custom']) ? $info['versi_custom'] : '';
        $this->patch       = isset($info['patch']) ? $info['patch'] : '';
        $this->title       = isset($info['title']) ? $info['title'] : '';
        $this->perusahaan  = isset($info['perusahaan']) ? $info['perusahaan'] : '';
        $this->email       = isset($info['email']) ? $info['email'] : '';
        $this->phone       = isset($info['phone']) ? $info['phone'] : '';
    }

    private function _emit($emit, $message, $progress = null, $type = 'log')
    {
        if (!$emit) return;

        $payload = [
            'type'     => $type,
            'message'  => $message,
            'progress' => $progress,
            'time'     => date('H:i:s')
        ];

        call_user_func($emit, $payload);
    }

    /**
     * Konfigurasi update statis
     * Base: https://poskasirpercetakan.my.id/update/update_kasir.json
     */
    private function _getUpdateCfg()
    {
        // Prioritas: URL_CHECKER (domain update)
        $base = trim((string) $this->url_checker);

        // fallback: url_api
        if ($base === '') {
            $base = trim((string) $this->url_api);
        }

        // Normalisasi scheme
        if ($base !== '' && !preg_match('~^https?://~i', $base)) {
            $base = 'https://' . $base;
        }

        // Folder update
        $updateUrl = rtrim($base, '/') . '/update';

        // Temp folder lokal
        $tempRel = 'temp/update_tmp/';
        $tempAbs = FCPATH . $tempRel;

        if (!is_dir($tempAbs)) {
            @mkdir($tempAbs, 0775, true);
        }

        // Log file
        $logsRel = 'temp/update.log';
        $logsAbs = FCPATH . $logsRel;
        $logsDir = dirname($logsAbs);
        if (!is_dir($logsDir)) {
            @mkdir($logsDir, 0775, true);
        }

        return (object) [
            'temp_abs'    => $tempAbs,
            'install_abs' => FCPATH,
            'time'        => 60,
            'updateurl'   => $updateUrl,
            'updatefile'  => 'update_kasir.json',
            'logs_abs'    => $logsAbs,
            'user'        => '',
            'auth'        => ''
        ];
    }

    private function default_api($command = '', $url_api = '')
    {
        $api_url = empty($url_api) ? $this->url_api : $url_api;

        $data = [
            'APP-API-KEY' => $this->api_key,
            'text'        => $command,
            'title'       => $this->title,
            'perusahaan'  => $this->perusahaan,
            'email'       => $this->email,
            'phone'       => $this->phone
        ];

        $this->curl->setOpt(CURLOPT_SSL_VERIFYPEER, $this->ssl_verify);
        $this->curl->setOpt(CURLOPT_SSL_VERIFYHOST, $this->ssl_verify ? 2 : 0);
        $this->curl->setDefaultJsonDecoder(true);
        $this->curl->setHeader('Content-Type', 'application/json');
        $this->curl->post(rtrim($api_url, '/') . '/api/command', $data);

        if ($this->curl->error) {
            return [$this->curl->errorMessage];
        }

        return (object) $this->curl->response;
    }

    private function curl_update($token = '', $counter = 0, $url_api = '')
    {
        $api_url = empty($url_api) ? $this->url_api : $url_api;

        $data = [
            'APP-API-KEY' => $this->api_key,
            'token'       => $token,
            'counter'     => $counter
        ];

        $this->curl->setOpt(CURLOPT_SSL_VERIFYPEER, $this->ssl_verify);
        $this->curl->setOpt(CURLOPT_SSL_VERIFYHOST, $this->ssl_verify ? 2 : 0);
        $this->curl->setDefaultJsonDecoder(true);
        $this->curl->setHeader('Content-Type', 'application/json');
        $this->curl->post(rtrim($api_url, '/') . '/api/counter', $data);

        if ($this->curl->error) {
            return [$this->curl->errorMessage];
        }

        return (object) $this->curl->response;
    }

    public function ping()
    {
        $output = [];
        $status = 0;

        $target = trim((string) $this->url_checker);
        if ($target === '') $target = 'poskasirpercetakan.my.id';

        if (preg_match('~^https?://~i', $target)) {
            $host = parse_url($target, PHP_URL_HOST);
            if ($host) $target = $host;
        }

        $cmd = 'ping -n 3 ' . escapeshellarg($target);
        @exec($cmd, $output, $status);

        return $output;
    }

    public function cleartmp()
{
    $base = FCPATH . 'temp' . DIRECTORY_SEPARATOR;
    $dir  = $base . 'update_tmp' . DIRECTORY_SEPARATOR;

    // pastikan folder ada
    if (!is_dir($dir)) {
        return ['Folder temp updater tidak ditemukan: temp/update_tmp'];
    }

    $deleted = 0;
    $skipped = 0;

    $items = @scandir($dir);
    if (!$items) {
        return ['Tidak ada item yang dihapus (folder kosong)'];
    }

    foreach ($items as $item) {
        if ($item === '.' || $item === '..') continue;

        // biarkan index.html / .htaccess kalau ada
        if ($item === 'index.html' || $item === '.htaccess') {
            $skipped++;
            continue;
        }

        $path = $dir . $item;

        // hapus file / folder
        if (is_dir($path)) {
            $this->_rrmdir($path);
            $deleted++;
        } else {
            if (@unlink($path)) $deleted++;
            else $skipped++;
        }
    }

    // optional: bersihkan cache & log updater
    @unlink($base . 'update.log');
    @unlink(FCPATH . 'cache/update-versions.php.cache');

    return [
        "Bersihkan temp selesai.",
        "Terhapus: {$deleted} item",
        "Dilewati: {$skipped} item"
    ];
}


    public function clearlog($command)
    {
        $response = $this->default_api($command);
        if (is_array($response)) return $response;

        $files = array($response->status ?? '', $response->pro ?? '', $response->custom ?? '');
        $arr = [];
        $no = 0;

        foreach ($files as $file) {
            $no++;
            if (!$file) continue;

            if ($file == ($response->status ?? '')) {
                $arr[$no] = (@unlink($response->status)) ? 'log update berhasil dibersihkan' : 'log update gagal dibersihkan / file tidak ditemukan';
            }
            if ($file == ($response->pro ?? '')) {
                $arr[$no] = (@unlink($response->pro)) ? 'log pro berhasil dibersihkan' : 'log pro gagal dibersihkan / file tidak ditemukan';
            }
            if ($file == ($response->custom ?? '')) {
                $arr[$no] = (@unlink($response->custom)) ? 'log custom berhasil dibersihkan' : 'log custom gagal dibersihkan / file tidak ditemukan';
            }
        }

        deleteLog('./application/logs');
        return $arr;
    }

    public function log($command)
    {
        $response = $this->default_api($command);
        if (is_array($response)) return ['status' => 'log', 'data' => implode("\n", $response)];

        $arr['status'] = 'log';
        $filename = FCPATH . ($response->status ?? '');
        if ($filename && file_exists($filename)) {
            $arr['data'] = file_get_contents($filename);
        } else {
            $arr['data'] = $response->msg ?? 'log tidak ditemukan';
        }

        return $arr;
    }

    public function migrasi($command)
    {
        $response = $this->default_api($command);
        if (is_array($response)) return $response;

        $arr[]  = 'status :' . ($response->status ?? '');
        $arr[]  = 'versi :' . ($response->version ?? '');
        $arr[]  = ($response->message ?? '');
        return $arr;
    }

    private function cek_kolom($arg)
    {
        $string = explode(' ', strtolower($arg));

        if (count($string) == 1) {
            return ['satu' => $arg];
        } elseif (count($string) == 2) {
            list($kolom, $field) = $string;
            return ['satu' => $kolom, 'dua' => $field];
        } elseif (count($string) == 3) {
            list($kolom, $field, $tiga) = $string;
            return ['satu' => $kolom, 'dua' => $field, 'tiga' => $tiga];
        } elseif (count($string) == 4) {
            list($satu, $dua, $tiga, $empat) = $string;
            return ['satu' => $satu, 'dua' => $dua, 'tiga' => $tiga, 'empat' => $empat];
        } elseif (count($string) == 5) {
            list($satu, $dua, $tiga, $empat, $lima) = $string;
            return ['satu' => $satu, 'dua' => $dua, 'tiga' => $tiga, 'empat' => $empat, 'lima' => $lima];
        }

        return ['satu' => '', 'dua' => '', 'tiga' => '', 'empat' => '', 'lima' => '', 'enam' => ''];
    }

    /**
     * ✅ cek versi baca update_kasir.json via AutoUpdate
     */
    private function cek_versi($command)
    {
        $cfg = $this->_getUpdateCfg();

        $err = '';
        $map = $this->_fetchUpdateMap($cfg->updateurl, $cfg->updatefile, $err);
        if ($map === false) {
            log_message('error', '[UPDATE] ' . $err);
            return $this->versi;
        }

        try {
            $update = new AutoUpdate($cfg->temp_abs, $cfg->install_abs, (int)$cfg->time);
            $update->setUpdateUrl($cfg->updateurl);
            $update->setUpdateFile($cfg->updatefile);
            $update->setCurrentVersion($this->versi);
            $update->setInstallDir('./');

            try {
                $logger = new \Monolog\Logger("default");
                $logger->pushHandler(new \Monolog\Handler\StreamHandler($cfg->logs_abs));
                $update->setLogger($logger);
            } catch (\Throwable $e) {}

            if ($update->checkUpdate() === false) return $this->versi;
            if ($update->newVersionAvailable()) return (string)$update->getLatestVersion();
            return $this->versi;
        } catch (\Throwable $e) {
            log_message('error', '[UPDATE] EXCEPTION cek_versi: ' . $e->getMessage());
            return $this->versi;
        }
    }

    public function global_command($command)
    {
        $explode = $this->cek_kolom($command);
        $counter = count($explode);

        if (
            $explode['satu'] == 'patch' ||
            $explode['satu'] == 'installpatch' ||
            $explode['satu'] == 'versipro' ||
            $explode['satu'] == 'updatepro' ||
            $explode['satu'] == 'readmepro' ||
            $explode['satu'] == 'readme_custom' ||
            ($explode['dua'] ?? '') == 'pro' ||
            $counter > 2
        ) {
            $data = [
                'APP-API-KEY' => $this->api_key,
                'text'        => $command,
                'env'         => ENVIRONMENT,
                'title'       => $this->title,
                'perusahaan'  => $this->perusahaan,
                'email'       => $this->email,
                'phone'       => $this->phone
            ];
        } else {
            $data = [
                'APP-API-KEY' => $this->api_key,
                'text'        => $command,
                'env'         => ENVIRONMENT,
                'version'     => $this->cek_versi($command),
                'title'       => $this->title,
                'perusahaan'  => $this->perusahaan,
                'email'       => $this->email,
                'phone'       => $this->phone
            ];
        }

        $this->curl->setOpt(CURLOPT_SSL_VERIFYPEER, $this->ssl_verify);
        $this->curl->setOpt(CURLOPT_SSL_VERIFYHOST, $this->ssl_verify ? 2 : 0);
        $this->curl->setDefaultJsonDecoder(true);
        $this->curl->setHeader('Content-Type', 'application/json');

        $this->curl->post(rtrim($this->url_api, '/') . '/api/command', $data);

        if ($this->curl->error) {
            return [$this->curl->errorMessage];
        }

        return (object) $this->curl->response;
    }

    public function add_column($response)
    {
        $fields = $response->fields;
        $kolom = array_keys($fields);

        if ($this->db->field_exists($kolom[0], $response->table)) {
            $arr['status'] = $this->lang->line('column_already_exists');
        } else {
            $this->load->dbforge();
            $this->dbforge->add_column($response->table, $fields);
            $arr['status'] = $response->message;
        }

        return $arr;
    }

    public function modify_column($response)
    {
        $fields = $response->fields;
        $this->load->dbforge();
        $this->dbforge->modify_column($response->table, $fields);

        $arr['status'] = $this->lang->line('column_moved_success');
        return $arr;
    }

    public function drop_column($command) {}
    public function add_key($command) {}

    public function create_table($response)
    {
        $fields = $response->fields;

        if ($this->db->table_exists($response->table)) {
            $arr['status'] = 'Table ' . $response->table . ' sudah ada';
        } else {
            $this->load->dbforge();
            $this->dbforge->add_field($fields);
            $this->dbforge->add_key($response->key, TRUE);
            $attributes = ['ENGINE' => 'MyISAM'];
            $this->dbforge->create_table($response->table, FALSE, $attributes);
            $arr['status'] = $response->message;
        }

        return $arr;
    }

    public function drop_table($command) {}

    public function input_data($response)
    {
        $cek_row = $this->model_app->view_where($response->table, [$response->coloum => $response->nama_menu]);

        if ($cek_row->num_rows() > 0) {
            $arr[] = $response->lang_status . ' ' . $response->nama_menu . ' ' . $response->notice;
        } else {
            $insert = $this->model_app->input($response->table, $response->fields);
            if (($insert['status'] ?? '') == 'ok') {
                $arr[] = $response->message;
            } else {
                $arr[] = $this->lang->line('add_menu_fail');
            }
        }

        return $arr;
    }

    public function update_data($response) {}

    /**
     * ✅ FIX: Update app baca update_kasir.json (cfg statis) + JALANKAN patch_db setelah update()
     */
    public function update_aplikasi($command, $emit = null)
{
    @unlink($this->stopFile());
    $this->_emit($emit, 'Mulai update aplikasi', 0, 'start');

    // Ambil konfigurasi update dari server (API kamu)
    $this->_emit($emit, 'Mengambil konfigurasi update dari server ...');
    $response = $this->global_command($command);

    if (!is_object($response)) {
        return ['[ERROR] Respon update tidak valid'];
    }
    if (isset($response->status) && $response->status === 'error') {
        return ['[ERROR] ' . ($response->message ?? 'Gagal mengambil konfigurasi update')];
    }

    // default config anti error
    $response->temp  = $response->temp  ?? 'temp/update_tmp/';
    $response->path  = $response->path  ?? '';
    $response->time  = $response->time  ?? 60;
    $response->logs  = $response->logs  ?? 'temp/update.log';
    $response->updateurl  = $response->updateurl  ?? rtrim($this->url_api, '/') . '/update';
    $response->updatefile = $response->updatefile ?? 'update_kasir.json';

    $tmpDir = FCPATH . ltrim($response->temp, '/');
    if (!is_dir($tmpDir)) @mkdir($tmpDir, 0775, true);

    $installDir = FCPATH . ltrim($response->path, '/');

    $this->_emit($emit, 'Inisialisasi AutoUpdate ...');
    $update = new \VisualAppeal\AutoUpdate($tmpDir, $installDir, (int)$response->time);
    $update->setUpdateUrl($response->updateurl);
    $update->setUpdateFile($response->updatefile);
    $update->setCurrentVersion($this->versi);
    $update->setInstallDir('./');

    // Logger optional
    try {
        $logFile = FCPATH . ltrim($response->logs, '/');
        @mkdir(dirname($logFile), 0775, true);
        $logger = new \Monolog\Logger('update');
        $logger->pushHandler(new \Monolog\Handler\StreamHandler($logFile));
        $update->setLogger($logger);
    } catch (\Throwable $e) {}

    $this->_emit($emit, 'checkUpdate() ...', 10);

    if ($update->checkUpdate() === false) {
        return ['[ERROR] Gagal cek update (checkUpdate false)'];
    }

    if (!$update->newVersionAvailable()) {
        return [
            'Versi saat ini: ' . $this->versi,
            'Aplikasi sudah versi terbaru.'
        ];
    }

    $latest = (string)$update->getLatestVersion();
    $this->_emit($emit, 'Versi terbaru tersedia: ' . $latest, 15);

    // progress callback
    $total = count((array)$update->getVersionsToUpdate());
    $done  = 0;

    $update->onEachUpdateFinish(function ($version, $file) use ($emit, &$done, $total) {
        $done++;
        $percent = 15 + intval(($done / max(1, $total)) * 80);
        $this->_emit($emit, "✔ Install versi {$version} selesai", $percent, 'step');

        if ($this->isStopped()) {
            $this->_emit($emit, 'Update dihentikan user', $percent, 'stop');
            exit;
        }
    });

    $this->_emit($emit, 'Download & install dimulai...', 20);

    // ✅ FIX UTAMA: JANGAN default, karena default = SIMULASI
    // update(false,true) = install beneran + hapus zip di temp
    $result = $update->update(false, true);

    // AutoUpdate bisa return integer error code
    if ($result === false) {
        return ['[ERROR] Proses update gagal (result=false)'];
    }
    if (is_int($result) && $result !== 1) {
        // 1 biasanya true di beberapa kasus, tapi aman kita handle
        return ['[ERROR] Proses update gagal (code=' . $result . '). Cek log: ' . ($response->logs ?? 'temp/update.log')];
    }

    // verifikasi minimal: pastikan install benar-benar terjadi
    $newVer = (string)$update->getLatestVersion();

    // simpan versi ke DB
    try {
        $this->model_app->update('info', ['version' => $newVer], ['id' => 1]);
        $this->versi = $newVer;
    } catch (\Throwable $e) {}

    @unlink(FCPATH . 'cache/update-versions.php.cache');
    @unlink($this->stopFile());

    $this->_emit($emit, 'Update selesai', 100, 'done');

    return [
        'Update selesai.',
        'Versi sekarang: ' . $newVer
    ];
}


    /**
     * Tombol "Cek Versi"
     */
    public function version($response, $emit = null)
    {
        $this->_emit($emit, 'Mulai cek versi...');

        $cfg = (object)[];
        if (is_string($response)) {
            $this->_emit($emit, 'Ambil config dari API (default_api) ...');
            $tmp = $this->default_api($response);
            if (is_array($tmp)) return $tmp;
            if (is_object($tmp)) $cfg = $tmp;
        } elseif (is_object($response)) {
            $cfg = $response;
        }

        // defaults
        $cfg->temp  = (!empty($cfg->temp) ? $cfg->temp : 'temp/update_tmp/');
        $cfg->path  = (isset($cfg->path) && $cfg->path !== null ? $cfg->path : '');
        $cfg->time  = (isset($cfg->time) && $cfg->time !== null && $cfg->time !== '' ? (int)$cfg->time : 60);

        // ✅ pakai cfg statis untuk updateurl/file (lebih aman)
        $static = $this->_getUpdateCfg();
        $cfg->updateurl  = $static->updateurl;
        $cfg->updatefile = $static->updatefile;
        $cfg->logs       = 'temp/update.log';

        $err = '';
        $map = $this->_fetchUpdateMap($cfg->updateurl, $cfg->updatefile, $err);
        if ($map === false) {
            $this->_emit($emit, '[ERROR] ' . $err);
            return ['[ERROR] ' . $err];
        }

        $tmpDir = FCPATH . ltrim($cfg->temp, '/');
        if (!is_dir($tmpDir)) {
            @mkdir($tmpDir, 0775, true);
            $this->_emit($emit, 'Buat folder temp: ' . $tmpDir);
        }

        $installDir = FCPATH . ltrim($cfg->path, '/');
        $this->_emit($emit, 'Init AutoUpdate ...');
        $update = new AutoUpdate($tmpDir, $installDir, (int)$cfg->time);

        $update->setUpdateUrl($cfg->updateurl);
        $update->setUpdateFile($cfg->updatefile);
        $update->setCurrentVersion($this->versi);
        $update->setInstallDir('./');

        try {
            $logFile = FCPATH . ltrim($cfg->logs, '/');
            @mkdir(dirname($logFile), 0775, true);
            $logger = new \Monolog\Logger("default");
            $logger->pushHandler(new \Monolog\Handler\StreamHandler($logFile));
            $update->setLogger($logger);
            $this->_emit($emit, 'Logger aktif: ' . $cfg->logs);
        } catch (\Throwable $e) {
            $this->_emit($emit, 'Logger gagal: ' . $e->getMessage());
        }

        $this->_emit($emit, 'checkUpdate() ...');
        if ($update->checkUpdate() === false) {
            return ['[ERROR] ' . $this->lang->line('check_update')];
        }

        $arr = [];
        $arr[] = '';

        if ($update->newVersionAvailable()) {
            $latest = (string)$update->getLatestVersion();
            $this->_emit($emit, 'Versi baru tersedia: ' . $latest);
            $arr[] = $this->lang->line('current_version_available') . ' ' . $latest;
        } else {
            $this->_emit($emit, 'Tidak ada versi baru. Versi saat ini: ' . $this->versi);
            $arr[] = $this->lang->line('current_version') . ' ' . $this->versi;
            $arr[] = $this->lang->line('already_updated');
        }

        return $arr;
    }

    // ===== PATCH =====
    public function patch($response = null)
    {
        try {
            $cfg = $this->_getUpdateCfg();

            $update = new AutoUpdate($cfg->temp_abs, $cfg->install_abs, (int)$cfg->time);
            $update->setUpdateUrl($cfg->updateurl);
            $update->setUpdateFile($cfg->updatefile);
            $update->setCurrentVersion($this->versi);
            $update->setInstallDir('./');

            try {
                $logger = new \Monolog\Logger("patch");
                $logger->pushHandler(new \Monolog\Handler\StreamHandler($cfg->logs_abs));
                $update->setLogger($logger);
            } catch (\Throwable $e) {}

            if ($update->checkUpdate() === false) return "Tidak ada patch tersedia";
            if (!$update->newVersionAvailable()) return "Patch tidak ditemukan (versi sudah terbaru)";

            return "Patch tersedia untuk versi " . $update->getLatestVersion();
        } catch (\Throwable $e) {
            return "ERROR patch: " . $e->getMessage();
        }
    }

    function install_patch($response = null)
{
    try {
        $cfg = $this->_getUpdateCfg();

        $update = new \VisualAppeal\AutoUpdate(
            $cfg->temp_abs,
            $cfg->install_abs,
            (int) $cfg->time
        );

        $update->setUpdateUrl($cfg->updateurl);
        $update->setUpdateFile($cfg->updatefile);

        // current version dari DB (info.id=1)
        $row = $this->db->where('id', 1)->get('info')->row();
        $current = ($row && $row->version) ? (string)$row->version : '0.0.0';

        $update->setCurrentVersion($current);
        $update->setInstallDir('./');

        if ($update->checkUpdate() === false) {
            return "Tidak ada patch tersedia (checkUpdate false)";
        }
        if (!$update->newVersionAvailable()) {
            return "Patch tidak ditemukan (versi sudah terbaru)";
        }

        $latest = (string)$update->getLatestVersion();

        // ✅ FIX UTAMA: default update() = SIMULASI
        $result = $update->update(false, true);

        if ($result === false) {
            return "ERROR: Proses install patch gagal (result=false)";
        }
        if (is_int($result) && $result !== 1) {
            return "ERROR: Proses install patch gagal (code={$result}). Cek log updater.";
        }

        // jalankan patch DB setelah file patch_db ter-install
        $dbLog = $this->_apply_patch_db(FCPATH);

        // update versi DI AKHIR
        $this->db->where('id', 1)->update('info', ['version' => $latest]);
        $this->versi = $latest;

        @unlink(FCPATH . 'cache/update-versions.php.cache');

        $msg = "Install patch berhasil. Versi sekarang: " . $latest;
        if (!empty($dbLog)) {
            $msg .= "\n\n=== PATCH DB ===\n" . $dbLog;
        } else {
            $msg .= "\n\nPATCH DB: tidak ada (folder patch_db tidak ditemukan).";
        }

        return $msg;

    } catch (\Throwable $e) {
        return "ERROR install patch: " . $e->getMessage();
    }
}


    // ===== PATCH DB RUNNER =====
    private function _apply_patch_db($rootPath)
    {
        $rootPath = rtrim($rootPath, '/\\') . DIRECTORY_SEPARATOR;
        $patchDir = $rootPath . 'patch_db' . DIRECTORY_SEPARATOR;

        if (!is_dir($patchDir)) {
            return "";
        }

        $logs = [];
        $logs[] = "Folder patch_db ditemukan: " . $patchDir;

        $sqlFiles = glob($patchDir . '*.sql');
        if ($sqlFiles && count($sqlFiles) > 0) {
            natsort($sqlFiles);
            $sqlFiles = array_values($sqlFiles);

            foreach ($sqlFiles as $file) {
                $logs[] = "Menjalankan SQL: " . basename($file);
                $logs[] = $this->_run_sql_file($file);
            }
        } else {
            $logs[] = "Tidak ada file .sql di patch_db/";
        }

        // Optional: migration
        try {
            $this->load->library('migration');
            $mig = $this->migration->latest();
            if ($mig === false) {
                $logs[] = "Migration latest(): GAGAL -> " . (string)$this->migration->error_string();
            } else {
                $logs[] = "Migration latest(): OK";
            }
        } catch (\Throwable $e) {
            $logs[] = "Migration latest(): ERROR -> " . $e->getMessage();
        }

        // cleanup
        $this->_rrmdir($patchDir);
        $logs[] = "patch_db dihapus (cleanup).";

        return implode("\n", $logs);
    }

    private function _run_sql_file($filepath)
    {
        if (!is_file($filepath)) return "ERROR: file tidak ditemukan.";

        $sql = file_get_contents($filepath);
        $sql = (string)$sql;
        $sql = preg_replace('/^\xEF\xBB\xBF/', '', $sql);

        $statements = $this->_split_sql_statements($sql);
        if (!$statements) return "OK: tidak ada statement SQL (kosong).";

        $this->db->trans_begin();

        try {
            @$this->db->query("SET FOREIGN_KEY_CHECKS=0");

            $count = 0;
            foreach ($statements as $st) {
                $st = trim($st);
                if ($st === '') continue;
                $this->db->query($st);
                $count++;
            }

            @$this->db->query("SET FOREIGN_KEY_CHECKS=1");

            if ($this->db->trans_status() === false) {
                $this->db->trans_rollback();
                return "GAGAL: transaksi SQL rollback.";
            }

            $this->db->trans_commit();
            return "OK: " . $count . " statement dieksekusi.";
        } catch (\Throwable $e) {
            $this->db->trans_rollback();
            return "ERROR SQL: " . $e->getMessage();
        }
    }

    private function _split_sql_statements($sql)
    {
        $lines = preg_split("/(\r\n|\n|\r)/", $sql);
        $clean = [];
        foreach ($lines as $line) {
            $trim = ltrim($line);
            if (strpos($trim, '--') === 0) continue;
            if (strpos($trim, '#') === 0) continue;
            $clean[] = $line;
        }
        $sql = implode("\n", $clean);

        $sql = preg_replace('!/\*.*?\*/!s', '', $sql);

        $parts = preg_split('/;\s*[\r\n]+|;\s*$/m', $sql);
        $out = [];
        foreach ($parts as $p) {
            $p = trim($p);
            if ($p !== '') $out[] = $p;
        }
        return $out;
    }

    private function _rrmdir($dir)
    {
        if (!is_dir($dir)) return;

        $items = scandir($dir);
        if (!$items) return;

        foreach ($items as $item) {
            if ($item === '.' || $item === '..') continue;
            $path = $dir . DIRECTORY_SEPARATOR . $item;
            if (is_dir($path)) $this->_rrmdir($path);
            else @unlink($path);
        }
        @rmdir($dir);
    }

    // ===== PRO / CUSTOM (tetap seperti punyamu) =====
    public function versipro($response)
    {
        $versipro = $this->pro != '' ? $this->pro : '0.0.0';

        $update = new AutoUpdate(FCPATH . $response->temp, FCPATH . $response->path, (int)$response->time);
        $update->setUpdateUrl($response->updateurl);
        $update->setUpdateFile($response->updatefile);
        $update->setCurrentVersion($versipro);
        $update->setInstallDir('./');
        $update->setBasicAuth($response->user, $response->auth);

        $logger = new \Monolog\Logger("default");
        $logger->pushHandler(new \Monolog\Handler\StreamHandler(FCPATH . $response->logs));
        $update->setLogger($logger);

        if ($update->checkUpdate() === false) {
            die($this->lang->line('check_update'));
        }

        $arr = [];

        if ($update->newVersionAvailable()) {
            $latest = (string)$update->getLatestVersion();
            $arr[] = $this->lang->line('current_version_available') . " {$latest} | " .
                     $this->lang->line('current_version') . " {$versipro}";

            if (!empty($response->readme)) {
                $arr[] = $this->lang->line('read_instructions') . ' ' . (string)$response->readme;
            } else {
                $arr[] = $this->lang->line('read_instructions') . ' README';
            }

            if (!empty($response->update)) {
                $arr[] = $this->lang->line('commands') . '> ' . (string)$response->update . ' ' . $this->lang->line('app_updates');
            } else {
                $arr[] = $this->lang->line('commands') . '> UPDATE PRO';
            }
        } else {
            $arr[] = $this->lang->line('latest_current_version') . ' : ' . $versipro;
        }

        return $arr;
    }

    public function updatepro($response)
    {
        if ($response->status == 'custom') {
            $versi = $this->custom != '' ? $this->custom : '0.0.0';
            $colom = 'versi_custom';
            $label = 'CUSTOM';
        } else {
            $versi = $this->pro != '' ? $this->pro : '0.0.0';
            $colom = 'versi_pro';
            $label = 'PRO';
        }

        $update = new AutoUpdate(FCPATH . $response->temp, FCPATH . $response->path, (int)$response->time);
        $update->setUpdateUrl($response->updateurl);
        $update->setUpdateFile($response->updatefile);
        $update->setCurrentVersion($versi);
        $update->setInstallDir('./');
        $update->setBasicAuth($response->user, $response->auth);

        $logger = new \Monolog\Logger("default");
        $logger->pushHandler(new \Monolog\Handler\StreamHandler(FCPATH . $response->logs));
        $update->setLogger($logger);

        if ($update->checkUpdate() === false) {
            die($this->lang->line('check_update'));
        }

        $arr = [];

        if ($update->newVersionAvailable()) {
            $latest = (string)$update->getLatestVersion();
            $arr[] = "VERSI BARU {$label}: {$latest}";
            $arr[] = "";

            $versionsToUpdate = array_map(function ($v) {
                return (string)$v;
            }, (array)$update->getVersionsToUpdate());

            $arr[] = "MEMPROSES PEMBARUAN:";
            foreach ($versionsToUpdate as $v) $arr[] = "- " . $v;

            $f = @fopen(FCPATH . $response->logs, 'rb+');
            if ($f !== false) {
                ftruncate($f, 0);
                fclose($f);
            }

            $update->onEachUpdateFinish(function ($updatedVersion) use (&$arr) {
                $arr[] = "Update selesai versi: " . (string)$updatedVersion;
            });

            $update->setOnAllUpdateFinishCallbacks(function ($updatedVersions) use (&$arr) {
                $arr[] = "Semua update selesai:";
                foreach ((array)$updatedVersions as $v) $arr[] = "- " . (string)$v;
            });

            $result = $update->update(false);

            if ($result === true) {
                $this->model_app->update('info', [$colom => $update->getLatestVersion()], ['id' => 1]);
                $arr[] = "";
                $arr[] = "UPDATE SUKSES";
                $arr[] = "Versi sekarang: " . (string)$update->getLatestVersion();
            } else {
                $arr[] = "UPDATE GAGAL: " . (string)$result;
                if ($result == AutoUpdate::ERROR_SIMULATE) {
                    $sim = $update->getSimulationResults();
                    if (is_array($sim)) foreach ($sim as $line) $arr[] = (string)$line;
                    else $arr[] = (string)$sim;
                }
            }
        } else {
            $arr[] = "Tidak ada update. Versi {$label} sudah terbaru: " . $versi;
        }

        return $arr;
    }

    public function demo($response)
    {
        $update = $this->model_app->update($response->table, [$response->status => $response->fields], ['id' => 1]);

        if (($update['status'] ?? '') == 'ok') {
            $arr[] = $response->message;
        } else {
            $arr[] = $this->lang->line('update_failed');
        }
        return $arr;
    }

    private function stopFile()
    {
        return FCPATH . 'temp/update.stop';
    }

    private function isStopped()
    {
        return file_exists($this->stopFile());
    }

    public function check_update_status()
    {
        $current = (string)$this->versi;
        $latest  = $current;

        try {
            $cfg = $this->_getUpdateCfg();

            $update = new AutoUpdate($cfg->temp_abs, $cfg->install_abs, (int)$cfg->time);
            $update->setUpdateUrl($cfg->updateurl);
            $update->setUpdateFile($cfg->updatefile);
            $update->setCurrentVersion($current);
            $update->setInstallDir('./');

            if ($update->checkUpdate() === false) {
                return ['available' => false, 'current' => $current, 'latest' => $current, 'error' => 'Gagal cek update'];
            }

            if ($update->newVersionAvailable()) {
                $latest = (string)$update->getLatestVersion();
                return ['available' => true, 'current' => $current, 'latest' => $latest, 'error' => null];
            }

            return ['available' => false, 'current' => $current, 'latest' => $current, 'error' => null];
        } catch (\Throwable $e) {
            return ['available' => false, 'current' => $current, 'latest' => $current, 'error' => $e->getMessage()];
        }
    }

    public function get_latest_version_only()
    {
        $cfg = $this->_getUpdateCfg();

        try {
            $update = new AutoUpdate($cfg->temp_abs, $cfg->install_abs, (int)$cfg->time);
            $update->setUpdateUrl($cfg->updateurl);
            $update->setUpdateFile($cfg->updatefile);
            $update->setCurrentVersion($this->versi);
            $update->setInstallDir('./');

            if ($update->checkUpdate() === false) return null;
            if ($update->newVersionAvailable()) return (string)$update->getLatestVersion();
            return null;
        } catch (\Throwable $e) {
            return null;
        }
    }

    private function _fetchUpdateMap($updateUrl, $updateFile, &$err = '')
    {
        $err = '';
        $url = rtrim($updateUrl, '/') . '/' . ltrim($updateFile, '/');

        $ctx = stream_context_create([
            'http' => [
                'timeout' => 10,
                'header'  => "User-Agent: POS-Kasir-Updater\r\n"
            ],
            'ssl' => [
                'verify_peer'       => $this->ssl_verify,
                'verify_peer_name'  => $this->ssl_verify,
                'allow_self_signed' => !$this->ssl_verify
            ],
        ]);

        $raw = @file_get_contents($url, false, $ctx);
        if ($raw === false) {
            $err = "Gagal ambil update file: $url";
            return false;
        }

        $map = json_decode($raw, true);
        if (!is_array($map)) {
            $err = "JSON tidak valid di $url (json_last_error=" . json_last_error() . ")";
            return false;
        }

        if (count($map) === 0) {
            $err = "JSON kosong di $url (tidak ada versi)";
            return false;
        }

        return $map;
    }
}
