<?php
declare(strict_types=1);
namespace App\Services\Trust;

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

final class TrustEngineService
{
    private const PUBLIC_KEY_B64='kqE15EtAhdv6ahZfDXUPsRLvh+Oj672/kUx0ARvuuBw=';

    /** @return array<string,mixed> */
    public function evaluatePackageFile(string $zipPath,?int $packageId=null,?int $userId=null,bool $persist=true): array
    {
        if(!is_file($zipPath)) throw new RuntimeException('Archive du package introuvable.');
        $started=microtime(true);$zip=new ZipArchive();
        if($zip->open($zipPath)!==true) throw new RuntimeException('Archive ZIP illisible.');
        try{
            $raw=$zip->getFromName('manifest.json');$manifest=is_string($raw)?json_decode($raw,true):null;
            $mv=(new TrustManifestValidator())->validate($manifest);$checks=[];
            $checks[]=$this->check('manifest',$mv['ok'],5,$mv['ok']?'Manifeste cohérent.':'Manifeste non conforme.',$mv['issues'],$mv['warnings']);
            $signature=$this->verifySignature(is_array($manifest)?$manifest:[]);
            $checks[]=$this->check('signature',$signature['ok'],30,$signature['message'],$signature['issues']);
            $integrity=$this->verifyIntegrity($zip,is_array($manifest)?$manifest:[]);
            $checks[]=$this->check('integrity',$integrity['ok'],20,$integrity['message'],$integrity['issues']);
            $version=$this->verifyVersion(is_array($manifest)?$manifest:[]);
            $checks[]=$this->check('compatibility',$version['ok'],20,$version['message'],$version['issues']);
            $dependencies=$this->verifyDependencies(is_array($manifest)?$manifest:[]);
            $checks[]=$this->check('dependencies',$dependencies['ok'],15,$dependencies['message'],$dependencies['issues'],$dependencies['warnings']);
            $structure=$this->verifyStructure($zip,is_array($manifest)?$manifest:[]);
            $checks[]=$this->check('structure',$structure['ok'],10,$structure['message'],$structure['issues']);
            $criteria=[];foreach($checks as $c)$criteria[$c['code']]=['ok'=>$c['ok'],'weight'=>$c['weight']];
            $score=(new TrustScoreCalculator())->calculate($criteria);
            $report=(new TrustReportBuilder())->build($checks,$score,['package_id'=>$packageId,'package'=>$mv['metadata']['package']??'','version'=>$mv['metadata']['version']??'','duration_ms'=>(int)round((microtime(true)-$started)*1000),'writes_performed'=>$persist]);
            if($persist)$this->persist($report,$packageId,$userId);
            return $report;
        } finally {$zip->close();}
    }

    /** @return array<string,mixed> */
    public function evaluatePackageId(int $packageId,?int $userId): array
    {
        $pdo=Database::connection();$s=$pdo->prepare('SELECT fichier_original FROM cda_update_packages WHERE id=:id');$s->execute(['id'=>$packageId]);$name=$s->fetchColumn();
        if(!is_string($name)||$name==='')throw new RuntimeException('Package introuvable.');
        return $this->evaluatePackageFile(BASE_PATH.'/storage/updates/packages/'.basename($name),$packageId,$userId,true);
    }

    /** @return array<string,mixed> */
    public function dashboard(): array
    {
        $pdo=Database::connection();$rows=$pdo->query('SELECT * FROM system_package_trust_reports ORDER BY id DESC LIMIT 50')->fetchAll(PDO::FETCH_ASSOC)?:[];
        return ['reports'=>$rows,'generated_at'=>date('Y-m-d H:i:s')];
    }

