<?php
declare(strict_types=1);
namespace App\Services\Pipeline;
use App\Core\Database; use App\Services\Pipeline\Steps\NativePreflightStep; use App\Services\Pipeline\Steps\TrustPipelineStep; use App\Services\Pipeline\Steps\CompatibilityPipelineStep; use App\Services\Pipeline\Steps\PolicyPipelineStep; use App\Services\Pipeline\Steps\DependencyPipelineStep; use RuntimeException; use Throwable; use ZipArchive;
final class PipelineOrchestrator {
 private array $steps;
 public function __construct(){ $this->steps=[new NativePreflightStep(),new TrustPipelineStep(),new CompatibilityPipelineStep(),new PolicyPipelineStep(),new DependencyPipelineStep()]; usort($this->steps,static fn(PipelineStep $a,PipelineStep $b):int=>$a->order()<=>$b->order());}
 public function execute(string $zipPath,?int $packageId=null,?int $userId=null,string $runType='simulation',bool $persist=true):array {
  if(!is_file($zipPath))throw new RuntimeException('Archive du package introuvable.');$manifest=$this->manifest($zipPath);$started=microtime(true);$runId=$persist?$this->startRun($packageId,$zipPath,$manifest,$runType,$userId):null;$results=[];$blocking=null;$ctx=['zip_path'=>$zipPath,'package_id'=>$packageId,'user_id'=>$userId,'run_type'=>$runType,'manifest'=>$manifest,'installed_version'=>$this->installedVersion()];
  foreach($this->steps as $step){$t=microtime(true);try{$r=$step->execute($ctx);}catch(Throwable $e){$r=['code'=>$step->code(),'label'=>$step->label(),'order'=>$step->order(),'status'=>'ERROR','decision'=>'ERROR','score'=>0,'critical'=>true,'message'=>'Étape interrompue.','issues'=>[$e->getMessage()],'warnings'=>[],'details'=>[]];}$r['duration_ms']=(int)round((microtime(true)-$t)*1000);$results[]=$r;if($runId!==null)$this->persistStep($runId,$r);if(($r['critical']??false)&&in_array((string)$r['decision'],['BLOCKED','DENIED','REJECTED','INCOMPATIBLE','ERROR'],true)){$blocking=(string)$r['code'];break;}}
  $scores=array_map(static fn(array $s):int=>(int)($s['score']??0),$results);$score=$scores?(int)round(array_sum($scores)/count($scores)):0;$issues=[];$warnings=[];foreach($results as $r){$issues=array_merge($issues,(array)($r['issues']??[]));$warnings=array_merge($warnings,(array)($r['warnings']??[]));}$decision=$blocking!==null?'DENIED':($warnings?'WARNING':'ALLOWED');$summary=['ok'=>$blocking===null,'decision'=>$decision,'score'=>$score,'blocking_step'=>$blocking,'package_id'=>$packageId,'package'=>(string)($manifest['package']??''),'version'=>(string)($manifest['version']??''),'steps'=>$results,'issues'=>array_values(array_unique($issues)),'warnings'=>array_values(array_unique($warnings)),'duration_ms'=>(int)round((microtime(true)-$started)*1000),'run_type'=>$runType,'writes_performed'=>false,'executed_at'=>gmdate('c')];if($runId!==null){$summary['run_id']=$runId;$this->finishRun($runId,$summary);}return $summary;
 }
 private function manifest(string $path):array{$z=new ZipArchive();if($z->open($path)!==true)throw new RuntimeException('Archive ZIP illisible.');try{$raw=$z->getFromName('manifest.json');$m=is_string($raw)?json_decode($raw,true):null;if(!is_array($m))throw new RuntimeException('manifest.json absent ou invalide.');return $m;}finally{$z->close();}}
 private function installedVersion():string{return is_file(BASE_PATH.'/VERSION')?trim((string)file_get_contents(BASE_PATH.'/VERSION')):'0.0.0';}
 private function startRun(?int $pid,string $path,array $m,string $type,?int $uid):int{$s=Database::connection()->prepare("INSERT INTO system_package_pipeline_runs(package_id,package_name,package_version,archive_name,run_type,status,decision,score,executed_by,created_at) VALUES(:pid,:name,:version,:archive,:type,'RUNNING','PENDING',0,:uid,NOW())");$s->execute(['pid'=>$pid,'name'=>(string)($m['package']??''),'version'=>(string)($m['version']??''),'archive'=>basename($path),'type'=>$type,'uid'=>$uid]);return (int)Database::connection()->lastInsertId();}
 private function persistStep(int $runId,array $r):void{$s=Database::connection()->prepare("INSERT INTO system_package_pipeline_steps(run_id,step_code,step_label,step_order,status,decision,score,critical,message,issues_json,warnings_json,details_json,duration_ms,created_at) VALUES(:run,:code,:label,:ord,:status,:decision,:score,:critical,:message,:issues,:warnings,:details,:duration,NOW())");$s->execute(['run'=>$runId,'code'=>(string)$r['code'],'label'=>(string)$r['label'],'ord'=>(int)$r['order'],'status'=>(string)$r['status'],'decision'=>(string)$r['decision'],'score'=>(int)$r['score'],'critical'=>!empty($r['critical'])?1:0,'message'=>(string)$r['message'],'issues'=>json_encode($r['issues']??[],JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),'warnings'=>json_encode($r['warnings']??[],JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),'details'=>json_encode($r['details']??[],JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),'duration'=>(int)$r['duration_ms']]);}
 private function finishRun(int $id,array $s):void{$q=Database::connection()->prepare("UPDATE system_package_pipeline_runs SET status=:status,decision=:decision,score=:score,blocking_step=:blocking,summary_json=:summary,duration_ms=:duration,finished_at=NOW() WHERE id=:id");$q->execute(['status'=>$s['ok']?'COMPLETED':'BLOCKED','decision'=>(string)$s['decision'],'score'=>(int)$s['score'],'blocking'=>$s['blocking_step'],'summary'=>json_encode($s,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),'duration'=>(int)$s['duration_ms'],'id'=>$id]);}
}
