<?php
session_start();
$PASSWORD='stepaway'; // Change before public launch
$root=__DIR__;
$dataDir=$root.'/data';
$packDir=$root.'/content/packs';
if(!is_dir($dataDir)) @mkdir($dataDir,0755,true);
if(!is_dir($packDir)) @mkdir($packDir,0755,true);
$files=[
  'cards'=>'data/cards.json',
  'cases'=>'data/cases.json',
  'campaigns'=>'data/campaigns.json',
  'wrinkles'=>'data/wrinkles.json',
  'settings'=>'data/settings.json',
  'pack_index'=>'content/packs/index.json',
  'installed_packs'=>'data/installed-packs.json',
  'community'=>'data/community.json',
  'profiles'=>'data/profiles.json',
  'submissions'=>'data/submissions.json',
  'shares'=>'data/share-index.json',
  'entitlements'=>'data/profile-entitlements.json',
  'access_grants'=>'data/access-grants.json',
  'paypal_transactions'=>'data/paypal-transactions.json',
  'about'=>'data/about.json'
];
function h($s){return htmlspecialchars((string)$s,ENT_QUOTES,'UTF-8');}
function clean_pack_name($s){return preg_replace('/[^a-zA-Z0-9_\-.]/','',(string)$s);}
function read_json_file($path,$fallback=[]){ if(!file_exists($path)) return $fallback; $raw=file_get_contents($path); $j=json_decode($raw,true); return is_array($j)?$j:$fallback; }
function write_json_file($path,$data){ $dir=dirname($path); if(!is_dir($dir)) @mkdir($dir,0755,true); $tmp=$path.'.tmp'; file_put_contents($tmp,json_encode($data,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE),LOCK_EX); rename($tmp,$path); }
function ids_from_list($list){ $ids=[]; foreach($list as $x){ if(is_array($x) && isset($x['id'])) $ids[]=$x['id']; elseif(is_string($x)) $ids[]=$x; } return $ids; }
function merge_by_id($existing,$incoming,&$count=0){
  if(!is_array($existing)) $existing=[]; if(!is_array($incoming)) return $existing;
  $map=[]; $order=[];
  foreach($existing as $item){ if(is_array($item)&&isset($item['id'])){$map[$item['id']]=$item;$order[]=$item['id'];} }
  foreach($incoming as $item){ if(is_array($item)&&isset($item['id'])){ if(!isset($map[$item['id']])) $order[]=$item['id']; $map[$item['id']]=$item; $count++; } }
  $out=[]; foreach($order as $id){ if(isset($map[$id])) $out[]=$map[$id]; }
  return $out;
}
function merge_wrinkle_bucket($existing,$incoming,&$count=0){ return merge_by_id(is_array($existing)?$existing:[], is_array($incoming)?$incoming:[], $count); }
function validate_case($case,$idx){
  $issues=[];
  if(!is_array($case)){ $issues[]="Case $idx is not an object."; return $issues; }
  foreach(['id','title','opening'] as $k){ if(empty($case[$k])) $issues[]="Case ".($case['id']??$idx)." missing $k."; }
  $beats=$case['beats']??$case['moments']??null;
  if(!is_array($beats) || count($beats)!==3) $issues[]="Case ".($case['id']??$idx)." should have exactly 3 beats/Moments.";
  if(is_array($beats)) foreach($beats as $i=>$b){
    $label=($case['id']??$idx).' beat '.($i+1);
    if(!is_array($b)){ $issues[]="$label is not an object."; continue; }
    foreach(['title','text','watch','aim'] as $k){ if(empty($b[$k])) $issues[]="$label missing $k."; }
    $targets=$b['suitTargets']??null;
    foreach(['D','H','C','S'] as $s){ if(!isset($targets[$s]) || !is_numeric($targets[$s])) $issues[]="$label missing numeric suitTargets.$s."; }
  }
  foreach(['closingWin','closingPartial','closingLoss'] as $k){ if(empty($case[$k])) $issues[]="Case ".($case['id']??$idx)." missing $k."; }
  return $issues;
}
function pack_info($path){
  $raw=file_exists($path)?file_get_contents($path):'';
  $pack=json_decode($raw,true);
  $errors=[]; $warnings=[]; $stats=['caseObjects'=>0,'caseRefs'=>0,'campaignObjects'=>0,'campaignRefs'=>0,'wrinkleObjects'=>0,'wrinkleRefs'=>0];
  if(json_last_error()!==JSON_ERROR_NONE) return ['ok'=>false,'errors'=>['JSON error: '.json_last_error_msg()],'warnings'=>[],'stats'=>$stats,'pack'=>null];
  if(!is_array($pack)) $errors[]='Pack root must be a JSON object.';
  if(is_array($pack)){
    foreach(['id','title','version'] as $k){ if(empty($pack[$k])) $warnings[]="Pack missing $k."; }
    if(isset($pack['cases']) && is_array($pack['cases'])) foreach($pack['cases'] as $i=>$case){
      if(is_array($case)){ $stats['caseObjects']++; $errors=array_merge($errors,validate_case($case,$i)); }
      elseif(is_string($case)) $stats['caseRefs']++;
      else $errors[]="Pack case entry $i is neither object nor id string.";
    }
    if(isset($pack['campaigns']) && is_array($pack['campaigns'])) foreach($pack['campaigns'] as $i=>$c){
      if(is_array($c)){ $stats['campaignObjects']++; if(empty($c['id'])||empty($c['title'])) $errors[]="Campaign object $i needs id and title."; }
      elseif(is_string($c)) $stats['campaignRefs']++;
      else $errors[]="Campaign entry $i is neither object nor id string.";
    }
    if(isset($pack['wrinkles']) && is_array($pack['wrinkles'])){
      foreach(['hard','easy'] as $bucket){ if(isset($pack['wrinkles'][$bucket]) && is_array($pack['wrinkles'][$bucket])) foreach($pack['wrinkles'][$bucket] as $i=>$w){
        if(is_array($w)){ $stats['wrinkleObjects']++; if(empty($w['id'])||empty($w['title'])||empty($w['text'])) $errors[]="Wrinkle $bucket $i needs id, title and text."; }
        elseif(is_string($w)) $stats['wrinkleRefs']++;
        else $errors[]="Wrinkle $bucket $i is neither object nor id string.";
      }}
    }
    if(!$stats['caseObjects']&&!$stats['campaignObjects']&&!$stats['wrinkleObjects']&&!isset($pack['settings'])) $warnings[]='This pack has no self-contained objects to import. It may only reference existing starter content.';
  }
  return ['ok'=>empty($errors),'errors'=>$errors,'warnings'=>$warnings,'stats'=>$stats,'pack'=>$pack];
}
function import_pack($filename,&$message){
  global $root,$packDir;
  $filename=clean_pack_name($filename); if(!$filename||$filename==='index.json'){ $message='Bad pack filename.'; return false; }
  $path=$packDir.'/'.$filename; $info=pack_info($path); if(!$info['ok']){ $message='Pack failed validation: '.implode(' ', $info['errors']); return false; }
  $pack=$info['pack']; $stats=['cases'=>0,'campaigns'=>0,'wrinkles'=>0,'settings'=>0];
  if(isset($pack['cases'])){ $cases=read_json_file($root.'/data/cases.json',[]); $incoming=array_values(array_filter($pack['cases'], 'is_array')); $cases=merge_by_id($cases,$incoming,$stats['cases']); write_json_file($root.'/data/cases.json',$cases); }
  if(isset($pack['campaigns'])){ $campaigns=read_json_file($root.'/data/campaigns.json',[]); $incoming=array_values(array_filter($pack['campaigns'], 'is_array')); $campaigns=merge_by_id($campaigns,$incoming,$stats['campaigns']); write_json_file($root.'/data/campaigns.json',$campaigns); }
  if(isset($pack['wrinkles']) && is_array($pack['wrinkles'])){ $wr=read_json_file($root.'/data/wrinkles.json',['hard'=>[],'easy'=>[]]); foreach(['hard','easy'] as $bucket){ $incoming=array_values(array_filter($pack['wrinkles'][$bucket]??[], 'is_array')); $wr[$bucket]=merge_wrinkle_bucket($wr[$bucket]??[],$incoming,$stats['wrinkles']); } write_json_file($root.'/data/wrinkles.json',$wr); }
  if(isset($pack['settings']) && is_array($pack['settings'])){ $settings=read_json_file($root.'/data/settings.json',[]); $settings=array_replace_recursive($settings,$pack['settings']); write_json_file($root.'/data/settings.json',$settings); $stats['settings']=1; }
  $index=read_json_file($root.'/content/packs/index.json',['packs'=>[]]); if(!isset($index['packs'])||!is_array($index['packs'])) $index['packs']=[]; if(!in_array($filename,$index['packs'],true)) $index['packs'][]=$filename; write_json_file($root.'/content/packs/index.json',$index);
  $installed=read_json_file($root.'/data/installed-packs.json',['schema'=>'stepaway-installed-packs-v1','packs'=>[]]); $installed['packs'][$pack['id']??$filename]=['file'=>$filename,'title'=>$pack['title']??$filename,'version'=>$pack['version']??'unknown','installedAt'=>gmdate('c'),'stats'=>$stats,'warnings'=>$info['warnings']]; write_json_file($root.'/data/installed-packs.json',$installed);
  $message='Imported '.$filename.' · cases '.$stats['cases'].' · campaigns '.$stats['campaigns'].' · wrinkles '.$stats['wrinkles'].($stats['settings']?' · settings updated':'');
  return true;
}
if(isset($_POST['password'])){ if($_POST['password']===$PASSWORD) $_SESSION['ok']=true; }
if(isset($_GET['logout'])){ session_destroy(); header('Location: admin.php'); exit; }
if(empty($_SESSION['ok'])){ echo '<!doctype html><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="stylesheet" href="assets/css/app.css"><main class="app-shell"><section class="panel"><h1>Step Away Admin</h1><form method="post"><input class="search" type="password" name="password" placeholder="Password"><button class="primary">Enter</button></form></section></main>'; exit; }
$msg='';