    /** @return array{ok:bool,message:string,issues:list<string>} */
    private function verifySignature(array $manifest):array
    {
        if(!function_exists('sodium_crypto_sign_verify_detached'))return ['ok'=>false,'message'=>'Extension Sodium indisponible.','issues'=>['Vérification Ed25519 impossible.']];
        $signature=base64_decode((string)($manifest['signature']??''),true);$public=base64_decode(self::PUBLIC_KEY_B64,true);
        if($signature===false||strlen($signature)!==SODIUM_CRYPTO_SIGN_BYTES)return ['ok'=>false,'message'=>'Signature absente ou invalide.','issues'=>['Signature Ed25519 non exploitable.']];
        unset($manifest['signature']);$payload=json_encode($this->canonicalize($manifest),JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
        $ok=is_string($payload)&&sodium_crypto_sign_verify_detached($signature,$payload,$public);
        return ['ok'=>$ok,'message'=>$ok?'Signature Ed25519 valide.':'Signature Ed25519 rejetée.','issues'=>$ok?[]:['Le manifeste signé ne correspond pas à la signature.']];
    }

    /** @return array{ok:bool,message:string,issues:list<string>} */
    private function verifyIntegrity(ZipArchive $zip,array $manifest):array
    {
        $issues=[];$seen=[];foreach((array)($manifest['files']??[]) as $f){$path=(string)($f['path']??'');$hash=(string)($f['sha256']??'');if($path===''||isset($seen[$path])){$issues[]='Chemin de fichier vide ou dupliqué.';continue;}$seen[$path]=true;$data=$zip->getFromName($path);if(!is_string($data)){$issues[]="Fichier absent : {$path}.";continue;}if(!preg_match('/^[a-f0-9]{64}$/i',$hash)||!hash_equals(strtolower($hash),hash('sha256',$data)))$issues[]="Empreinte incorrecte : {$path}.";}
        return ['ok'=>empty($issues),'message'=>empty($issues)?'Toutes les empreintes SHA-256 sont conformes.':'Intégrité du package compromise.','issues'=>$issues];
    }

    /** @return array{ok:bool,message:string,issues:list<string>} */
    private function verifyVersion(array $manifest):array
    {
        $current=trim((string)@file_get_contents(BASE_PATH.'/VERSION'));if($current==='')$current='0.0.0';$min=(string)($manifest['min_version']??'0.0.0');$max=(string)($manifest['max_version']??'');$issues=[];
        if(version_compare($current,$min,'<'))$issues[]="Version {$current} inférieure au minimum {$min}.";
        if($max!==''&&version_compare($current,$max,'>'))$issues[]="Version {$current} supérieure au maximum {$max}.";
        return ['ok'=>empty($issues),'message'=>empty($issues)?"Version courante {$current} compatible.":'Version de plateforme incompatible.','issues'=>$issues];
    }

    /** @return array{ok:bool,message:string,issues:list<string>,warnings:list<string>} */
    private function verifyDependencies(array $manifest):array
    {
        $issues=[];$warnings=[];$deps=(array)($manifest['dependencies']??[]);if($deps===[])$warnings[]='Aucune dépendance déclarée.';
        if($deps!==[]){$pdo=Database::connection();foreach($deps as $dep){$id=(string)($dep['package_id']??'');$min=(string)($dep['min_version']??'0.0.0');if($id===''){$issues[]='Dépendance sans package_id.';continue;}$s=$pdo->prepare("SELECT version_package FROM cda_update_packages WHERE nom_package=:id AND statut='installed' ORDER BY id DESC LIMIT 1");$s->execute(['id'=>$id]);$installed=$s->fetchColumn();if(!is_string($installed))$issues[]="Dépendance absente : {$id}.";elseif(version_compare($installed,$min,'<'))$issues[]="Dépendance {$id} trop ancienne ({$installed} < {$min}).";}}
        return ['ok'=>empty($issues),'message'=>empty($issues)?'Dépendances satisfaites ou non requises.':'Dépendances non satisfaites.','issues'=>$issues,'warnings'=>$warnings];
    }

    /** @return array{ok:bool,message:string,issues:list<string>} */
    private function verifyStructure(ZipArchive $zip,array $manifest):array
    {
        $issues=[];$allowed=['manifest.json','migrations/','payload/'];for($i=0;$i<$zip->numFiles;$i++){$n=(string)$zip->getNameIndex($i);if(str_contains($n,'../')||str_starts_with($n,'/')||preg_match('#(^|/)\.env$#',$n))$issues[]="Chemin interdit : {$n}.";if($n==='manifest.json')continue;if(!str_starts_with($n,'payload/')&&!str_starts_with($n,'migrations/'))$issues[]="Racine non autorisée : {$n}.";}
        if(empty($manifest['files']))$issues[]='Liste de fichiers vide.';
        return ['ok'=>empty($issues),'message'=>empty($issues)?'Structure du package autorisée.':'Structure du package non conforme.','issues'=>$issues];
    }

    /** @return array<string,mixed> */
    private function check(string $code,bool $ok,int $weight,string $message,array $issues=[],array $warnings=[]):array{return compact('code','ok','weight','message','issues','warnings');}
    private function canonicalize(mixed $v):mixed{if(!is_array($v))return $v;if(array_is_list($v))return array_map(fn($x)=>$this->canonicalize($x),$v);ksort($v,SORT_STRING);foreach($v as $k=>$x)$v[$k]=$this->canonicalize($x);return $v;}
    private function persist(array $report,?int $packageId,?int $userId):void{$pdo=Database::connection();$s=$pdo->prepare('INSERT INTO system_package_trust_reports(package_id,package_name,package_version,trust_score,trust_level,decision,report_json,evaluated_by,created_at) VALUES(:pid,:name,:version,:score,:level,:decision,:report,:uid,NOW())');$s->execute(['pid'=>$packageId,'name'=>(string)($report['context']['package']??''),'version'=>(string)($report['context']['version']??''),'score'=>(int)$report['score'],'level'=>(string)$report['trust_level'],'decision'=>(string)$report['decision'],'report'=>json_encode($report,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),'uid'=>$userId]);}
}
