<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Notifications\BackupFailed;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
use Symfony\Component\Process\Process;

/**
 * Run nightly backups for the W·Dot platform.
 *
 *  --type=db|files|code|all  (default: all)
 *  --rotate=N                (default: 7, days to retain)
 */
class BackupCommand extends Command
{
    protected $signature = 'backup:run {--type=all} {--rotate=7}';

    protected $description = 'Create DB / files / code backups with rotation.';

    private const ROOT = '/www/backup';

    public function handle(): int
    {
        $type = (string) $this->option('type');
        $rotate = (int) $this->option('rotate');
        $stamp = Carbon::now()->format('Y-m-d-H-i');
        $errors = [];

        $this->ensureDirs();

        $types = $type === 'all' ? ['db', 'files', 'code'] : [$type];

        foreach ($types as $t) {
            try {
                match ($t) {
                    'db' => $this->backupDb($stamp),
                    'files' => $this->backupFiles($stamp),
                    'code' => $this->backupCode($stamp),
                    default => throw new \InvalidArgumentException("Unknown type: {$t}"),
                };
            } catch (\Throwable $e) {
                $errors[] = "{$t}: " . $e->getMessage();
                $this->error("[backup:{$t}] " . $e->getMessage());
                Log::error('backup.failed', ['type' => $t, 'error' => $e->getMessage()]);
            }
        }

        $this->rotate($rotate);

        if (! empty($errors)) {
            $this->notifyFailure($errors);

            return self::FAILURE;
        }

        $this->components->info("Backup completed at {$stamp}");

        return self::SUCCESS;
    }

    private function ensureDirs(): void
    {
        foreach (['db', 'files', 'code'] as $kind) {
            $path = self::ROOT . "/easybox-{$kind}";
            if (! is_dir($path)) {
                @mkdir($path, 0750, true);
            }
        }
    }

    private function backupDb(string $stamp): void
    {
        $db = (string) config('database.connections.mysql.database');
        $user = (string) config('database.connections.mysql.username');
        $pass = (string) config('database.connections.mysql.password');
        $host = (string) config('database.connections.mysql.host');
        $port = (string) config('database.connections.mysql.port');

        $out = self::ROOT . "/easybox-db/{$stamp}.sql.gz";
        $tmp = self::ROOT . "/easybox-db/.{$stamp}.tmp";

        $cmd = sprintf(
            'mysqldump --single-transaction --quick --routines --triggers -h%s -P%s -u%s %s %s | gzip > %s',
            escapeshellarg($host),
            escapeshellarg($port),
            escapeshellarg($user),
            $pass !== '' ? '-p' . escapeshellarg($pass) : '',
            escapeshellarg($db),
            escapeshellarg($tmp),
        );

        $proc = Process::fromShellCommandline($cmd);
        $proc->setTimeout(3600);
        $proc->mustRun();

        rename($tmp, $out);
        chmod($out, 0640);

        $size = $this->humanSize(filesize($out) ?: 0);
        $this->line("  [db] {$out} ({$size})");
    }

    private function backupFiles(string $stamp): void
    {
        $out = self::ROOT . "/easybox-files/{$stamp}.tar.gz";
        $base = base_path();

        $cmd = sprintf(
            'tar czf %s -C %s storage/app storage/install.lock .env public/uploads 2>/dev/null || true',
            escapeshellarg($out),
            escapeshellarg($base),
        );

        $proc = Process::fromShellCommandline($cmd);
        $proc->setTimeout(1800);
        $proc->mustRun();

        if (! file_exists($out)) {
            throw new \RuntimeException('files archive not produced');
        }
        chmod($out, 0640);

        $size = $this->humanSize(filesize($out) ?: 0);
        $this->line("  [files] {$out} ({$size})");
    }

    private function backupCode(string $stamp): void
    {
        $out = self::ROOT . "/easybox-code/{$stamp}.tar.gz";
        $root = dirname(base_path()); // /www/wwwroot/easybox

        $excludes = [
            '--exclude=node_modules',
            '--exclude=vendor',
            '--exclude=dist',
            '--exclude=.git',
            '--exclude=storage/logs',
            '--exclude=storage/framework/cache',
            '--exclude=storage/framework/sessions',
            '--exclude=storage/framework/views',
        ];

        $targets = [];
        foreach (['backend', 'apps', 'packages', 'docker-compose.yml'] as $t) {
            if (file_exists($root . '/' . $t)) {
                $targets[] = $t;
            }
        }
        if (empty($targets)) {
            throw new \RuntimeException('no code targets found under ' . $root);
        }

        $cmd = sprintf(
            'tar czf %s -C %s %s %s',
            escapeshellarg($out),
            escapeshellarg($root),
            implode(' ', $excludes),
            implode(' ', array_map('escapeshellarg', $targets)),
        );

        $proc = Process::fromShellCommandline($cmd);
        $proc->setTimeout(3600);
        $proc->mustRun();

        chmod($out, 0640);
        $size = $this->humanSize(filesize($out) ?: 0);
        $this->line("  [code] {$out} ({$size})");
    }

    private function rotate(int $days): void
    {
        if ($days <= 0) {
            return;
        }

        foreach (['db', 'files', 'code'] as $kind) {
            $dir = self::ROOT . "/easybox-{$kind}";
            if (! is_dir($dir)) {
                continue;
            }
            $cmd = sprintf('find %s -type f -mtime +%d -delete', escapeshellarg($dir), $days);
            Process::fromShellCommandline($cmd)->setTimeout(120)->run();
        }
    }

    private function notifyFailure(array $errors): void
    {
        try {
            $slack = config('services.slack.webhook_url');
            $mail = config('services.ops.alert_email');

            $recipients = [];
            if ($slack) {
                $recipients[] = (new \Illuminate\Notifications\AnonymousNotifiable)
                    ->route('slack', $slack);
            }
            if ($mail) {
                $recipients[] = (new \Illuminate\Notifications\AnonymousNotifiable)
                    ->route('mail', $mail);
            }
            if (! empty($recipients)) {
                Notification::send($recipients, new BackupFailed($errors));
            }
        } catch (\Throwable $e) {
            Log::error('backup.notify.failed', ['error' => $e->getMessage()]);
        }
    }

    private function humanSize(int $bytes): string
    {
        $u = ['B', 'KB', 'MB', 'GB', 'TB'];
        $i = 0;
        while ($bytes >= 1024 && $i < count($u) - 1) {
            $bytes /= 1024;
            $i++;
        }

        return sprintf('%.2f %s', $bytes, $u[$i]);
    }
}
