<?php
declare(strict_types=1);
namespace App\Services\Compatibility;
use App\Core\Database;
use App\Services\Trust\TrustEngineService;
use PDO;
use RuntimeException;
use ZipArchive;
final class PackageCompatibilityService
{
    public function evaluatePackageId(int $packageId,?int $userId,bool $persist=true):array
    {
        $pdo=Database::connection();$stmt=$pdo->prepare('SELECT fichier_original FROM cda_update_packages WHERE id=:id');$stmt->execute(['id'=>$packageId]);$name=$stmt->fetchColumn();
        if(!is_string($name)||$name==='')throw new RuntimeException('Package introuvable.');
        return $this->evaluatePackageFile(BASE_PATH.'/storage/updates/packages/'.basename($name),$packageId,$userId,$persist);
    }
    public function evaluatePackageFile(string $zipPath,?int $packageId=null,?int $userId=null,bool $persist=true):array
    {
        if(!is_file($zipPath))throw new RuntimeException('Archive introuvable.');$zip=new ZipArchive();if($zip->open($zipPath)!==true)throw new RuntimeException('Archive ZIP illisible.');$started=microtime(true);
        try{$raw=$zip->getFromName('manifest.json');$manifest=is_string($raw)?json_decode($raw,true):null;if(!is_array($manifest))throw new RuntimeException('Manifeste invalide.');
            $checks=[];$trust=(new TrustEngineService())->evaluatePackageFile($zipPath,$packageId,$userId,false);$checks[]=$this->check('trust',$trust['decision']!=='REJECTED',25,'Trust Engine : '.($trust['decision']??'INCONNU').'.',$trust['decision']==='REJECTED'?['Package rejeté par le Trust Engine.']:[],$trust['decision']==='WARNING'?['Le package présente des avertissements de confiance.']:[]);
            $checks[]=$this->checkRuntime($manifest);$checks[]=$this->checkDependencies($manifest);$checks[]=$this->checkFiles($zip,$manifest);$checks[]=$this->checkRoutes($zip,$manifest);$checks[]=$this->checkPermissions($zip,$manifest);$checks[]=$this->checkMigrations($zip,$manifest);
            $score=0;$critical=false;$warnings=[];$issues=[];foreach($checks as $c){if($c['ok'])$score+=$c['weight'];if(!$c['ok']&&$c['critical'])$critical=true;$issues=array_merge($issues,$c['issues']);$warnings=array_merge($warnings,$c['warnings']);}
            $score=max(0,min(100,$score));$decision=$critical?'INCOMPATIBLE':($warnings||$issues?'WARNING':'COMPATIBLE');$report=['score'=>$score,'decision'=>$decision,'checks'=>$checks,'issues'=>array_values(array_unique($issues)),'warnings'=>array_values(array_unique($warnings)),'context'=>['package_id'=>$packageId,'package'=>(string)($manifest['package']??''),'version'=>(string)($manifest['version']??''),'duration_ms'=>(int)round((microtime(true)-$started)*1000)]];
            if($persist)$this->persist($report,$packageId,$userId);return $report;
        }finally{$zip->close();}
    }
    public function dashboard():array{$pdo=Database::connection();$stmt=$pdo->prepare('SELECT * FROM system_package_compatibility_reports ORDER BY id DESC LIMIT 50');$stmt->execute();return ['reports'=>$stmt->fetchAll(PDO::FETCH_ASSOC)?:[],'generated_at'=>date('Y-m-d H:i:s')];}
    private function checkRuntime(array $manifest):array{$issues=[];$warnings=[];$runtime=(array)($manifest['runtime']??[]);$php=(string)($runtime['php']??'');if($php!==''&&version_compare(PHP_VERSION,$php,'<'))$issues[]='PHP '.PHP_VERSION.' inférieur au minimum '.$php.'.';foreach((array)($runtime['extensions']??[]) as $ext){if(!extension_loaded((string)$ext))$issues[]='Extension PHP absente : '.$ext.'.';}if($runtime===[])$warnings[]='Aucune exigence runtime déclarée.';return $this->check('runtime',!$issues,15,$issues?'Runtime incompatible.':'Runtime compatible.', $issues,$warnings,true);}
    private function checkDependencies(array $manifest):array{$issues=[];$warnings=[];$pdo=Database::connection();foreach((array)($manifest['dependencies']??[]) as $dep){$id=(string)($dep['package_id']??'');$min=(string)($dep['min_version']??'0.0.0');$max=(string)($dep['max_version']??'');if($id===''){$issues[]='Dépendance sans identifiant.';continue;}$stmt=$pdo->prepare("SELECT version_package FROM cda_update_packages WHERE nom_package=:name AND statut IN ('installe','installed') ORDER BY id DESC LIMIT 1");$stmt->execute(['name'=>$id]);$v=$stmt->fetchColumn();if(!is_string($v)){$issues[]='Dépendance absente : '.$id.'.';continue;}if(version_compare($v,$min,'<'))$issues[]='Dépendance '.$id.' trop ancienne.';if($max!==''&&version_compare($v,$max,'>'))$issues[]='Dépendance '.$id.' trop récente.';}foreach((array)($manifest['conflicts']??[]) as $conflict){$id=(string)($conflict['package_id']??$conflict);if($id==='')continue;$stmt=$pdo->prepare("SELECT COUNT(*) FROM cda_update_packages WHERE nom_package=:name AND statut IN ('installe','installed')");$stmt->execute(['name'=>$id]);if((int)$stmt->fetchColumn()>0)$issues[]='Package incompatible installé : '.$id.'.';}if(empty($manifest['dependencies']))$warnings[]='Aucune dépendance déclarée.';return $this->check('dependencies',!$issues,15,$issues?'Dépendances incompatibles.':'Dépendances satisfaites.', $issues,$warnings,true);}
    private function checkFiles(ZipArchive $zip,array $manifest):array{$issues=[];$warnings=[];$critical=['app/Core/','app/Security/','app/Services/UpdatePackageService.php','routes/web.php','VERSION','CORE_VERSION'];foreach((array)($manifest['files']??[]) as $f){$path=(string)($f['path']??'');if(!str_starts_with($path,'payload/'))continue;$dest=substr($path,8);$target=BASE_PATH.'/'.$dest;if(!is_file($target))continue;$data=$zip->getFromName($path);if(!is_string($data))continue;if(hash_file('sha256',$target)===hash('sha256',$data))continue;$isCritical=false;foreach($critical as $prefix){if($dest===$prefix||str_starts_with($dest,$prefix)){$isCritical=true;break;}}if($isCritical)$warnings[]='Composant cœur remplacé : '.$dest.'.';else $warnings[]='Fichier existant remplacé : '.$dest.'.';}return $this->check('files',!$issues,10,'Collisions de fichiers analysées.',$issues,$warnings,false);}
    private function checkRoutes(ZipArchive $zip,array $manifest):array{$issues=[];$warnings=[];$packageRoutes=$this->zipText($zip,'payload/routes/web.php');if($packageRoutes==='')return $this->check('routes',true,10,'Aucune route modifiée.',[],[],false);$current=(string)@file_get_contents(BASE_PATH.'/routes/web.php');$a=$this->routes($current);$b=$this->routes($packageRoutes);foreach($b as $key=>$handler){if(isset($a[$key])&&$a[$key]!==$handler)$issues[]='Collision de route : '.$key.'.';}return $this->check('routes',!$issues,10,$issues?'Collisions de routes détectées.':'Routes compatibles.',$issues,$warnings,true);}
    private function checkPermissions(ZipArchive $zip,array $manifest):array{$issues=[];$warnings=[];$pdo=Database::connection();foreach((array)($manifest['migrations']??[]) as $migration){$sql=$this->zipText($zip,(string)$migration);if($sql==='')continue;preg_match_all("/['\"]([a-z0-9_.-]+)['\"]\s*,\s*['\"]([a-z0-9_.-]+)['\"]\s*,\s*['\"]([a-z0-9_.-]+)['\"]/i",$sql,$matches,PREG_SET_ORDER);foreach($matches as $m){$code=$m[1];if(!str_contains($code,'.'))continue;$stmt=$pdo->prepare('SELECT module,action FROM permissions WHERE code=:code LIMIT 1');$stmt->execute(['code'=>$code]);$row=$stmt->fetch(PDO::FETCH_ASSOC);if($row&&((string)$row['module']!==$m[2]||(string)$row['action']!==$m[3]))$issues[]='Permission incompatible : '.$code.'.';}}return $this->check('permissions',!$issues,10,$issues?'Collisions de permissions détectées.':'Permissions compatibles.',$issues,$warnings,true);}
    private function checkMigrations(ZipArchive $zip,array $manifest):array{$issues=[];$warnings=[];$pdo=Database::connection();foreach((array)($manifest['migrations']??[]) as $migration){$path=(string)$migration;$sql=$this->zipText($zip,$path);if($sql===''){$issues[]='Migration absente : '.$path.'.';continue;}$checksum=hash('sha256',$sql);$stmt=$pdo->prepare('SELECT checksum_sha256,statut FROM axiom_update_migrations WHERE migration_key LIKE :suffix ORDER BY id DESC LIMIT 1');$stmt->execute(['suffix'=>'%:'.$path]);$row=$stmt->fetch(PDO::FETCH_ASSOC);if($row&&$row['statut']==='appliquee'&&!hash_equals((string)$row['checksum_sha256'],$checksum))$issues[]='Migration déjà appliquée avec une autre empreinte : '.$path.'.';if(preg_match('/\b(DROP\s+TABLE|TRUNCATE\s+TABLE|DROP\s+COLUMN)\b/i',$sql))$warnings[]='Migration destructive détectée : '.$path.'.';}return $this->check('migrations',!$issues,15,$issues?'Migrations incompatibles.':'Migrations compatibles.',$issues,$warnings,true);}
    private function routes(string $source):array{$out=[];preg_match_all("/\\$router->(get|post|put|patch|delete)\\(\\s*['\"]([^'\"]+)['\"]\\s*,\\s*\\[([^\\]]+)\\]/i",$source,$m,PREG_SET_ORDER);foreach($m as $r)$out[strtoupper($r[1]).' '.$r[2]]=preg_replace('/\s+/','',trim($r[3]));return $out;}
    private function zipText(ZipArchive $zip,string $path):string{$value=$zip->getFromName($path);return is_string($value)?$value:'';}
    private function check(string $code,bool $ok,int $weight,string $message,array $issues=[],array $warnings=[],bool $critical=false):array{return compact('code','ok','weight','message','issues','warnings','critical');}
    private function persist(array $report,?int $packageId,?int $userId):void{$pdo=Database::connection();$stmt=$pdo->prepare('INSERT INTO system_package_compatibility_reports(package_id,package_name,package_version,compatibility_score,decision,report_json,evaluated_by,created_at) VALUES(:pid,:name,:version,:score,:decision,:report,:uid,NOW())');$stmt->execute(['pid'=>$packageId,'name'=>(string)$report['context']['package'],'version'=>(string)$report['context']['version'],'score'=>(int)$report['score'],'decision'=>(string)$report['decision'],'report'=>json_encode($report,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),'uid'=>$userId]);}
}