function entitlements_default(){ return ['schema'=>'stepaway-profile-entitlements-v1','profiles'=>[]]; }
function grants_default_admin(){ return ['schema'=>'stepaway-access-grants-v1','grants'=>[]]; }
function profiles_default_admin(){ return ['schema'=>'stepaway-profiles-v1','profiles'=>[]]; }
function clean_profile_id_admin($id){ return preg_replace('/[^a-zA-Z0-9_\-]/','',(string)$id); }
function manual_access_token_admin($profileId){ return hash('sha256', clean_profile_id_admin($profileId).'|stepaway-admin-full-access|'.__DIR__); }
function grant_profile_full_access($profileId,$displayName='',$credits=0,&$message=''){
  global $root;
  $profileId=clean_profile_id_admin($profileId);
  if(!$profileId){ $message='Profile ID required.'; return false; }
  $ents=read_json_file($root.'/data/profile-entitlements.json', entitlements_default());
  if(!isset($ents['profiles'][$profileId])){
    $ents['profiles'][$profileId]=['profileId'=>$profileId,'fullApp'=>false,'deckOwner'=>false,'appAccessOnly'=>false,'walletCredits'=>0,'transactions'=>[],'creditLog'=>[],'createdAt'=>gmdate('c')];
  }
  $ents['profiles'][$profileId]['fullApp']=true;
  $ents['profiles'][$profileId]['source']='admin_manual_grant';
  $ents['profiles'][$profileId]['walletCredits']=(int)($ents['profiles'][$profileId]['walletCredits']??0)+(int)$credits;
  $ents['profiles'][$profileId]['lastUpdated']=gmdate('c');
  if($credits>0) $ents['profiles'][$profileId]['creditLog'][]=['type'=>'admin_credit','credit'=>(int)$credits,'time'=>gmdate('c')];
  write_json_file($root.'/data/profile-entitlements.json',$ents);

  $profiles=read_json_file($root.'/data/profiles.json', profiles_default_admin());
  if(!isset($profiles['profiles'][$profileId])) $profiles['profiles'][$profileId]=['profileId'=>$profileId,'createdAt'=>gmdate('c'),'totalCases'=>0,'publishedStories'=>0];
  if(trim($displayName)!=='') $profiles['profiles'][$profileId]['displayName']=trim($displayName);
  if(empty($profiles['profiles'][$profileId]['displayName'])) $profiles['profiles'][$profileId]['displayName']='Step Away Table';
  $profiles['profiles'][$profileId]['fullApp']=true;
  $profiles['profiles'][$profileId]['walletCredits']=(int)($ents['profiles'][$profileId]['walletCredits']??0);
  $profiles['profiles'][$profileId]['lastSeen']=gmdate('c');
  write_json_file($root.'/data/profiles.json',$profiles);

  $token=manual_access_token_admin($profileId);
  $grants=read_json_file($root.'/data/access-grants.json', grants_default_admin());
  $grants['grants'][$token]=['profileId'=>$profileId,'tier'=>'full','source'=>'admin_manual_grant','label'=>'Admin full access','issuedAt'=>gmdate('c')];
  write_json_file($root.'/data/access-grants.json',$grants);

  $message='Full app access granted to '.$profileId.'. Ask them to open the app online and press Sync now, or reload once.';
  return true;
}
function revoke_profile_full_access($profileId,&$message=''){
  global $root;
  $profileId=clean_profile_id_admin($profileId);
  if(!$profileId){ $message='Profile ID required.'; return false; }

  $ents=read_json_file($root.'/data/profile-entitlements.json', entitlements_default());
  $source=$ents['profiles'][$profileId]['source']??'';
  if($source!=='' && $source!=='admin_manual_grant'){
    $message='This profile has purchase-based access, not a manual admin grant. Revoke the purchase entitlement separately if needed.';
    return false;
  }

  if(isset($ents['profiles'][$profileId])){
    $ents['profiles'][$profileId]['fullApp']=false;
    $ents['profiles'][$profileId]['lastUpdated']=gmdate('c');
    write_json_file($root.'/data/profile-entitlements.json',$ents);
  }

  $grants=read_json_file($root.'/data/access-grants.json', grants_default_admin());
  foreach(($grants['grants']??[]) as $tok=>$g){
    if(($g['profileId']??'')===$profileId && ($g['source']??'')==='admin_manual_grant') unset($grants['grants'][$tok]);
  }
  write_json_file($root.'/data/access-grants.json',$grants);

  $stillHasGrant=false;
  foreach(($grants['grants']??[]) as $tok=>$g){
    if(($g['profileId']??'')===$profileId && ($g['tier']??'')==='full') $stillHasGrant=true;
  }
  $profiles=read_json_file($root.'/data/profiles.json', profiles_default_admin());
  if(isset($profiles['profiles'][$profileId]) && !$stillHasGrant){
    $profiles['profiles'][$profileId]['fullApp']=false;
    write_json_file($root.'/data/profiles.json',$profiles);
  }
  $message='Manual full access revoked for '.$profileId.'.';
  return true;
}

