<?php

function cmsBackupDirectory($rootPath)
{
    $rootPath = rtrim((string) $rootPath, '/\\');
    return dirname($rootPath) . '/.' . basename($rootPath) . '-private/backups';
}

function cmsDatabaseResolveMaintenanceTable($db,$value)
{
    $value=trim((string)$value);if($value===''||!preg_match('/^[a-zA-Z0-9_]+$/',$value))return'';
    foreach($db->fetchAll('SHOW TABLE STATUS')as$row){if(isset($row['Name'])&&hash_equals((string)$row['Name'],$value))return$value;}return'';
}

function cmsBackupSafeFilename($filename)
{
    $filename=basename((string)$filename);return preg_match('/^[a-zA-Z0-9_.-]+\.sql$/',$filename)?$filename:'';
}

function cmsBackupExportValue($value)
{
    if($value===null)return'NULL';return"'".str_replace(["\\","\0","\r","\n","\t","\x1a","'"],["\\\\","\\0","\\r","\\n","\\t","\\Z","\\'"],(string)$value)."'";
}

function cmsCreateDatabaseSnapshot($db,$directory,$type='manual')
{
    if(!is_dir($directory)&&!@mkdir($directory,0755,true)&&!is_dir($directory))return['success'=>false,'message'=>'备份目录不可写'];$config=$db->getConfig();$filename=$config['database'].'_'.date('Ymd_His').'_'.strtoupper(substr(hash('sha256',uniqid('',true)),0,6)).'.sql';$path=$directory.'/'.$filename;$handle=@fopen($path,'xb');if(!$handle)return['success'=>false,'message'=>'无法创建备份文件'];$ok=true;
    try{fwrite($handle,"-- EnterpriseCMS Database Backup\n-- Type: {$type}\n-- Date: ".date('c')."\nSET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS=0;\n");$tables=$db->fetchAll("SHOW FULL TABLES WHERE Table_type='BASE TABLE'");foreach($tables as$tableRow){$table=array_values($tableRow)[0];$create=$db->fetch("SHOW CREATE TABLE `{$table}`");$createSql=array_values($create)[1];if(fwrite($handle,"\nDROP TABLE IF EXISTS `{$table}`;\n{$createSql};\n")===false){$ok=false;break;}$stmt=$db->query("SELECT * FROM `{$table}`");while(($row=$stmt->fetch())!==false){$fields=array_keys($row);$values=array_map('cmsBackupExportValue',array_values($row));$sql="INSERT INTO `{$table}` (`".implode('`,`',$fields)."`) VALUES (".implode(',',$values).");\n";if(fwrite($handle,$sql)===false){$ok=false;break 2;}}}if($ok)fwrite($handle,"SET FOREIGN_KEY_CHECKS=1;\n");}catch(Throwable$e){$ok=false;error_log('EnterpriseCMS snapshot failed: '.$e->getMessage());}fclose($handle);if(!$ok||!is_file($path)||filesize($path)<100){@unlink($path);return['success'=>false,'message'=>'备份生成失败'];}cmsWriteBackupManifest($path,$type);return['success'=>true,'filename'=>$filename,'path'=>$path,'size'=>filesize($path)];
}

function cmsWriteBackupManifest($path,$type='manual')
{
    if(!is_file($path))return false;$data=['filename'=>basename($path),'sha256'=>hash_file('sha256',$path),'size'=>filesize($path),'created_at'=>time(),'type'=>$type,'php_version'=>PHP_VERSION];return file_put_contents($path.'.meta.json',json_encode($data,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT),LOCK_EX)!==false;
}

function cmsReadBackupManifest($path)
{
    $file=$path.'.meta.json';$data=is_file($file)?json_decode((string)file_get_contents($file),true):null;return is_array($data)?$data:null;
}

function cmsSplitSqlStatements($sql,$limit=200000)
{
    $statements=[];$buffer='';$quote='';$escaped=false;$lineComment=false;$blockComment=false;$length=strlen($sql);
    for($i=0;$i<$length;$i++){$char=$sql[$i];$next=$i+1<$length?$sql[$i+1]:'';if($lineComment){if($char==="\n"){$lineComment=false;$buffer.=$char;}continue;}if($blockComment){if($char==='*'&&$next==='/'){$blockComment=false;$i++;}continue;}if($quote===''){if(($char==='-'&&$next==='-'&&($i+2>=$length||ctype_space($sql[$i+2])))||$char==='#'){$lineComment=true;if($char==='-')$i++;continue;}if($char==='/'&&$next==='*'){$blockComment=true;$i++;continue;}if($char==="'"||$char==='"'||$char==='`'){$quote=$char;$buffer.=$char;continue;}if($char===';'){$statement=trim($buffer);if($statement!=='')$statements[]=$statement;$buffer='';if(count($statements)>$limit)throw new RuntimeException('SQL 语句数量超过限制');continue;}}else{if($escaped){$escaped=false;$buffer.=$char;continue;}if($char==='\\'){$escaped=true;$buffer.=$char;continue;}if($char===$quote)$quote='';}$buffer.=$char;}
    if($quote!==''||$blockComment)throw new RuntimeException('SQL 文件存在未闭合的引号或注释');$last=trim($buffer);if($last!=='')$statements[]=$last;return$statements;
}

