<?php

declare(strict_types=1);
const DOWNLOAD_ROOT=__DIR__.'/../downloads';const DATA_ROOT=__DIR__.'/../data';const SCREENSHOT_ROOT=__DIR__.'/../uploads/screenshots';
if(session_status()!==PHP_SESSION_ACTIVE)session_start();
function ensureData():void{if(!is_dir(DATA_ROOT))mkdir(DATA_ROOT,0755,true);foreach(['releases.json'=>'[]','settings.json'=>'{}'] as $f=>$d)if(!file_exists(DATA_ROOT.'/'.$f))file_put_contents(DATA_ROOT.'/'.$f,$d,LOCK_EX);}
function readJson(string $name,array $default=[]):array{ensureData();$raw=@file_get_contents(DATA_ROOT.'/'.$name);$v=json_decode((string)$raw,true);return is_array($v)?$v:$default;}
function writeJson(string $name,array $data):void{ensureData();$tmp=DATA_ROOT.'/'.$name.'.tmp';file_put_contents($tmp,json_encode($data,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES),LOCK_EX);rename($tmp,DATA_ROOT.'/'.$name);}
function releasesRaw():array{return readJson('releases.json',[]);}function saveReleases(array $r):void{writeJson('releases.json',array_values($r));}
function setting(string $key,?string $default=null):?string{$s=readJson('settings.json',[]);return isset($s[$key])?(string)$s[$key]:$default;}function setSetting(string $key,string $value):void{$s=readJson('settings.json',[]);$s[$key]=$value;writeJson('settings.json',$s);}function adminConfigured():bool{return(bool)setting('admin_password_hash');}function adminLoggedIn():bool{return!empty($_SESSION['phantom_admin']);}function requireAdmin():void{if(!adminLoggedIn()){header('Location: login.php');exit;}}function csrfToken():string{if(empty($_SESSION['csrf']))$_SESSION['csrf']=bin2hex(random_bytes(24));return$_SESSION['csrf'];}function checkCsrf():void{$t=(string)($_POST['csrf']??'');if(!$t||!hash_equals((string)($_SESSION['csrf']??''),$t)){http_response_code(403);exit('Invalid security token.');}}
function humanFileSize(int $b):string{if($b<1024)return$b.' B';$u=['KB','MB','GB','TB'];$s=$b/1024;foreach($u as$x){if($s<1024||$x==='TB')return number_format($s,$s>=10?1:2).' '.$x;$s/=1024;}return number_format($s,2).' TB';}
function titleFromFilename(string $f):string{$n=pathinfo($f,PATHINFO_FILENAME);$n=preg_replace('/[_-]+/',' ',$n)??$n;return ucwords(trim($n));}
function safeDownloadPath(string $r):?string{$root=realpath(DOWNLOAD_ROOT);if($root===false)return null;$c=realpath($root.DIRECTORY_SEPARATOR.str_replace(['/','\\'],DIRECTORY_SEPARATOR,$r));if($c===false||!is_file($c))return null;$nr=rtrim(str_replace('\\','/',$root),'/').'/';$nc=str_replace('\\','/',$c);return str_starts_with($nc,$nr)?$c:null;}
function relativeFileInfo(string $r):?array{$p=safeDownloadPath($r);if(!$p)return null;$seg=explode('/',str_replace('\\','/',$r));$f=basename($p);return['filename'=>$f,'path'=>str_replace('\\','/',$r),'platform'=>strtolower($seg[0]??'other'),'game'=>strtolower($seg[1]??''),'extension'=>strtolower(pathinfo($f,PATHINFO_EXTENSION)),'size'=>humanFileSize((int)filesize($p)),'bytes'=>(int)filesize($p),'modified'=>(int)filemtime($p),'modified_label'=>date('d M Y',(int)filemtime($p))];}
function syncDownloads():void{$root=realpath(DOWNLOAD_ROOT);if($root===false)return;$rows=releasesRaw();$by=[];$max=0;foreach($rows as$r){$by[$r['file_path']]=$r;$max=max($max,(int)$r['id']);}$it=new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root,FilesystemIterator::SKIP_DOTS));$changed=false;foreach($it as$f){if(!$f->isFile())continue;$full=$f->getRealPath();if(!$full)continue;$rel=ltrim(str_replace('\\','/',substr($full,strlen($root))),'/');if(isset($by[$rel]))continue;$seg=explode('/',$rel);$fn=array_pop($seg);$max++;$by[$rel]=['id'=>$max,'file_path'=>$rel,'title'=>titleFromFilename($fn),'description'=>'','platform'=>strtolower($seg[0]??'other'),'game'=>strtolower($seg[1]??''),'tags'=>'','screenshot'=>'','featured'=>0,'downloads'=>0,'created_at'=>$f->getMTime(),'updated_at'=>time()];$changed=true;}if($changed)saveReleases(array_values($by));}
function hydrate(array $r):?array{$f=relativeFileInfo($r['file_path']);if(!$f)return null;return array_merge($r,$f,['tags_array'=>array_values(array_filter(array_map('trim',explode(',',(string)$r['tags'])))),'screenshot_url'=>$r['screenshot']?'uploads/screenshots/'.rawurlencode(basename($r['screenshot'])):'']);}
function filterRows(array $o=[]):array{syncDownloads();$rows=[];$q=strtolower(trim((string)($o['search']??'')));$p=strtolower((string)($o['platform']??'all'));foreach(releasesRaw() as$r){if(!empty($o['featured'])&&!$r['featured'])continue;if($p!=='all'&&$p!==''&&strtolower($r['platform'])!==$p)continue;if($q!==''){$hay=strtolower(implode(' ',[$r['title'],$r['description'],$r['tags'],$r['game'],$r['file_path']]));if(!str_contains($hay,$q))continue;}$h=hydrate($r);if($h)$rows[]=$h;}$sort=$o['sort']??'newest';usort($rows,function($a,$b)use($sort){return match($sort){'name'=>strcasecmp($a['title'],$b['title']),'popular'=>(int)$b['downloads']<=>(int)$a['downloads']?:((int)$b['updated_at']<=>(int)$a['updated_at']),'oldest'=>(int)$a['created_at']<=>(int)$b['created_at'],default=>(int)$b['featured']<=>(int)$a['featured']?:((int)$b['updated_at']<=>(int)$a['updated_at'])};});return$rows;}
function getReleases(array $o=[]):array{$rows=filterRows($o);$off=max(0,(int)($o['offset']??0));return isset($o['limit'])?array_slice($rows,$off,max(1,(int)$o['limit'])):array_slice($rows,$off);}function countReleases(array $o=[]):int{return count(filterRows($o));}function getRelease(int $id):?array{syncDownloads();foreach(releasesRaw() as$r)if((int)$r['id']===$id)return hydrate($r);return null;}function platforms():array{$p=[];foreach(filterRows() as$r)$p[$r['platform']]=true;$p=array_keys($p);sort($p);return$p;}function totalDownloads():int{$n=0;foreach(releasesRaw()as$r)$n+=(int)$r['downloads'];return$n;}
function updateRelease(int $id,array $changes):void{$rows=releasesRaw();foreach($rows as&$r)if((int)$r['id']===$id){$r=array_merge($r,$changes);break;}unset($r);saveReleases($rows);}function deleteReleaseRecord(int $id):void{$r=array_values(array_filter(releasesRaw(),fn($x)=>(int)$x['id']!==$id));saveReleases($r);}function incrementDownload(int $id):void{$rows=releasesRaw();foreach($rows as&$r)if((int)$r['id']===$id){$r['downloads']=(int)$r['downloads']+1;break;}unset($r);saveReleases($rows);}
function uploadScreenshot(array $f,string $existing=''):string{if(($f['error']??UPLOAD_ERR_NO_FILE)===UPLOAD_ERR_NO_FILE)return$existing;if(($f['error']??UPLOAD_ERR_OK)!==UPLOAD_ERR_OK)throw new RuntimeException('Screenshot upload failed.');if(($f['size']??0)>5*1024*1024)throw new RuntimeException('Screenshot must be under 5 MB.');$fi=new finfo(FILEINFO_MIME_TYPE);$m=$fi->file($f['tmp_name']);$a=['image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp'];if(!isset($a[$m]))throw new RuntimeException('Screenshot must be JPG, PNG or WEBP.');if(!is_dir(SCREENSHOT_ROOT))mkdir(SCREENSHOT_ROOT,0755,true);$n=bin2hex(random_bytes(12)).'.'.$a[$m];if(!move_uploaded_file($f['tmp_name'],SCREENSHOT_ROOT.'/'.$n))throw new RuntimeException('Could not save screenshot.');if($existing&&is_file(SCREENSHOT_ROOT.'/'.basename($existing)))@unlink(SCREENSHOT_ROOT.'/'.basename($existing));return$n;}
function uploadReleaseFile(array $f,string $platform,string $game):string{if(($f['error']??UPLOAD_ERR_NO_FILE)!==UPLOAD_ERR_OK)throw new RuntimeException('Choose a download file first.');if(($f['size']??0)>1024*1024*1024)throw new RuntimeException('File exceeds the 1 GB application limit.');$n=preg_replace('/[^A-Za-z0-9._()\- ]+/','_',basename((string)$f['name']))?:'download.bin';$platform=preg_replace('/[^a-z0-9_-]+/i','',strtolower($platform))?:'other';$game=preg_replace('/[^a-z0-9_-]+/i','',strtolower($game));$dir=DOWNLOAD_ROOT.'/'.$platform.($game?'/'.$game:'');if(!is_dir($dir))mkdir($dir,0755,true);$t=$dir.'/'.$n;if(file_exists($t)){$b=pathinfo($n,PATHINFO_FILENAME);$e=pathinfo($n,PATHINFO_EXTENSION);$n=$b.'-'.date('Ymd-His').($e?'.'.$e:'');$t=$dir.'/'.$n;}if(!move_uploaded_file($f['tmp_name'],$t))throw new RuntimeException('Could not save download file.');$root=realpath(DOWNLOAD_ROOT);return ltrim(str_replace('\\','/',substr(realpath($t),strlen($root))),'/');}