if($_SERVER['REQUEST_METHOD']==='POST'){
  $action=$_POST['action']??'';
  if($action==='save_json' && isset($_POST['file'],$_POST['json'])){
    $key=$_POST['file']; if(isset($files[$key])){ $decoded=json_decode($_POST['json'],true); if(json_last_error()===JSON_ERROR_NONE){ write_json_file($root.'/'.$files[$key],$decoded); $msg='Saved '.$key; } else $msg='JSON error: '.json_last_error_msg(); }
  }
  if($action==='upload_pack' && isset($_FILES['packfile']) && is_uploaded_file($_FILES['packfile']['tmp_name'])){
    $name=clean_pack_name($_FILES['packfile']['name']); if(substr($name, -5) !== '.json') $name.='.json';
    $target=$packDir.'/'.$name; $raw=file_get_contents($_FILES['packfile']['tmp_name']); $j=json_decode($raw,true);
    if(json_last_error()===JSON_ERROR_NONE){ file_put_contents($target,json_encode($j,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE),LOCK_EX); $msg='Uploaded pack '.$name; }
    else $msg='Upload rejected, JSON error: '.json_last_error_msg();
  }
  if($action==='import_pack' && isset($_POST['pack'])) import_pack($_POST['pack'],$msg);
  if($action==='grant_profile_full') grant_profile_full_access($_POST['profileId']??'', $_POST['displayName']??'', (int)($_POST['credits']??0), $msg);
  if($action==='revoke_profile_full') revoke_profile_full_access($_POST['profileId']??'', $msg);
  if($action==='save_about'){
    $about=read_json_file($root.'/data/about.json',['schema'=>'stepaway-about-v1']);
    $about['title']=trim($_POST['title']??'About Step Away')?:'About Step Away';
    $about['lead']=trim($_POST['lead']??'');
    $about['body']=trim($_POST['body']??'');
    $about['footer']=trim($_POST['footer']??'');
    $about['updatedAt']=gmdate('c');
    write_json_file($root.'/data/about.json',$about);
    $msg='About section saved.';
  }
}

