<?php
declare(strict_types=1);

namespace App\Services\PackageManager;

use App\Core\Database;
use PDO;
use RuntimeException;
use Throwable;
use ZipArchive;

final class PackageRepairService
{
    public function packages(): array
    {
        $stmt = Database::connection()->prepare(
            "SELECT r.id,r.package_name,r.version,r.package_status,r.update_package_id,p.fichier_original,p.nom_package
             FROM system_package_registry r
             LEFT JOIN cda_update_packages p ON p.id=r.update_package_id
             WHERE r.package_status IN ('INSTALLED','SYSTEM','BROKEN','REPAIR_REQUIRED')
             ORDER BY r.package_name,r.version DESC"
        );
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }

    public function latestDiagnostics(int $limit = 30): array
    {
        $limit = max(1, min(100, $limit));
        $stmt = Database::connection()->prepare(
            'SELECT d.*,r.package_name,r.version FROM system_package_repair_diagnostics d
             JOIN system_package_registry r ON r.id=d.registry_id ORDER BY d.id DESC LIMIT '.$limit
        );
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
    }

    public function diagnose(int $registryId, ?int $userId = null): array
    {
        $package = $this->package($registryId);
        $archive = $this->archivePath($package);
        [$manifest, $zip] = $this->openManifest($archive);
        $files = is_array($manifest['files'] ?? null) ? $manifest['files'] : [];
        $missing = []; $modified = []; $healthy = 0; $checked = 0;
        foreach ($files as $entry) {
            $source = (string)($entry['path'] ?? '');
            $expected = strtolower((string)($entry['sha256'] ?? ''));
            if (!str_starts_with($source, 'payload/') || $expected === '') continue;
            $relative = substr($source, 8);
            if ($relative === '' || !$this->safeRelative($relative)) continue;
            $checked++;
            $target = BASE_PATH.'/'.$relative;
            if (!is_file($target)) { $missing[] = $relative; continue; }
            $actual = strtolower((string)hash_file('sha256', $target));
            if (!hash_equals($expected, $actual)) $modified[] = $relative; else $healthy++;
        }
        $zip->close();
        $state = (!$missing && !$modified) ? 'HEALTHY' : (($missing || $modified) ? 'REPAIRABLE' : 'DEGRADED');
        $score = $checked > 0 ? (int)round(($healthy / $checked) * 100) : 0;
        $result = ['state'=>$state,'score'=>$score,'checked'=>$checked,'healthy'=>$healthy,'missing'=>$missing,'modified'=>$modified,'archive'=>basename($archive)];
        $this->storeDiagnostic($registryId, $result, $userId);
        return $result;
    }

    public function repair(int $registryId, bool $simulate, ?int $userId = null): array
    {
        $diagnostic = $this->diagnose($registryId, $userId);
        $targets = array_values(array_unique(array_merge($diagnostic['missing'], $diagnostic['modified'])));
        if ($simulate || !$targets) {
            return ['simulation'=>$simulate,'repaired'=>0,'targets'=>$targets,'diagnostic'=>$diagnostic,'status'=>$targets ? 'SIMULATED' : 'NOT_REQUIRED'];
        }
        $package = $this->package($registryId);
        $archive = $this->archivePath($package);
        [$manifest, $zip] = $this->openManifest($archive);
        $map = [];
        foreach (($manifest['files'] ?? []) as $entry) {
            $path = (string)($entry['path'] ?? '');
            if (str_starts_with($path, 'payload/')) $map[substr($path, 8)] = $entry;
        }
        $repairId = bin2hex(random_bytes(12));
        $backupRoot = BASE_PATH.'/storage/private/package_repair/'.$repairId;
        if (!is_dir($backupRoot) && !mkdir($backupRoot, 0700, true) && !is_dir($backupRoot)) throw new RuntimeException('Impossible de créer la sauvegarde de réparation.');
        $backedUp = []; $written = [];
        try {
            foreach ($targets as $relative) {
                if (!$this->safeRelative($relative) || !isset($map[$relative])) throw new RuntimeException('Fichier de réparation non autorisé : '.$relative);
                $target = BASE_PATH.'/'.$relative;
                if (is_file($target)) {
                    $backup = $backupRoot.'/'.$relative;
                    if (!is_dir(dirname($backup))) mkdir(dirname($backup), 0700, true);
                    if (!copy($target, $backup)) throw new RuntimeException('Sauvegarde impossible : '.$relative);
                    $backedUp[$relative] = $backup;
                }
                $content = $zip->getFromName('payload/'.$relative);
                if ($content === false) throw new RuntimeException('Fichier absent du package signé : '.$relative);
                if (!is_dir(dirname($target))) mkdir(dirname($target), 0755, true);
                if (file_put_contents($target, $content, LOCK_EX) === false) throw new RuntimeException('Écriture impossible : '.$relative);
                $expected = strtolower((string)($map[$relative]['sha256'] ?? ''));
                if ($expected === '' || !hash_equals($expected, strtolower(hash_file('sha256', $target)))) throw new RuntimeException('Empreinte invalide après réparation : '.$relative);
                $written[] = $relative;
            }
            $zip->close();
            $after = $this->diagnose($registryId, $userId);
            if ($after['state'] !== 'HEALTHY') throw new RuntimeException('Le contrôle final reste dégradé.');
            $this->operation($registryId, 'REPAIR', 'SUCCESS', count($written).' fichier(s) restauré(s).', $userId, ['repair_id'=>$repairId,'files'=>$written]);
            return ['simulation'=>false,'repaired'=>count($written),'targets'=>$written,'diagnostic'=>$after,'status'=>'SUCCESS','repair_id'=>$repairId];
        } catch (Throwable $e) {
            foreach (array_reverse($written) as $relative) {
                $target = BASE_PATH.'/'.$relative;
                if (isset($backedUp[$relative])) copy($backedUp[$relative], $target); else @unlink($target);
            }
            $zip->close();
            $this->operation($registryId, 'REPAIR', 'FAILED', $e->getMessage(), $userId, ['repair_id'=>$repairId]);
            throw $e;
        }
    }

