<?php

namespace WMS\Services;

/**
 * BackupService - Respaldo manual del sistema y la base de datos.
 *
 * Genera dos tipos de respaldo dentro de /var/www/WMS/backups:
 *   - Base de datos:  wms_pro_YYYYMMDD_HHMM.sql        (mysqldump)
 *   - Sistema (app):  wms_system_YYYYMMDD_HHMM.tar.gz  (src, public, scripts, database)
 *
 * El cron diario (scripts/backup.sh) sigue generando los .sql automaticos a las 02:00;
 * este servicio cubre el respaldo MANUAL disparado por el admin desde la interfaz.
 *
 * Retencion: conserva los ultimos N respaldos de cada tipo (DB=30, sistema=15).
 */
class BackupService
{
    private const BACKUP_DIR     = '/var/www/WMS/backups';
    private const APP_ROOT       = '/var/www/WMS';
    private const KEEP_DB        = 30;
    private const KEEP_SYSTEM    = 15;

    /** Subdirectorios del sistema a incluir en el tar.gz. */
    private const SYSTEM_PATHS = ['src', 'public', 'scripts', 'database', '.htaccess'];

    public static function dir(): string
    {
        if (!is_dir(self::BACKUP_DIR)) {
            @mkdir(self::BACKUP_DIR, 0775, true);
        }
        return self::BACKUP_DIR;
    }

    /**
     * Lista los respaldos existentes (DB y sistema), mas recientes primero.
     * @return array<int, array{name:string,type:string,size:int,size_human:string,created_at:string}>
     */
    public static function listBackups(): array
    {
        $dir = self::dir();
        $out = [];
        foreach (['wms_pro_*.sql', 'wms_system_*.tar.gz'] as $glob) {
            foreach (glob($dir . '/' . $glob) ?: [] as $path) {
                if (!is_file($path)) {
                    continue;
                }
                $name = basename($path);
                $out[] = [
                    'name'       => $name,
                    'type'       => str_starts_with($name, 'wms_system_') ? 'system' : 'db',
                    'size'       => filesize($path) ?: 0,
                    'size_human' => self::humanSize(filesize($path) ?: 0),
                    'created_at' => date('Y-m-d H:i:s', filemtime($path) ?: time()),
                ];
            }
        }
        usort($out, static fn($a, $b) => strcmp($b['created_at'], $a['created_at']));
        return $out;
    }

    /**
     * Genera el respaldo de la base de datos via mysqldump.
     * @return array{name:string,type:string,size:int,size_human:string}
     * @throws \RuntimeException si mysqldump falla.
     */
    public static function createDatabaseBackup(): array
    {
        $cfg  = require self::APP_ROOT . '/src/config/app.php';
        $db   = $cfg['db'];
        $dir  = self::dir();
        $name = 'wms_pro_' . date('Ymd_Hi') . '.sql';
        $path = $dir . '/' . $name;

        $cmd = sprintf(
            'mysqldump --single-transaction --quick --routines --triggers --no-tablespaces -h %s -P %s -u %s -p%s %s > %s 2>&1',
            escapeshellarg((string) $db['host']),
            escapeshellarg((string) $db['port']),
            escapeshellarg((string) $db['username']),
            escapeshellarg((string) $db['password']),
            escapeshellarg((string) $db['database']),
            escapeshellarg($path)
        );

        $output = [];
        $code   = 0;
        @exec($cmd, $output, $code);

        if ($code !== 0 || !is_file($path) || filesize($path) < 1024) {
            @unlink($path);
            throw new \RuntimeException('mysqldump fallo (codigo ' . $code . '): ' . implode(' ', $output));
        }

        self::applyRetention('wms_pro_*.sql', self::KEEP_DB);

        return [
            'name'       => $name,
            'type'       => 'db',
            'size'       => filesize($path) ?: 0,
            'size_human' => self::humanSize(filesize($path) ?: 0),
        ];
    }

    /**
     * Genera el respaldo del sistema (codigo de la aplicacion) en un tar.gz.
     * @return array{name:string,type:string,size:int,size_human:string}
     * @throws \RuntimeException si tar falla.
     */
    public static function createSystemBackup(): array
    {
        $dir  = self::dir();
        $name = 'wms_system_' . date('Ymd_Hi') . '.tar.gz';
        $path = $dir . '/' . $name;

        // Solo incluir los paths existentes para evitar que tar aborte.
        $paths = array_filter(
            self::SYSTEM_PATHS,
            static fn($p) => file_exists(self::APP_ROOT . '/' . $p)
        );
        $pathArgs = implode(' ', array_map('escapeshellarg', array_values($paths)));

        $cmd = sprintf(
            'tar -czf %s -C %s --exclude=%s --exclude=%s --exclude=%s %s 2>&1',
            escapeshellarg($path),
            escapeshellarg(self::APP_ROOT),
            escapeshellarg('*.tar.gz'),
            escapeshellarg('public/downloads'),
            escapeshellarg('storage'),
            $pathArgs
        );

        $output = [];
        $code   = 0;
        @exec($cmd, $output, $code);

        // tar puede devolver 1 por "file changed as we read it" pero generar un archivo valido.
        if (!is_file($path) || filesize($path) < 1024) {
            @unlink($path);
            throw new \RuntimeException('tar fallo (codigo ' . $code . '): ' . implode(' ', $output));
        }

        self::applyRetention('wms_system_*.tar.gz', self::KEEP_SYSTEM);

        return [
            'name'       => $name,
            'type'       => 'system',
            'size'       => filesize($path) ?: 0,
            'size_human' => self::humanSize(filesize($path) ?: 0),
        ];
    }

    /**
     * Devuelve la ruta absoluta de un respaldo validando el nombre, o null si es invalido.
     */
    public static function resolvePath(string $name): ?string
    {
        $name = basename($name); // anti path traversal
        if (!preg_match('/^(wms_pro_\d{8}_\d{4}\.sql|wms_system_\d{8}_\d{4}\.tar\.gz)$/', $name)) {
            return null;
        }
        $path = self::dir() . '/' . $name;
        return is_file($path) ? $path : null;
    }

    /** Elimina los respaldos mas antiguos conservando los $keep mas recientes. */
    private static function applyRetention(string $glob, int $keep): void
    {
        $files = glob(self::dir() . '/' . $glob) ?: [];
        if (count($files) <= $keep) {
            return;
        }
        usort($files, static fn($a, $b) => filemtime($b) <=> filemtime($a));
        foreach (array_slice($files, $keep) as $old) {
            @unlink($old);
        }
    }

    private static function humanSize(int $bytes): string
    {
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
        $i = 0;
        $val = (float) $bytes;
        while ($val >= 1024 && $i < count($units) - 1) {
            $val /= 1024;
            $i++;
        }
        return ($i === 0 ? (string) $bytes : number_format($val, 1)) . ' ' . $units[$i];
    }
}
