<?php

namespace App\Services;

use ZipArchive;

class BackupService
{
    private const MAX_BACKUPS = 10;

    public function createFullBackup(): string
    {
        if (!class_exists(ZipArchive::class)) {
            throw new \RuntimeException('Ekstensi PHP ZipArchive belum tersedia.');
        }

        $backupDir = WRITEPATH . 'backups';
        if (!is_dir($backupDir) && !mkdir($backupDir, 0750, true) && !is_dir($backupDir)) {
            throw new \RuntimeException('Folder backup tidak dapat dibuat.');
        }

        $filename = 'simak_full_backup_' . date('Y-m-d_H-i-s') . '.zip';
        $zipPath = $backupDir . DIRECTORY_SEPARATOR . $filename;
        $zip = new ZipArchive();

        if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            throw new \RuntimeException('File ZIP backup tidak dapat dibuat.');
        }

        try {
            $zip->addFromString('database/' . $this->databaseName() . '.sql', $this->buildSql());
            $this->addDirectory($zip, FCPATH . 'uploads', 'public/uploads');
            $this->addDirectory($zip, WRITEPATH . 'uploads', 'writable/uploads');

            $zip->addFromString('backup-manifest.json', json_encode([
                'application' => 'SIMAK',
                'created_at'  => date('c'),
                'database'    => $this->databaseName(),
                'contents'    => ['database', 'public/uploads', 'writable/uploads'],
            ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
            $zip->close();
        } catch (\Throwable $e) {
            $zip->close();
            @unlink($zipPath);
            throw $e;
        }

        $this->pruneBackups();
        return $zipPath;
    }

    public function listBackups(): array
    {
        $files = glob(WRITEPATH . 'backups/simak_full_backup_*.zip') ?: [];
        usort($files, static fn(string $a, string $b): int => filemtime($b) <=> filemtime($a));

        return array_map(static function (string $path): array {
            return [
                'name'     => basename($path),
                'size'     => filesize($path) ?: 0,
                'modified' => filemtime($path) ?: 0,
            ];
        }, $files);
    }

    public function getBackupPath(string $filename): ?string
    {
        if (!preg_match('/\Asimak_full_backup_[0-9_-]+\.zip\z/', $filename)) {
            return null;
        }

        $path = WRITEPATH . 'backups' . DIRECTORY_SEPARATOR . $filename;
        return is_file($path) ? $path : null;
    }

    public function deleteBackup(string $filename): bool
    {
        $path = $this->getBackupPath($filename);
        return $path !== null && unlink($path);
    }

    private function pruneBackups(): void
    {
        $files = glob(WRITEPATH . 'backups/simak_full_backup_*.zip') ?: [];
        usort($files, static fn(string $a, string $b): int => filemtime($b) <=> filemtime($a));

        foreach (array_slice($files, self::MAX_BACKUPS) as $file) {
            @unlink($file);
        }
    }

    private function addDirectory(ZipArchive $zip, string $source, string $prefix): void
    {
        if (!is_dir($source)) {
            return;
        }

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($source, \FilesystemIterator::SKIP_DOTS)
        );

        foreach ($iterator as $file) {
            if (!$file->isFile()) {
                continue;
            }

            $relative = ltrim(str_replace($source, '', $file->getPathname()), DIRECTORY_SEPARATOR);
            $zip->addFile($file->getPathname(), $prefix . '/' . str_replace(DIRECTORY_SEPARATOR, '/', $relative));
        }
    }

    private function buildSql(): string
    {
        $db = db_connect();
        $sql = "-- SIMAK Full Database Backup\n";
        $sql .= '-- Generated: ' . date('Y-m-d H:i:s') . "\n";
        $sql .= '-- Database: ' . $this->databaseName() . "\n\n";
        $sql .= "SET FOREIGN_KEY_CHECKS=0;\nSET SQL_MODE='NO_AUTO_VALUE_ON_ZERO';\n\n";

        $tables = $db->query('SHOW TABLES')->getResultArray();
        if (empty($tables)) {
            throw new \RuntimeException('Tidak ada tabel yang dapat dibackup.');
        }

        $key = array_key_first($tables[0]);
        foreach ($tables as $tableRow) {
            $table = $tableRow[$key];
            $quotedTable = $db->protectIdentifiers($table);
            $create = $db->query("SHOW CREATE TABLE {$quotedTable}")->getRowArray();
            $createSql = $create['Create Table'] ?? $create['Create View'] ?? null;
            if ($createSql === null) {
                continue;
            }

            $sql .= "DROP TABLE IF EXISTS {$quotedTable};\n" . $createSql . ";\n";
            $rows = $db->query("SELECT * FROM {$quotedTable}")->getResultArray();
            if ($rows !== []) {
                $columns = array_map([$db, 'protectIdentifiers'], array_keys($rows[0]));
                $sql .= 'INSERT INTO ' . $quotedTable . ' (' . implode(', ', $columns) . ") VALUES\n";
                $values = [];
                foreach ($rows as $row) {
                    $values[] = '(' . implode(', ', array_map(fn($value) => $value === null ? 'NULL' : $db->escape($value), $row)) . ')';
                }
                $sql .= implode(",\n", $values) . ";\n";
            }
            $sql .= "\n";
        }

        return $sql . "SET FOREIGN_KEY_CHECKS=1;\n";
    }

    private function databaseName(): string
    {
        return (string) config('Database')->default['database'];
    }
}