$view=$_GET['view']??'packs';
$key=$_GET['file']??'cases'; if(!isset($files[$key])) $key='cases';
$path=$root.'/'.$files[$key]; if(!file_exists($path)) write_json_file($path, $key==='installed_packs'?['schema'=>'stepaway-installed-packs-v1','packs'=>[]]:[]); $json=file_get_contents($path);
$packFiles=[]; foreach(glob($packDir.'/*.json') ?: [] as $pf){ if(basename($pf)!=='index.json') $packFiles[]=basename($pf); }
sort($packFiles);
$installed=read_json_file($root.'/data/installed-packs.json',['packs'=>[]]);
?><!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Step Away Admin</title><link rel="stylesheet" href="assets/css/app.css"><style>
textarea{width:100%;min-height:62vh;background:#fffaf1;border:1px solid #d4c8b8;border-radius:16px;padding:12px;font-family:ui-monospace,monospace;font-size:13px}.admin-tabs{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0}.admin-tabs a{background:#20272f;color:#f7efe4;padding:10px 12px;border-radius:12px;text-decoration:none;font-weight:900}.admin-tabs a.active{background:#a51628}.pack-table{display:grid;gap:12px}.pack-row{background:#fffaf1;border:1px solid #d4c8b8;border-radius:18px;padding:14px}.ok{color:#166c35;font-weight:900}.bad{color:#a51628;font-weight:900}.warn{color:#8a5a00;font-weight:900}.mini{font-size:12px;color:#5b5147}.inline-form{display:inline}.pill{display:inline-block;padding:4px 8px;border-radius:999px;background:#20272f;color:#f7efe4;font-size:12px;font-weight:900;margin:2px}.danger{background:#7f1225!important;color:#fffaf1!important}.about-editor textarea{min-height:280px}</style></head><body><main class="app-shell"><section class="panel"><p class="eyebrow">Launch content pipeline</p><h1 style="color:#17130f;text-shadow:none">Step Away Admin</h1><p><?php echo h($msg); ?></p><div class="admin-tabs"><a class="<?php echo $view==='packs'?'active':''; ?>" href="?view=packs">Pack Manager</a><a class="<?php echo $view==='editor'?'active':''; ?>" href="?view=editor&file=<?php echo h($key); ?>">JSON Editor</a><a class="<?php echo $view==='access'?'active':''; ?>" href="?view=access">Access Manager</a><a class="<?php echo $view==='about'?'active':''; ?>" href="?view=about">About Editor</a><a href="index.php">Open app</a><a href="share.php">Public shares</a><a href="?logout=1">Logout</a></div>
<?php if($view==='packs'): ?>
  <h2>Pack Manager</h2>
  <p>Drop self-contained case packs into <code>content/packs/</code>, upload them here, then validate/import. Core deck and app code do not need regenerating for new case content.</p>
  <form method="post" enctype="multipart/form-data" class="pack-row"><input type="hidden" name="action" value="upload_pack"><h3>Upload JSON pack</h3><input class="search" type="file" name="packfile" accept="application/json,.json"><button class="primary">Upload pack</button></form>
  <div class="pack-table">
  <?php foreach($packFiles as $pf): $info=pack_info($packDir.'/'.$pf); $pack=$info['pack']??[]; $pid=$pack['id']??$pf; $isInstalled=isset($installed['packs'][$pid]); ?>
    <article class="pack-row">
      <h3><?php echo h($pack['title']??$pf); ?> <?php echo $isInstalled?'<span class="pill">installed</span>':''; ?></h3>
      <p class="mini"><b>File:</b> <?php echo h($pf); ?> · <b>ID:</b> <?php echo h($pid); ?> · <b>Version:</b> <?php echo h($pack['version']??'unknown'); ?></p>
      <p><span class="pill">case objects <?php echo (int)$info['stats']['caseObjects']; ?></span><span class="pill">case refs <?php echo (int)$info['stats']['caseRefs']; ?></span><span class="pill">campaign objects <?php echo (int)$info['stats']['campaignObjects']; ?></span><span class="pill">wrinkle objects <?php echo (int)$info['stats']['wrinkleObjects']; ?></span></p>
      <?php if($info['ok']): ?><p class="ok">Valid for import.</p><?php else: ?><p class="bad">Not valid: <?php echo h(implode(' ', $info['errors'])); ?></p><?php endif; ?>
      <?php if($info['warnings']): ?><p class="warn">Warnings: <?php echo h(implode(' ', $info['warnings'])); ?></p><?php endif; ?>
      <form method="post" class="inline-form"><input type="hidden" name="action" value="import_pack"><input type="hidden" name="pack" value="<?php echo h($pf); ?>"><button class="primary" <?php echo $info['ok']?'':'disabled'; ?>>Import / update pack</button></form>
    </article>
  <?php endforeach; if(!$packFiles): ?><p>No pack files found.</p><?php endif; ?>
  </div>
  <article class="pack-row"><h3>Pack format</h3><p>A self-contained pack can include <code>cases</code>, <code>campaigns</code>, <code>wrinkles</code> and optional <code>settings</code>. Cases should contain 3 beats and each beat should include <code>suitTargets</code> for D/H/C/S.</p><p>Template: <code>content/pack_templates/self-contained-case-pack-template.json</code></p></article>


<?php elseif($view==='about'): ?>
  <?php $about=read_json_file($root.'/data/about.json',['title'=>'About Step Away','lead'=>'','body'=>'','footer'=>'']); ?>
  <h2>About Editor</h2>
  <p>This text appears inside the app. Keep it human and clear: awareness tool, not cure, not emergency service.</p>
  <form method="post" class="pack-row about-editor">
    <input type="hidden" name="action" value="save_about">
    <h3>About section</h3>
    <input class="search" name="title" value="<?php echo h($about['title']??'About Step Away'); ?>" placeholder="Title">
    <input class="search" name="lead" value="<?php echo h($about['lead']??''); ?>" placeholder="Lead sentence">
    <textarea name="body"><?php echo h($about['body']??''); ?></textarea>
    <input class="search" name="footer" value="<?php echo h($about['footer']??''); ?>" placeholder="Footer / safety note">
    <button class="primary big">Save About</button>
  </form>

<?php elseif($view==='access'): ?>
  <h2>Access Manager</h2>
  <p>Use this to manually mark a table/profile as having full app access. The player must open the app online once and press <b>Sync now</b>, or reload while online, before offline play can use the grant.</p>
  <form method="post" class="pack-row">
    <input type="hidden" name="action" value="grant_profile_full">
    <h3>Grant full access to profile</h3>
    <p class="mini">Ask the player for their Table profile ID from Settings. It looks like <code>local_...</code> or <code>table_...</code>.</p>
    <input class="search" name="profileId" placeholder="Profile ID" required>
    <input class="search" name="displayName" placeholder="Display name optional">
    <input class="search" name="credits" type="number" min="0" value="0" placeholder="Pack credits to add">
    <button class="primary">Grant full app access</button>
  </form>
  <h3>Known profiles</h3>
  <div class="pack-table">
  <?php
    $profilesA=read_json_file($root.'/data/profiles.json', profiles_default_admin());
    $entsA=read_json_file($root.'/data/profile-entitlements.json', entitlements_default());
    $rows=$profilesA['profiles']??[];
    if(!$rows && !empty($entsA['profiles'])) $rows=$entsA['profiles'];
    foreach($rows as $pid=>$p):
      $ent=$entsA['profiles'][$pid]??[];
      $full=!empty($p['fullApp'])||!empty($ent['fullApp']);
      $credits=(int)($ent['walletCredits']??($p['walletCredits']??0));
  ?>
    <article class="pack-row">
      <h3><?php echo h($p['displayName']??'Step Away Table'); ?> <?php echo $full?'<span class="pill">full access</span>':'<span class="pill">demo</span>'; ?> <?php echo $credits?'<span class="pill">credits '.(int)$credits.'</span>':''; ?></h3>
      <p class="mini"><b>Profile:</b> <code><?php echo h($pid); ?></code> · <b>Last seen:</b> <?php echo h($p['lastSeen']??'unknown'); ?></p>
      <?php $manual=($ent['source']??'')==='admin_manual_grant'; ?>
      <?php if($full && $manual): ?>
        <form method="post" class="inline-form"><input type="hidden" name="action" value="revoke_profile_full"><input type="hidden" name="profileId" value="<?php echo h($pid); ?>"><button class="danger">Revoke manual full access</button></form>
      <?php elseif($full): ?>
        <p class="mini">Purchase-based or synced full access. Not revoked from this manual-grant button.</p>
      <?php else: ?>
        <form method="post" class="inline-form"><input type="hidden" name="action" value="grant_profile_full"><input type="hidden" name="profileId" value="<?php echo h($pid); ?>"><button class="primary">Grant full access</button></form>
      <?php endif; ?>
    </article>
  <?php endforeach; if(!$rows): ?><p>No profiles found yet.</p><?php endif; ?>
  </div>

<?php else: ?>
  <h2>JSON Editor</h2><div class="admin-tabs"><?php foreach($files as $k=>$v){echo '<a class="'.($k===$key?'active':'').'" href="?view=editor&file='.h($k).'">'.h($k).'</a>';} ?></div><form method="post"><input type="hidden" name="action" value="save_json"><input type="hidden" name="file" value="<?php echo h($key); ?>"><textarea name="json"><?php echo h($json); ?></textarea><button class="primary big">Save JSON</button></form>
<?php endif; ?>
</section></main></body></html>
