<?php
declare(strict_types=1);
namespace PpPhp\Core\Projects;

final class ProjectVersionService
{
    public function __construct(private ?string $storageRoot=null){$this->storageRoot??=(defined('BASE_PATH')?constant('BASE_PATH'):getcwd()).'/storage/project-versions';}
    public function checkpoint(int $ownerId,array $project,string $projectDir,array $connection,string $label,string $notes='',string $kind='manual'):array
    {
        $label=trim($label);if($label===''||mb_strlen($label)>120)throw new \InvalidArgumentException('Checkpoint label is required and must not exceed 120 characters.');if(mb_strlen($notes)>2000)throw new \InvalidArgumentException('Checkpoint notes must not exceed 2,000 characters.');
        $export=(new ProjectPackageExporter())->export($project,$projectDir,$connection,['include_custom'=>true,'include_sqlite'=>false,'capture_schema'=>true]);$dir=rtrim((string)$this->storageRoot,'/').'/'.$ownerId.'/'.(int)$project['id'];if(!is_dir($dir)&&!mkdir($dir,0700,true)&&!is_dir($dir)){@unlink($export['path']);throw new \RuntimeException('Could not prepare protected checkpoint storage.');}$path=$dir.'/'.date('Ymd-His').'-'.bin2hex(random_bytes(6)).'.ppphp';if(!rename($export['path'],$path)){@unlink($export['path']);throw new \RuntimeException('Could not store the checkpoint.');}@chmod($path,0600);
        try{$id=ProjectVersionCatalog::create(['project_id'=>$project['id'],'owner_id'=>$ownerId,'label'=>$label,'notes'=>$notes,'kind'=>$kind,'package_path'=>$path,'manifest'=>$export['manifest'],'package_size'=>$export['size']]);}catch(\Throwable $e){@unlink($path);throw $e;}return ['id'=>$id,'path'=>$path,'manifest'=>$export['manifest'],'size'=>$export['size']];
    }
    public function compare(array $version,array $project,string $projectDir,array $connection):array
    {
        if(!is_file($version['package_path']))throw new \RuntimeException('The checkpoint package is missing.',404);$current=(new ProjectPackageExporter())->export($project,$projectDir,$connection,['include_custom'=>true,'include_sqlite'=>false,'capture_schema'=>true]);
        try{$before=(array)($version['manifest']['checksums']??[]);$after=(array)($current['manifest']['checksums']??[]);$added=$changed=$removed=[];foreach($after as $path=>$hash){if(!isset($before[$path]))$added[]=$path;elseif(!hash_equals((string)$before[$path],(string)$hash))$changed[]=$path;}foreach($before as $path=>$hash)if(!isset($after[$path]))$removed[]=$path;return ['added'=>$this->describe($added),'changed'=>$this->describe($changed),'removed'=>$this->describe($removed),'counts'=>['added'=>count($added),'changed'=>count($changed),'removed'=>count($removed)],'clean'=>!$added&&!$changed&&!$removed];}finally{@unlink($current['path']);}
    }
    public function restore(array $version,string $projectDir):array
    {
        if(!is_file($version['package_path']))throw new \RuntimeException('The checkpoint package is missing.',404);(new ProjectPackageInspector())->inspectArchive($version['package_path']);$parent=dirname($projectDir);$stage=$parent.'/.restore-stage-'.bin2hex(random_bytes(6));$backup=$parent.'/.restore-backup-'.bin2hex(random_bytes(6));$importer=new ProjectPackageImporter();$importer->import($version['package_path'],$stage,true);mkdir($backup.'/config',0700,true);
        $currentConfig=$projectDir.'/config';$stageConfig=$stage.'/config';$currentCustom=$projectDir.'/custom';$stageCustom=$stage.'/custom';$backupCustom=$backup.'/custom';
        try{
            foreach(glob($currentConfig.'/*.json')?:[] as $file)if(!copy($file,$backup.'/config/'.basename($file)))throw new \RuntimeException('Could not prepare configuration rollback.');
            if(is_dir($currentCustom)&&!rename($currentCustom,$backupCustom))throw new \RuntimeException('Could not prepare Custom Files rollback.');
            $snapshotNames=[];foreach(glob($stageConfig.'/*.json')?:[] as $file){$snapshotNames[]=basename($file);$tmp=tempnam($currentConfig,'.restore-');if($tmp===false||!copy($file,$tmp)||!rename($tmp,$currentConfig.'/'.basename($file)))throw new \RuntimeException('Could not restore '.basename($file).'.');}
            foreach(glob($currentConfig.'/*.json')?:[] as $file)if(!in_array(basename($file),$snapshotNames,true))@unlink($file);
            if(is_dir($stageCustom)&&!rename($stageCustom,$currentCustom))throw new \RuntimeException('Could not restore Custom Files.');
            $this->removeDirectory($backup);$this->removeDirectory($stage);return ['files'=>count($snapshotNames),'custom_restored'=>is_dir($currentCustom)];
        }catch(\Throwable $e){foreach(glob($currentConfig.'/*.json')?:[] as $file)@unlink($file);foreach(glob($backup.'/config/*.json')?:[] as $file)@copy($file,$currentConfig.'/'.basename($file));if(is_dir($currentCustom))$this->removeDirectory($currentCustom);if(is_dir($backupCustom))@rename($backupCustom,$currentCustom);$this->removeDirectory($backup);$this->removeDirectory($stage);throw $e;}
    }
    public function deletePackage(array $version):void{$path=(string)$version['package_path'];$root=realpath((string)$this->storageRoot);$real=is_file($path)?realpath($path):false;if($root!==false&&$real!==false&&str_starts_with($real,rtrim($root,DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR)&&!unlink($real))throw new \RuntimeException('Could not remove the checkpoint package.');}
    private function describe(array $paths):array{return array_map(static function(string $path):array{$name=basename($path);$label=str_starts_with($path,'project/config/')?ucwords(str_replace(['-','_'],[' ',' '],pathinfo($name,PATHINFO_FILENAME))).' settings':(str_starts_with($path,'project/custom/')?'Custom Files · '.substr($path,15):$path);return ['path'=>$path,'label'=>$label];},$paths);}
    private function removeDirectory(string $dir):void{if(!is_dir($dir))return;$it=new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir,\FilesystemIterator::SKIP_DOTS),\RecursiveIteratorIterator::CHILD_FIRST);foreach($it as $item)$item->isDir()?@rmdir($item->getPathname()):@unlink($item->getPathname());@rmdir($dir);}
}