function cmsBackupStatementAllowed($statement)
{
    $statement=ltrim((string)$statement);if(preg_match('/\b(?:OUTFILE|DUMPFILE|LOAD\s+DATA|CREATE\s+USER|ALTER\s+USER|DROP\s+USER|GRANT\s+|REVOKE\s+|INSTALL\s+PLUGIN|UNINSTALL\s+PLUGIN)\b/i',$statement)||preg_match('/^(?:SOURCE|DELIMITER)\b/i',$statement))return false;return(bool)preg_match('/^(?:SET\b|USE\b|DROP\s+TABLE\b|CREATE\s+TABLE\b|ALTER\s+TABLE\b|INSERT\s+INTO\b|REPLACE\s+INTO\b|LOCK\s+TABLES\b|UNLOCK\s+TABLES\b)/i',$statement);
}

function cmsBackupPreflight($directory,$filename)
{
    $filename=cmsBackupSafeFilename($filename);if($filename==='')return['success'=>false,'message'=>'备份文件名不安全'];$base=realpath($directory);$path=realpath($directory.'/'.$filename);if(!$base||!$path||!is_file($path)||strpos(strtolower($path),strtolower($base.DIRECTORY_SEPARATOR))!==0)return['success'=>false,'message'=>'备份文件不存在'];$size=filesize($path);if($size<100||$size>1024*1024*1024)return['success'=>false,'message'=>'备份文件大小异常'];$manifest=cmsReadBackupManifest($path);$hash=hash_file('sha256',$path);$integrity=$manifest?hash_equals((string)($manifest['sha256']??''),$hash):null;if($integrity===false)return['success'=>false,'message'=>'备份哈希不匹配，文件可能已损坏','integrity'=>'failed'];$sql=file_get_contents($path);try{$statements=cmsSplitSqlStatements($sql);}catch(Throwable$e){return['success'=>false,'message'=>$e->getMessage(),'integrity'=>$integrity?'verified':'untracked'];}$blocked=[];$allowed=[];foreach($statements as$index=>$statement){if(cmsBackupStatementAllowed($statement))$allowed[]=$statement;else$blocked[]=$index+1;}if($blocked)return['success'=>false,'message'=>'发现不允许执行的 SQL 语句：'.implode(',',array_slice($blocked,0,10)),'integrity'=>$integrity?'verified':'untracked'];if(!$allowed)return['success'=>false,'message'=>'未发现可恢复的 SQL 语句'];return['success'=>true,'message'=>'预检通过','path'=>$path,'filename'=>$filename,'size'=>$size,'sha256'=>$hash,'integrity'=>$integrity?'verified':'untracked','statements'=>$allowed,'statement_count'=>count($allowed),'manifest'=>$manifest];
}

function cmsRestoreDatabaseBackup($db,array $preflight)
{
    if(empty($preflight['success'])||empty($preflight['statements']))return['success'=>false,'message'=>'恢复预检未通过','executed'=>0];$executed=0;try{foreach($preflight['statements']as$statement){$db->query($statement);$executed++;}return['success'=>true,'message'=>'数据库恢复完成','executed'=>$executed];}catch(Throwable$e){error_log('EnterpriseCMS restore failed: '.$e->getMessage());return['success'=>false,'message'=>'恢复在第 '.($executed+1).' 条语句失败，请使用恢复前快照回退','executed'=>$executed];}
}

function cmsListDatabaseBackups($directory)
{
    $items=[];foreach((array)glob(rtrim($directory,'/\\').'/*.sql')as$path){$manifest=cmsReadBackupManifest($path);$hash=$manifest?hash_file('sha256',$path):'';$items[]=['name'=>basename($path),'path'=>$path,'size'=>filesize($path),'time'=>filemtime($path),'manifest'=>$manifest,'integrity'=>$manifest?(hash_equals((string)($manifest['sha256']??''),$hash)?'verified':'failed'):'untracked','type'=>$manifest['type']??'legacy'];}usort($items,function($a,$b){return$b['time']<=>$a['time'];});return$items;
}

function cmsCleanupDatabaseBackups($directory,$keep)
{
    $keep=max(1,min(100,intval($keep)));$items=array_values(array_filter(cmsListDatabaseBackups($directory),function($item){return($item['type']??'')!=='pre_restore';}));$deleted=[];foreach(array_slice($items,$keep)as$item){if(@unlink($item['path'])){@unlink($item['path'].'.meta.json');$deleted[]=$item['name'];}}return$deleted;
}

function cmsCleanupTaskCenterBackups($directory,$keep)
{
    $keep=max(1,min(100,intval($keep)));$items=array_values(array_filter(cmsListDatabaseBackups($directory),function($item){return($item['type']??'')==='task_center';}));$deleted=[];
    foreach(array_slice($items,$keep)as$item){if(@unlink($item['path'])){@unlink($item['path'].'.meta.json');$deleted[]=$item['name'];}}
    return$deleted;
}
