<?php
// BoardGameStars Währungs-Admin v2.8: Admin-Codes erzeugen und Wallets direkt korrigieren.
$configFile = __DIR__ . '/api/config.php';
if (!is_file($configFile)) { http_response_code(500); echo 'api/config.php fehlt.'; exit; }
$config = require $configFile;
$secureCookie = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
if (PHP_VERSION_ID >= 70300) session_set_cookie_params(['lifetime'=>0,'path'=>'/','secure'=>$secureCookie,'httponly'=>true,'samesite'=>'Lax']);
session_start();
function h($v): string { return htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
function json_out($payload, int $status=200): void { http_response_code($status); header('Content-Type: application/json; charset=utf-8'); echo json_encode($payload, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); exit; }
function db(array $config): PDO { $file=(string)($config['db_file'] ?? (__DIR__.'/data/boardgamestars.sqlite')); $dir=dirname($file); if(!is_dir($dir)) @mkdir($dir,0755,true); $pdo=new PDO('sqlite:'.$file,null,null,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC]); $pdo->exec('PRAGMA busy_timeout=5000'); install_schema($pdo); return $pdo; }
function ensure_columns(PDO $pdo, string $table, array $columns): void { $existing=[]; try{$rows=$pdo->query('PRAGMA table_info('.$table.')')->fetchAll();}catch(Throwable $e){$rows=[];} foreach($rows as $row)$existing[(string)$row['name']]=true; foreach($columns as $name=>$def) if(!isset($existing[$name])) $pdo->exec('ALTER TABLE '.$table.' ADD COLUMN '.$name.' '.$def); }
function install_schema(PDO $pdo): void {
    $pdo->exec("CREATE TABLE IF NOT EXISTS players (player_id TEXT PRIMARY KEY, secret_hash TEXT NOT NULL, public_name TEXT NOT NULL, avatar TEXT DEFAULT '', silver_balance INTEGER NOT NULL DEFAULT 0, gold_balance INTEGER NOT NULL DEFAULT 0, elo INTEGER NOT NULL DEFAULT 1000, last_daily_bonus_date TEXT NOT NULL DEFAULT '', base_bonus_granted_at INTEGER NOT NULL DEFAULT 0, gold_code_redeem_count INTEGER NOT NULL DEFAULT 0, last_gold_code_redeemed_at INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)");
    ensure_columns($pdo,'players',['is_network_player'=>'INTEGER NOT NULL DEFAULT 0','locked_identity'=>'INTEGER NOT NULL DEFAULT 0','stats_visible'=>'INTEGER NOT NULL DEFAULT 1','last_seen_at'=>'INTEGER NOT NULL DEFAULT 0','wins'=>'INTEGER NOT NULL DEFAULT 0','losses'=>'INTEGER NOT NULL DEFAULT 0','games_played'=>'INTEGER NOT NULL DEFAULT 0']);
    $pdo->exec("CREATE TABLE IF NOT EXISTS ledger (id TEXT PRIMARY KEY, ts INTEGER NOT NULL, player_id TEXT NOT NULL, type TEXT NOT NULL, currency TEXT NOT NULL, amount INTEGER NOT NULL, reason TEXT NOT NULL, related_id TEXT DEFAULT '', meta TEXT DEFAULT '')");
    $pdo->exec("CREATE INDEX IF NOT EXISTS ledger_player_ts ON ledger(player_id, ts DESC)");
    $pdo->exec("CREATE TABLE IF NOT EXISTS admin_star_codes (code_hash TEXT PRIMARY KEY, code_prefix TEXT NOT NULL, currency TEXT NOT NULL DEFAULT 'gold', amount INTEGER NOT NULL DEFAULT 0, max_redemptions INTEGER NOT NULL DEFAULT 1, redeemed_count INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'active', note TEXT DEFAULT '', created_by TEXT DEFAULT 'admin', created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL DEFAULT 0, last_redeemed_at INTEGER NOT NULL DEFAULT 0)");
    $pdo->exec("CREATE INDEX IF NOT EXISTS admin_star_codes_prefix ON admin_star_codes(code_prefix)");
    $pdo->exec("CREATE TABLE IF NOT EXISTS admin_star_code_redemptions (id TEXT PRIMARY KEY, ts INTEGER NOT NULL, code_hash TEXT NOT NULL, code_prefix TEXT NOT NULL, player_id TEXT NOT NULL, currency TEXT NOT NULL, amount INTEGER NOT NULL, meta TEXT DEFAULT '')");
    $pdo->exec("CREATE INDEX IF NOT EXISTS admin_star_redemptions_player_ts ON admin_star_code_redemptions(player_id, ts DESC)");
}
function body(): array { $raw=file_get_contents('php://input'); $data=json_decode($raw?:'',true); return is_array($data)?$data:$_POST; }
function normalize_code($code): string { $code=strtoupper((string)$code); $code=preg_replace('/[^A-Z0-9]/','',$code); return substr($code??'',0,80); }
function code_hash(string $code, array $config): string { return hash_hmac('sha256', normalize_code($code), (string)($config['server_secret'] ?? '')); }
function amount($v, int $min=1, int $max=10000000): int { if(is_string($v))$v=trim($v); if(!is_numeric($v))return 0; $n=(int)floor((float)$v); return ($n>=$min&&$n<=$max)?$n:0; }
function make_code(string $currency): string { $prefix=$currency==='silver'?'BGS-SIL':'BGS-GOLD'; $alphabet='ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; $parts=[]; for($p=0;$p<3;$p++){ $s=''; for($i=0;$i<4;$i++) $s.=$alphabet[random_int(0,strlen($alphabet)-1)]; $parts[]=$s; } return $prefix.'-'.implode('-',$parts); }
function create_codes(PDO $pdo, array $config, array $data): array {
    $currency=strtolower((string)($data['currency']??'gold')); if(!in_array($currency,['silver','gold'],true)) return ['status'=>'invalid_currency'];
    $amt=amount($data['amount']??0,1,10000000); $count=amount($data['count']??1,1,200); $maxRed=amount($data['maxRedemptions']??1,1,10000);
    if($amt<=0||$count<=0) return ['status'=>'invalid_amount'];
    $expiresDays=(int)($data['expiresDays']??0); $expiresAt=$expiresDays>0 ? time()+($expiresDays*86400) : 0;
    $note=trim(substr((string)($data['note']??''),0,160)); $now=time(); $codes=[]; $stmt=$pdo->prepare('INSERT INTO admin_star_codes (code_hash, code_prefix, currency, amount, max_redemptions, redeemed_count, status, note, created_by, created_at, expires_at, last_redeemed_at) VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, 0)');
    for($i=0;$i<$count;$i++){ do{ $code=make_code($currency); $hash=code_hash($code,$config); $exists=$pdo->prepare('SELECT 1 FROM admin_star_codes WHERE code_hash=?'); $exists->execute([$hash]); } while($exists->fetch()); $prefix=substr(normalize_code($code),0,12); $stmt->execute([$hash,$prefix,$currency,$amt,$maxRed,'active',$note,'admin_currency',$now,$expiresAt]); $codes[]=['code'=>$code,'currency'=>$currency,'amount'=>$amt,'maxRedemptions'=>$maxRed,'expiresAt'=>$expiresAt,'note'=>$note]; }
    return ['status'=>'ok','codes'=>$codes];
}
function adjust_wallet(PDO $pdo, array $data): array {
    $playerId=trim((string)($data['playerId']??'')); if(!preg_match('/^[A-Za-z0-9_\-]{3,100}$/',$playerId)) return ['status'=>'invalid_player_id'];
    $silver=(int)($data['silverDelta']??0); $gold=(int)($data['goldDelta']??0); if($silver===0&&$gold===0)return ['status'=>'no_change'];
    if(abs($silver)>10000000||abs($gold)>10000000)return ['status'=>'amount_too_large'];
    $reason=trim(substr((string)($data['reason']??'Admin-Korrektur'),0,140)); if($reason==='')$reason='Admin-Korrektur'; $now=time();
    $pdo->exec('BEGIN IMMEDIATE');
    try{ $stmt=$pdo->prepare('SELECT * FROM players WHERE player_id=? LIMIT 1'); $stmt->execute([$playerId]); $row=$stmt->fetch(); if(!$row){$pdo->exec('ROLLBACK'); return ['status'=>'player_not_found'];}
        $newSilver=(int)$row['silver_balance']+$silver; $newGold=(int)$row['gold_balance']+$gold; if($newSilver<0||$newGold<0){$pdo->exec('ROLLBACK'); return ['status'=>'balance_would_be_negative','silverBalance'=>(int)$row['silver_balance'],'goldBalance'=>(int)$row['gold_balance']];}
        $pdo->prepare('UPDATE players SET silver_balance=?, gold_balance=?, updated_at=? WHERE player_id=?')->execute([$newSilver,$newGold,$now,$playerId]);
        $meta=json_encode(['source'=>'admin_currency','reason'=>$reason],JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
        if($silver!==0)$pdo->prepare('INSERT INTO ledger (id, ts, player_id, type, currency, amount, reason, related_id, meta) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')->execute([bin2hex(random_bytes(12)),$now,$playerId,$silver>0?'credit':'debit','silver',abs($silver),'admin_wallet_adjust','admin_currency',$meta]);
        if($gold!==0)$pdo->prepare('INSERT INTO ledger (id, ts, player_id, type, currency, amount, reason, related_id, meta) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')->execute([bin2hex(random_bytes(12)),$now,$playerId,$gold>0?'credit':'debit','gold',abs($gold),'admin_wallet_adjust','admin_currency',$meta]);
        $pdo->exec('COMMIT'); return ['status'=>'ok','playerId'=>$playerId,'silverBalance'=>$newSilver,'goldBalance'=>$newGold];
    } catch(Throwable $e){ if($pdo->inTransaction())$pdo->rollBack(); throw $e; }
}
function overview(PDO $pdo): array {
    $players=$pdo->query("SELECT player_id, public_name, silver_balance, gold_balance, elo, is_network_player, locked_identity, updated_at FROM players WHERE stats_visible=1 ORDER BY updated_at DESC LIMIT 5000")->fetchAll();
    $codes=$pdo->query("SELECT c.*, COALESCE(r.redemptions,0) redemptions, COALESCE(r.last_player,'') last_player FROM admin_star_codes c LEFT JOIN (SELECT code_hash, COUNT(*) redemptions, MAX(player_id) last_player FROM admin_star_code_redemptions GROUP BY code_hash) r ON r.code_hash=c.code_hash ORDER BY c.created_at DESC LIMIT 300")->fetchAll();
    $actions=$pdo->query("SELECT l.*, p.public_name FROM ledger l LEFT JOIN players p ON p.player_id=l.player_id WHERE l.reason IN ('admin_wallet_adjust','admin_code_redeem') ORDER BY l.ts DESC LIMIT 300")->fetchAll();
    return ['status'=>'ok','players'=>$players,'codes'=>$codes,'actions'=>$actions,'now'=>time()];
}
$adminPasswordHash=(string)($config['admin_stats_password_hash']??''); $authError='';
if(isset($_GET['logout'])){$_SESSION=[];session_destroy();header('Location: admin_currency.php');exit;}
if(($_SERVER['REQUEST_METHOD']??'GET')==='POST' && isset($_POST['admin_password'])){ $pw=(string)$_POST['admin_password']; if($adminPasswordHash!=='' && password_verify($pw,$adminPasswordHash)){session_regenerate_id(true);$_SESSION['bgs_stats_admin']=true;header('Location: admin_currency.php');exit;} $authError='Falsches Passwort.'; }
$authenticated=!empty($_SESSION['bgs_stats_admin']);
if($authenticated && isset($_GET['ajax'])){ try{ $pdo=db($config); $data=body(); $ajax=(string)$_GET['ajax']; if($ajax==='overview')json_out(overview($pdo)); if($ajax==='create_codes')json_out(create_codes($pdo,$config,$data)); if($ajax==='adjust_wallet')json_out(adjust_wallet($pdo,$data)); json_out(['status'=>'unknown_action'],404);} catch(Throwable $e){error_log('BGS currency admin error: '.$e->getMessage());json_out(['status'=>'temporary_unavailable','message'=>$e->getMessage()],500);} }
if(!$authenticated && isset($_GET['ajax'])) json_out(['status'=>'forbidden'],403);
?><!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>BoardGameStars Währungsadmin</title><style>
:root{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#172033;background:#f6f8ff}body{margin:0}.wrap{max-width:1320px;margin:auto;padding:24px}.hero{background:linear-gradient(135deg,#4f2d95,#f5a623);color:white;border-radius:28px;padding:24px;box-shadow:0 20px 50px rgba(50,30,90,.2)}.panel{background:white;border:1px solid #e4e9f7;border-radius:22px;padding:18px;margin:18px 0;box-shadow:0 12px 30px rgba(30,45,80,.08)}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px}input,select,button,textarea{border:1px solid #cbd6ee;border-radius:12px;padding:10px 12px;background:white;box-sizing:border-box}button{cursor:pointer;background:#173b7a;color:white;font-weight:900;border:0}.danger{background:#8d2a2a}.login{max-width:560px;margin:10vh auto;background:white;border-radius:24px;padding:28px;box-shadow:0 18px 60px rgba(20,45,90,.18)}table{width:100%;border-collapse:collapse;font-size:14px}th,td{text-align:left;padding:10px;border-bottom:1px solid #edf1f8;vertical-align:top}th{font-size:12px;text-transform:uppercase;color:#43516b}.wide{overflow:auto}.note{background:#fff8df;border:1px solid #ffe49b;border-radius:16px;padding:12px;margin-top:12px}.ok{background:#e9fff0;border-color:#b8efc8}.muted{color:#69738a}code{background:#f2f5ff;padding:2px 5px;border-radius:6px}.toolbar{display:flex;gap:10px;flex-wrap:wrap;align-items:center}.full{width:100%}@media(max-width:760px){.wrap{padding:14px}.hero{border-radius:18px}table{font-size:12px}}
</style></head><body><?php if(!$authenticated): ?><div class="login"><h1>⭐ BoardGameStars Währungsadmin</h1><p class="muted">Geschützt mit demselben Admin-Passwort wie die Statistik.</p><?php if($authError!==''): ?><div class="note"><?=h($authError)?></div><?php endif; ?><form method="post"><input class="full" type="password" name="admin_password" autocomplete="current-password" placeholder="Admin-Passwort"><br><br><button type="submit">Öffnen</button></form></div><?php else: ?><div class="wrap"><section class="hero"><h1>⭐ BoardGameStars Währungsadmin</h1><p>Silber-/Goldcodes erzeugen und Wallets direkt erhöhen oder korrigieren.</p><div class="toolbar"><button id="reload">Aktualisieren</button><a href="admin_stats.php" style="color:white">Statistik</a><a href="?logout=1" style="color:white">Logout</a></div></section><div id="msg" class="note" style="display:none"></div><div class="grid"><div class="panel"><h2>Codes erzeugen</h2><form id="codeForm"><label>Währung<br><select name="currency"><option value="gold">Gold</option><option value="silver">Silber</option></select></label><br><br><label>Betrag pro Code<br><input class="full" name="amount" type="number" min="1" value="500"></label><br><br><label>Anzahl Codes<br><input class="full" name="count" type="number" min="1" max="200" value="1"></label><br><br><label>Einlösungen pro Code<br><input class="full" name="maxRedemptions" type="number" min="1" value="1"></label><br><br><label>Ablauf in Tagen (0 = kein Ablauf)<br><input class="full" name="expiresDays" type="number" min="0" value="0"></label><br><br><label>Notiz<br><input class="full" name="note" maxlength="160" placeholder="z.B. Testcode, Gewinn, Support"></label><br><br><button type="submit">Codes erzeugen</button></form><div id="newCodes"></div></div><div class="panel"><h2>Wallet direkt erhöhen/korrigieren</h2><p class="muted">Positive Werte erhöhen. Negative Werte korrigieren nach unten, nie unter 0.</p><form id="walletForm"><label>Spieler suchen<br><input class="full" id="playerSearch" placeholder="Name oder ID"></label><br><br><label>Player-ID<br><input class="full" name="playerId" required placeholder="bgs_..."></label><br><br><label>Silber-Änderung<br><input class="full" name="silverDelta" type="number" value="0"></label><br><br><label>Gold-Änderung<br><input class="full" name="goldDelta" type="number" value="0"></label><br><br><label>Grund<br><input class="full" name="reason" maxlength="140" value="Admin-Gutschrift"></label><br><br><button type="submit">Wallet speichern</button></form></div></div><div class="panel"><h2>User auswählen</h2><div class="wide"><table id="players"></table></div></div><div class="panel"><h2>Erzeugte Admin-Codes</h2><div class="wide"><table id="codes"></table></div></div><div class="panel"><h2>Admin-Buchungen und Code-Einlösungen</h2><div class="wide"><table id="actions"></table></div></div></div><script>
const $=id=>document.getElementById(id); const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); const num=v=>Number(v||0)||0; const when=ts=>ts?new Date(num(ts)*1000).toLocaleString('de-DE',{day:'2-digit',month:'2-digit',year:'2-digit',hour:'2-digit',minute:'2-digit'}):'—'; let DATA={players:[],codes:[],actions:[]};
function msg(t,ok=false){$('msg').style.display='block';$('msg').className='note '+(ok?'ok':'');$('msg').textContent=t}
async function api(action,payload={}){const res=await fetch('admin_currency.php?ajax='+encodeURIComponent(action),{method:'POST',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},body:JSON.stringify(payload)});const data=await res.json();if(data.status!=='ok')throw new Error(data.status||'Fehler');return data}
function table(id,head,rows){$(id).innerHTML='<thead><tr>'+head.map(h=>`<th>${esc(h)}</th>`).join('')+'</tr></thead><tbody>'+rows.join('')+'</tbody>'}
function render(){const q=String($('playerSearch').value||'').toLowerCase().trim();const players=DATA.players.filter(p=>!q||(String(p.public_name||'')+' '+String(p.player_id||'')).toLowerCase().includes(q));table('players',['Name/ID','Typ','Silber','Gold','Rangpunkte','Auswahl'],players.slice(0,300).map(p=>`<tr><td><b>${esc(p.public_name)}</b><br><small>${esc(p.player_id)}</small></td><td>${num(p.is_network_player)?'🌐 Netzwerk':'User'}${num(p.locked_identity)?' 🔒':''}</td><td>${num(p.silver_balance).toLocaleString('de-DE')}</td><td><b>${num(p.gold_balance).toLocaleString('de-DE')}</b></td><td>${num(p.elo)}</td><td><button type="button" data-pid="${esc(p.player_id)}">Auswählen</button></td></tr>`));document.querySelectorAll('[data-pid]').forEach(b=>b.onclick=()=>{$('#walletForm [name=playerId]').value=b.dataset.pid;msg('Spieler übernommen: '+b.dataset.pid,true)});table('codes',['Zeit','Code-Präfix','Währung','Betrag','Status','Einlösungen','Ablauf','Notiz'],DATA.codes.map(c=>`<tr><td>${when(c.created_at)}</td><td><code>${esc(c.code_prefix)}</code></td><td>${esc(c.currency)}</td><td><b>${num(c.amount).toLocaleString('de-DE')}</b></td><td>${esc(c.status)}</td><td>${num(c.redeemed_count)}/${num(c.max_redemptions)}</td><td>${when(c.expires_at)}</td><td>${esc(c.note)}</td></tr>`));table('actions',['Zeit','Name','ID','Typ','Währung','Betrag','Grund'],DATA.actions.map(a=>`<tr><td>${when(a.ts)}</td><td><b>${esc(a.public_name||'')}</b></td><td><small>${esc(a.player_id)}</small></td><td>${esc(a.type)}</td><td>${esc(a.currency)}</td><td><b>${num(a.amount).toLocaleString('de-DE')}</b></td><td>${esc(a.reason)}<br><small>${esc(a.related_id)}</small></td></tr>`));}
async function load(){try{DATA=await api('overview');render()}catch(e){msg('Laden fehlgeschlagen: '+e.message)}}
$('reload').onclick=load; $('playerSearch').oninput=render;
$('codeForm').onsubmit=async ev=>{ev.preventDefault();const fd=new FormData(ev.currentTarget);const payload=Object.fromEntries(fd.entries());try{const data=await api('create_codes',payload);$('newCodes').innerHTML='<h3>Neue Codes</h3><textarea class="full" rows="'+Math.min(12,Math.max(3,data.codes.length+1))+'" readonly>'+data.codes.map(c=>`${c.code}  |  ${c.amount} ${c.currency}  |  ${c.maxRedemptions}x`).join('\n')+'</textarea><p class="muted">Diese Klartext-Codes werden nur jetzt angezeigt. Bitte kopieren/speichern.</p>';msg(data.codes.length+' Code(s) erzeugt.',true);load()}catch(e){msg('Codes konnten nicht erzeugt werden: '+e.message)}};
$('walletForm').onsubmit=async ev=>{ev.preventDefault();const fd=new FormData(ev.currentTarget);const payload=Object.fromEntries(fd.entries());payload.silverDelta=Number(payload.silverDelta||0);payload.goldDelta=Number(payload.goldDelta||0);try{const data=await api('adjust_wallet',payload);msg(`Wallet gespeichert. Silber: ${num(data.silverBalance).toLocaleString('de-DE')}, Gold: ${num(data.goldBalance).toLocaleString('de-DE')}`,true);ev.currentTarget.silverDelta.value=0;ev.currentTarget.goldDelta.value=0;load()}catch(e){msg('Wallet konnte nicht geändert werden: '+e.message)}};
load();
</script><?php endif; ?></body></html>
