<?php

namespace App\Services;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

/**
 * Whole-database backup and restore for the SaaS control panel. Works
 * against whichever connection is actually configured (sqlite locally,
 * mysql in production) — sqlite is a straight file copy; mysql shells out
 * to mysqldump/mysql, which need to be on the server's PATH (standard on
 * any box that also has a MySQL server, but worth checking on deploy).
 *
 * Only ever reachable behind the saas.root middleware — restore in
 * particular replaces the entire database, so this is real trusted-operator
 * power, not something exposed anywhere near a company admin.
 */
class BackupService
{
    private const DIR = 'backups';

    public function list(): Collection
    {
        Storage::disk('local')->makeDirectory(self::DIR);

        return collect(Storage::disk('local')->files(self::DIR))
            ->filter(fn($path) => preg_match('/\.(sqlite|sql)$/', $path))
            ->sortByDesc(fn($path) => Storage::disk('local')->lastModified($path))
            ->map(fn($path) => [
                'name' => basename($path),
                'size' => Storage::disk('local')->size($path),
                'modified' => now()->createFromTimestamp(Storage::disk('local')->lastModified($path)),
            ])
            ->values();
    }

    public function create(): string
    {
        Storage::disk('local')->makeDirectory(self::DIR);

        $timestamp = $this->timestamp();

        if (config('database.default') === 'sqlite') {
            $filename = "backup_{$timestamp}.sqlite";
            copy(config('database.connections.sqlite.database'), $this->fullPath($filename));

            return $filename;
        }

        $db = config('database.connections.mysql');
        $filename = "backup_{$timestamp}.sql";

        $result = Process::timeout(300)->run([
            'mysqldump',
            '-h', $db['host'],
            '-P', (string) $db['port'],
            '-u', $db['username'],
            '--password=' . $db['password'],
            $db['database'],
        ]);

        if (!$result->successful()) {
            throw new \RuntimeException('mysqldump failed: ' . $result->errorOutput());
        }

        file_put_contents($this->fullPath($filename), $result->output());

        return $filename;
    }

    /**
     * Overwrites the live database with an uploaded backup. A safety copy
     * of whatever was there immediately before is always taken first —
     * this is the one operation in the whole app that can destroy data in
     * one click, so an accidental wrong-file upload is still recoverable.
     */
    public function restore(UploadedFile $file): void
    {
        if (config('database.default') === 'sqlite') {
            $dbPath = config('database.connections.sqlite.database');
            copy($dbPath, $dbPath . '.before-restore-' . $this->timestamp());
            copy($file->getRealPath(), $dbPath);

            return;
        }

        $this->create(); // safety copy of the current database, timestamped by create() itself

        $db = config('database.connections.mysql');
        $result = Process::timeout(300)->input(file_get_contents($file->getRealPath()))->run([
            'mysql',
            '-h', $db['host'],
            '-P', (string) $db['port'],
            '-u', $db['username'],
            '--password=' . $db['password'],
            $db['database'],
        ]);

        if (!$result->successful()) {
            throw new \RuntimeException('mysql restore failed: ' . $result->errorOutput());
        }
    }

    public function delete(string $filename): void
    {
        Storage::disk('local')->delete(self::DIR . '/' . $this->assertSafeFilename($filename));
    }

    public function path(string $filename): string
    {
        return $this->fullPath($this->assertSafeFilename($filename));
    }

    private function fullPath(string $filename): string
    {
        return storage_path('app/' . self::DIR . '/' . $filename);
    }

    /**
     * Second-resolution timestamps collide when two backups are triggered
     * within the same second (two quick clicks of "Create backup now", or
     * a manual click landing in the same second as the scheduled run) —
     * the second one would silently overwrite the first rather than create
     * a distinct file. The random suffix is what actually guarantees
     * uniqueness; the timestamp is there for humans reading the list.
     */
    private function timestamp(): string
    {
        return now()->format('Y-m-d_His') . '_' . Str::random(6);
    }

    /**
     * The filename comes straight from a route parameter — reject anything
     * that isn't exactly the shape create() itself produces before it ever
     * touches a filesystem path, closing off path traversal entirely
     * rather than trying to sanitize it.
     */
    private function assertSafeFilename(string $filename): string
    {
        if (!preg_match('/^backup_[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{6}_[a-zA-Z0-9]{6}\.(sqlite|sql)$/', $filename)) {
            abort(400, 'Invalid backup filename.');
        }

        return $filename;
    }
}
