<?php

namespace App\Services;

use App\Models\SystemBackup;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;

class SystemBackupService
{
    /**
     * @return array<int, string>
     */
    private function backupTables(): array
    {
        $skip = ['migrations', 'failed_jobs', 'jobs', 'job_batches', 'password_reset_tokens', 'personal_access_tokens'];

        return collect(Schema::getTableListing())
            ->filter(fn (string $table) => ! in_array($table, $skip, true))
            ->values()
            ->all();
    }

    public function create(string $type = 'full', ?int $createdBy = null): SystemBackup
    {
        $tables = $this->backupTables();
        $payload = [
            'generated_at' => now()->toDateTimeString(),
            'db_connection' => config('database.default'),
            'type' => $type,
            'tables' => [],
        ];

        foreach ($tables as $table) {
            $payload['tables'][$table] = DB::table($table)->get()->map(fn ($row) => (array) $row)->all();
        }

        $json = json_encode($payload, JSON_UNESCAPED_UNICODE);
        $gzip = gzencode((string) $json, 9);
        $fileName = 'backups/system-backup-'.now()->format('Ymd-His').'.json.gz';

        Storage::disk('local')->put($fileName, $gzip);
        $size = Storage::disk('local')->size($fileName);

        return SystemBackup::query()->create([
            'backup_type' => $type,
            'file_path' => $fileName,
            'file_size' => (int) $size,
            'status' => 'created',
            'created_by' => $createdBy,
            'meta' => ['tables' => count($tables)],
        ]);
    }

    public function restore(SystemBackup $backup, ?int $restoredBy = null): void
    {
        if (! Storage::disk('local')->exists($backup->file_path)) {
            throw new \RuntimeException('Backup file not found.');
        }

        $raw = Storage::disk('local')->get($backup->file_path);
        $decoded = json_decode((string) gzdecode($raw), true);
        if (! is_array($decoded) || ! isset($decoded['tables']) || ! is_array($decoded['tables'])) {
            throw new \RuntimeException('Invalid backup file format.');
        }

        $tables = array_keys($decoded['tables']);
        $ordered = $this->restoreOrder($tables);

        DB::transaction(function () use ($decoded, $ordered): void {
            $driver = DB::connection()->getDriverName();
            if ($driver === 'mysql') {
                DB::statement('SET FOREIGN_KEY_CHECKS=0');
            } elseif ($driver === 'sqlite') {
                DB::statement('PRAGMA foreign_keys = OFF');
            }

            foreach ($ordered as $table) {
                if (! Schema::hasTable($table)) {
                    continue;
                }

                DB::table($table)->truncate();
                $rows = $decoded['tables'][$table] ?? [];
                if (! is_array($rows) || $rows === []) {
                    continue;
                }

                foreach (array_chunk($rows, 200) as $chunk) {
                    DB::table($table)->insert($chunk);
                }
            }

            if ($driver === 'mysql') {
                DB::statement('SET FOREIGN_KEY_CHECKS=1');
            } elseif ($driver === 'sqlite') {
                DB::statement('PRAGMA foreign_keys = ON');
            }
        });

        $backup->status = 'restored';
        $backup->restored_by = $restoredBy;
        $backup->restored_at = now();
        $backup->save();
    }

    /**
     * @param  array<int, string>  $tables
     * @return array<int, string>
     */
    private function restoreOrder(array $tables): array
    {
        $priority = [
            'roles',
            'permissions',
            'settings',
            'ranks',
            'plans',
            'products',
            'users',
            'user_roles',
            'member_tree_closure',
            'wallet_balances',
            'orders',
        ];

        usort($tables, function (string $a, string $b) use ($priority): int {
            $aIndex = array_search($a, $priority, true);
            $bIndex = array_search($b, $priority, true);
            $aScore = $aIndex === false ? 999 : $aIndex;
            $bScore = $bIndex === false ? 999 : $bIndex;

            if ($aScore !== $bScore) {
                return $aScore <=> $bScore;
            }

            return strcmp($a, $b);
        });

        return $tables;
    }
}
