<?php
namespace App\Services;

use App\Core\Audit;
use App\Core\Auth;
use App\Core\Config;
use App\Core\Database;
use App\Core\Dates;

/**
 * Pencadangan & pemulihan basis data.
 *
 * Memakai mysqldump bila tersedia; bila tidak (umum pada shared hosting),
 * memakai penulis SQL internal berbasis PDO sehingga tetap berfungsi.
 */
class BackupService
{
    public static function dir(): string
    {
        $d = (string)Config::get('paths.backups', dirname(__DIR__, 2) . '/storage/backups');
        if (!is_dir($d)) @mkdir($d, 0775, true);
        return rtrim($d, '/');
    }

    public static function create(string $type = 'manual'): array
    {
        $cfg = Config::get('db');
        $name = 'backup_' . ($cfg['database'] ?? 'db') . '_' . date('Ymd_His') . '.sql';
        $path = self::dir() . '/' . $name;

        $ok = self::viaMysqldump($cfg, $path);
        $method = 'mysqldump';
        if (!$ok) { $ok = self::viaPdo($path); $method = 'internal'; }

        $size = $ok && is_file($path) ? (int)filesize($path) : 0;
        Database::i()->insert('backup_logs', [
            'file_name' => $name, 'file_size' => $size,
            'type' => $type === 'otomatis' ? 'otomatis' : 'manual',
            'status' => $ok ? 'sukses' : 'gagal',
            'message' => $ok ? ('Metode: ' . $method) : 'Pencadangan gagal dijalankan.',
            'created_by' => Auth::id() ?: null,
        ]);
        if ($ok) {
            Audit::log('backup', 'backup', null, null, 'Pencadangan basis data dibuat: ' . $name);
            self::prune();
        }
        return ['sukses' => $ok, 'file' => $name, 'path' => $path, 'ukuran' => $size, 'metode' => $method];
    }

    private static function viaMysqldump(array $cfg, string $path): bool
    {
        if (!function_exists('exec')) return false;
        $disabled = array_map('trim', explode(',', (string)ini_get('disable_functions')));
        if (in_array('exec', $disabled, true)) return false;

        foreach (['mysqldump', '/usr/bin/mysqldump', '/usr/local/bin/mysqldump', '/usr/local/mysql/bin/mysqldump'] as $bin) {
            $cmd = sprintf(
                '%s --host=%s --port=%d --user=%s --password=%s --single-transaction --quick --skip-lock-tables --default-character-set=utf8mb4 %s > %s 2>/dev/null',
                escapeshellcmd($bin),
                escapeshellarg((string)$cfg['host']), (int)($cfg['port'] ?? 3306),
                escapeshellarg((string)$cfg['username']), escapeshellarg((string)$cfg['password']),
                escapeshellarg((string)$cfg['database']), escapeshellarg($path)
            );
            @exec($cmd, $out, $code);
            if ($code === 0 && is_file($path) && filesize($path) > 1024) return true;
            @unlink($path);
        }
        return false;
    }

    /** Penulis dump SQL internal (tanpa exec). */
    private static function viaPdo(string $path): bool
    {
        $db = Database::i();
        $fh = @fopen($path, 'w');
        if (!$fh) return false;
        try {
            fwrite($fh, "-- SIPIUTANG backup internal\n-- Dibuat: " . Dates::now() . "\nSET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS=0;\n\n");
            $tables = $db->column('SHOW TABLES');
            foreach ($tables as $t) {
                $create = $db->one('SHOW CREATE TABLE `' . str_replace('`', '', (string)$t) . '`');
                $ddl = $create['Create Table'] ?? ($create['Create View'] ?? null);
                if (!$ddl) continue;
                fwrite($fh, "DROP TABLE IF EXISTS `$t`;\n$ddl;\n\n");

                $count = (int)$db->value("SELECT COUNT(*) FROM `$t`");
                if ($count === 0) continue;
                $offset = 0; $chunk = 500;
                while ($offset < $count) {
                    $rows = $db->all("SELECT * FROM `$t` LIMIT $chunk OFFSET $offset");
                    if (!$rows) break;
                    $cols = array_keys($rows[0]);
                    $values = [];
                    foreach ($rows as $r) {
                        $vals = [];
                        foreach ($cols as $c) {
                            $v = $r[$c];
                            if ($v === null) $vals[] = 'NULL';
                            elseif (is_int($v) || is_float($v)) $vals[] = (string)$v;
                            else $vals[] = $db->pdo()->quote((string)$v);
                        }
                        $values[] = '(' . implode(',', $vals) . ')';
                    }
                    fwrite($fh, 'INSERT INTO `' . $t . '` (`' . implode('`,`', $cols) . '`) VALUES ' . implode(",\n", $values) . ";\n");
                    $offset += $chunk;
                }
                fwrite($fh, "\n");
            }
            fwrite($fh, "SET FOREIGN_KEY_CHECKS=1;\n");
            fclose($fh);
            return filesize($path) > 512;
        } catch (\Throwable $e) {
            fclose($fh);
            @unlink($path);
            \App\Core\Logger::error('Backup internal gagal: ' . $e->getMessage());
            return false;
        }
    }