    private function package(int $registryId): array
    {
        if ($registryId <= 0) throw new RuntimeException('Package invalide.');
        $stmt = Database::connection()->prepare(
            'SELECT r.*,p.fichier_original,p.nom_package FROM system_package_registry r
             LEFT JOIN cda_update_packages p ON p.id=r.update_package_id WHERE r.id=:id LIMIT 1'
        );
        $stmt->execute(['id'=>$registryId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) throw new RuntimeException('Package introuvable dans le registre.');
        return $row;
    }

    private function archivePath(array $package): string
    {
        $file = basename((string)($package['fichier_original'] ?? ''));
        if ($file === '') throw new RuntimeException('Archive source non référencée pour ce package.');
        $path = BASE_PATH.'/storage/updates/packages/'.$file;
        if (!is_file($path)) throw new RuntimeException('Archive source indisponible : '.$file);
        return $path;
    }

    private function openManifest(string $archive): array
    {
        $zip = new ZipArchive();
        if ($zip->open($archive) !== true) throw new RuntimeException('Archive source illisible.');
        $json = $zip->getFromName('manifest.json');
        $manifest = is_string($json) ? json_decode($json, true) : null;
        if (!is_array($manifest)) { $zip->close(); throw new RuntimeException('Manifest source invalide.'); }
        return [$manifest, $zip];
    }

    private function safeRelative(string $path): bool
    {
        return $path !== '' && !str_contains($path, '..') && !str_starts_with($path, '/') && preg_match('#^[A-Za-z0-9_./-]+$#', $path) === 1;
    }

    private function storeDiagnostic(int $registryId, array $r, ?int $userId): void
    {
        $stmt = Database::connection()->prepare(
            'INSERT INTO system_package_repair_diagnostics(registry_id,health_state,health_score,checked_files,healthy_files,missing_files,modified_files,result_json,performed_by,created_at)
             VALUES(:registry_id,:state,:score,:checked,:healthy,:missing,:modified,:result_json,:performed_by,NOW())'
        );
        $stmt->execute(['registry_id'=>$registryId,'state'=>$r['state'],'score'=>$r['score'],'checked'=>$r['checked'],'healthy'=>$r['healthy'],'missing'=>count($r['missing']),'modified'=>count($r['modified']),'result_json'=>json_encode($r, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),'performed_by'=>$userId]);
        Database::connection()->prepare("UPDATE system_package_registry SET package_status=CASE WHEN :state='HEALTHY' THEN package_status ELSE 'REPAIR_REQUIRED' END,updated_at=NOW() WHERE id=:id")->execute(['state'=>$r['state'],'id'=>$registryId]);
    }

    private function operation(int $registryId, string $type, string $status, string $message, ?int $userId, array $details): void
    {
        $stmt = Database::connection()->prepare('INSERT INTO system_package_operations(registry_id,operation_type,status,message,performed_by,created_at) VALUES(:registry_id,:type,:status,:message,:user_id,NOW())');
        $stmt->execute(['registry_id'=>$registryId,'type'=>$type,'status'=>$status,'message'=>$message,'user_id'=>$userId]);
    }
}