    public static function listFiles(): array
    {
        $out = [];
        foreach (glob(self::dir() . '/*.sql') ?: [] as $f) {
            $out[] = ['nama' => basename($f), 'ukuran' => (int)filesize($f), 'waktu' => date('Y-m-d H:i:s', (int)filemtime($f)), 'path' => $f];
        }
        usort($out, fn($a, $b) => strcmp($b['waktu'], $a['waktu']));
        return $out;
    }

    public static function delete(string $name): bool
    {
        $name = basename($name);
        $p = self::dir() . '/' . $name;
        if (!is_file($p) || !str_ends_with($name, '.sql')) return false;
        @unlink($p);
        Audit::log('backup', 'delete', null, null, 'Berkas pencadangan dihapus: ' . $name);
        return true;
    }

    /** Menghapus berkas lama melebihi batas simpan. */
    public static function prune(): int
    {
        $max = \App\Core\Settings::int('backup.simpan_maks_berkas', 10);
        $files = self::listFiles();
        $n = 0;
        foreach (array_slice($files, $max) as $f) { @unlink($f['path']); $n++; }
        return $n;
    }

    /** Memulihkan basis data dari berkas SQL. */
    public static function restore(string $name): array
    {
        $name = basename($name);
        $path = self::dir() . '/' . $name;
        if (!is_file($path)) return ['sukses' => false, 'pesan' => 'Berkas pencadangan tidak ditemukan.'];

        $db = Database::i();
        $sql = (string)file_get_contents($path);
        $statements = self::splitSql($sql);
        $n = 0;
        try {
            $db->pdo()->exec('SET FOREIGN_KEY_CHECKS=0');
            foreach ($statements as $st) {
                $st = trim($st);
                if ($st === '' || str_starts_with($st, '--') || str_starts_with($st, '/*')) continue;
                $db->pdo()->exec($st);
                $n++;
            }
            $db->pdo()->exec('SET FOREIGN_KEY_CHECKS=1');
        } catch (\Throwable $e) {
            return ['sukses' => false, 'pesan' => 'Pemulihan gagal pada pernyataan ke-' . ($n + 1) . ': ' . $e->getMessage()];
        }
        Audit::log('backup', 'restore_db', null, null, 'Basis data dipulihkan dari ' . $name);
        return ['sukses' => true, 'pesan' => "Pemulihan selesai. $n pernyataan SQL dijalankan.", 'jumlah' => $n];
    }

    /** Memecah skrip SQL menjadi pernyataan (menghormati string & komentar). */
    private static function splitSql(string $sql): array
    {
        $out = [];
        $buf = '';
        $inStr = false; $q = '';
        $len = strlen($sql);
        for ($i = 0; $i < $len; $i++) {
            $ch = $sql[$i];
            if ($inStr) {
                $buf .= $ch;
                if ($ch === '\\') { if ($i + 1 < $len) { $buf .= $sql[$i + 1]; $i++; } continue; }
                if ($ch === $q) $inStr = false;
                continue;
            }
            if ($ch === "'" || $ch === '"' || $ch === '`') { $inStr = true; $q = $ch; $buf .= $ch; continue; }
            if ($ch === '-' && $i + 1 < $len && $sql[$i + 1] === '-' && ($buf === '' || str_ends_with($buf, "\n"))) {
                while ($i < $len && $sql[$i] !== "\n") $i++;
                continue;
            }
            if ($ch === ';') { $out[] = $buf; $buf = ''; continue; }
            $buf .= $ch;
        }
        if (trim($buf) !== '') $out[] = $buf;
        return $out;
    }
}
