<?php
// ═══ Public site front controller ═══
require __DIR__ . '/app/bootstrap.php';
handle_status_api();   // ExpertAims HQ fleet monitoring (signed; 403 otherwise)
handle_command_api();  // ExpertAims HQ signed commands, e.g. remote admin reset (POST; 403 otherwise)

$uri  = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
$base = rtrim((string) config('base_url'), '/');
if ($base !== '' && strpos($uri, $base) === 0) $uri = substr($uri, strlen($base));
$path = trim(rawurldecode($uri), '/');
$path = preg_replace('/\.html$/', '', $path);

// ─── Contact enquiry capture (fetch POST from the contact form) ───
// Server-side gauntlet — every check independent of the client-side one:
// honeypot empty → session token matches → sum captcha correct → required
// fields present and valid → rate limit (5/hour/IP) → spam heuristics only
// FLAG (is_spam=1), so a mis-scored real enquiry still reaches the inbox.
if (($_GET['capture'] ?? '') === 'enquiry' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
  header('Content-Type: application/json');
  $fail = function ($err) { echo json_encode(['ok' => false, 'err' => $err]); exit; };
  if (!empty($_POST['company_website'])) { if (function_exists('security_log')) security_log('honeypot', 1, 'honeypot field filled'); $fail('blocked'); } // honeypot tripped
  if (($_SESSION['csrf'] ?? '') === ''
      || !hash_equals((string) $_SESSION['csrf'], (string) ($_POST['_token'] ?? ''))) $fail('session');
  if (!captcha_ok($_POST['sum'] ?? '')) $fail('captcha');
  $name    = mb_substr(trim((string) ($_POST['name'] ?? '')), 0, 160);
  $email   = mb_substr(trim((string) ($_POST['email'] ?? '')), 0, 190);
  $phone   = mb_substr(trim((string) ($_POST['phone'] ?? '')), 0, 60);
  $subject = mb_substr(trim((string) ($_POST['subject'] ?? '')), 0, 200);
  $message = mb_substr(trim((string) ($_POST['message'] ?? '')), 0, 5000);
  // strip control characters + header-injection newlines from one-line fields
  foreach (['name', 'email', 'phone', 'subject'] as $f) { $$f = preg_replace('/[\x00-\x1F\x7F]/u', ' ', $$f); }
  $message = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $message);
  if ($name === '' || $message === '') $fail('fields');
  if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $fail('email');
  if ($phone !== '' && !preg_match('/^[0-9+()\-\.\s]{7,20}$/', $phone)) $fail('phone');
  if (form_throttled('enquiry')) $fail('rate');                        // 5 per IP per hour
  $spam = looks_spammy($name . ' ' . $email . ' ' . $subject . ' ' . $message) ? 1 : 0;
  try {
    try {
      db()->prepare('INSERT INTO submissions(name,email,phone,subject,message,page,is_spam) VALUES(?,?,?,?,?,?,?)')
        ->execute([$name, $email, $phone, $subject, $message, mb_substr(trim((string) ($_POST['_page'] ?? $path)), 0, 160), $spam]);
    } catch (\Throwable $e) { // pre-upgrade DB without is_spam — degrade gracefully
      db()->prepare('INSERT INTO submissions(name,email,phone,subject,message,page) VALUES(?,?,?,?,?,?)')
        ->execute([$name, $email, $phone, $subject, $message, mb_substr(trim((string) ($_POST['_page'] ?? $path)), 0, 160)]);
    }
  } catch (\Throwable $e) { $fail('server'); }
  captcha_rotate();   // a solved sum can't be replayed
  echo json_encode(['ok' => true]);
  exit;
}

// ─── Career application capture (job "Apply" form → fetch POST) ───
// Same server-side gauntlet as the contact form, PLUS a time-trap: honeypot →
// session token → sum captcha → time-trap (≥2s since the form rendered) →
// required first/last name + valid email + optional-but-validated phone/YouTube →
// rate limit (5/hour/IP) → spam heuristics FLAG (never drop). Records which JOB
// (job title) and page the application came from.
if (($_GET['capture'] ?? '') === 'application' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
  if (!feature_on('applications')) { http_response_code(404); exit; }
  header('Content-Type: application/json');
  $fail = function ($err) { echo json_encode(['ok' => false, 'err' => $err]); exit; };
  // A multipart POST that PHP dropped because it exceeded post_max_size /
  // upload_max_filesize arrives with an empty $_POST but a non-zero Content-Length
  // — surface a friendly "too large" message (with the email fallback in the UI)
  // rather than a silent failure.
  if (empty($_POST) && (int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > 0) $fail('toolarge');
  if (!empty($_POST['company_website'])) { if (function_exists('security_log')) security_log('honeypot', 1, 'honeypot field filled'); $fail('blocked'); } // honeypot tripped
  if (($_SESSION['csrf'] ?? '') === ''
      || !hash_equals((string) $_SESSION['csrf'], (string) ($_POST['_token'] ?? ''))) $fail('session');
  if (!captcha_ok($_POST['sum'] ?? '')) $fail('captcha');
  $openedAt = (int) ($_SESSION['app_time'] ?? 0);
  if ($openedAt <= 0 || (time() - $openedAt) < 2) $fail('blocked');   // time-trap (generic err → bot learns nothing)
  $first = mb_substr(trim((string) ($_POST['first_name'] ?? '')), 0, 80);
  $last  = mb_substr(trim((string) ($_POST['last_name'] ?? '')), 0, 80);
  $phone = mb_substr(trim((string) ($_POST['phone'] ?? '')), 0, 60);
  $email = mb_substr(trim((string) ($_POST['email'] ?? '')), 0, 190);
  $loc   = mb_substr(trim((string) ($_POST['location'] ?? '')), 0, 160);
  $video = mb_substr(trim((string) ($_POST['video_url'] ?? '')), 0, 300);
  $jobT  = mb_substr(trim((string) ($_POST['job_title'] ?? '')), 0, 200);
  $relRaw = strtolower(trim((string) ($_POST['relocate'] ?? '')));
  $relocate = ($relRaw === 'yes' || $relRaw === '1') ? 'Yes' : (($relRaw === 'no' || $relRaw === '0') ? 'No' : '');
  $message = mb_substr(trim((string) ($_POST['message'] ?? '')), 0, 5000);
  // strip control characters + header-injection newlines from one-line fields
  foreach (['first', 'last', 'phone', 'email', 'loc', 'video', 'jobT'] as $f) { $$f = preg_replace('/[\x00-\x1F\x7F]/u', ' ', $$f); }
  $message = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $message);
  if ($first === '' || $last === '') $fail('fields');
  if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $fail('email');
  if ($phone !== '' && !preg_match('/^[0-9+()\-\.\s]{7,20}$/', $phone)) $fail('phone');
  if ($video !== '' && !preg_match('#^https?://(www\.|m\.)?(youtube\.com|youtu\.be)/#i', $video)) $fail('video');
  if (form_throttled('application')) $fail('rate');                    // 5 per IP per hour
  $spam = looks_spammy($first . ' ' . $last . ' ' . $email . ' ' . $message . ' ' . $video) ? 1 : 0;
  $pageFrom = mb_substr(trim((string) ($_POST['_page'] ?? $path)), 0, 160);

  // ─── Secure file attachments (resumes are PII) ───
  // Up to three OPTIONAL files (Resume / Teaching philosophy / Teaching-or-demo
  // document). EACH file is validated by BOTH an extension whitelist AND a finfo
  // MIME whitelist, capped at 10 MB, and stored under uploads/applications/ with a
  // RANDOM name (the user's filename is kept only as a DB label — never reused on
  // disk, never web-reachable: uploads/applications/.htaccess denies all access;
  // downloads go only through the authenticated admin route ?p=application_file).
  // A disallowed type or an oversize file rejects the whole submission with a
  // clear error. If the folder can't be written the text fields still save and we
  // flag that files couldn't be stored (graceful — the UI shows the email fallback).
  $fileMap    = ['file_resume' => 1, 'file_philosophy' => 2, 'file_sample' => 3];
  $allowedExt = [
    'pdf'  => ['application/pdf'],
    'doc'  => ['application/msword', 'application/octet-stream', 'application/vnd.ms-office', 'application/x-ole-storage'],
    'docx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/zip', 'application/x-zip-compressed', 'application/octet-stream'],
    'txt'  => ['text/plain', 'application/octet-stream'],
    'rtf'  => ['application/rtf', 'text/rtf', 'text/plain', 'application/msword', 'application/octet-stream'],
  ];
  $maxBytes = 10 * 1024 * 1024;                                        // 10 MB per file
  $stored   = [1 => ['name' => null, 'orig' => null], 2 => ['name' => null, 'orig' => null], 3 => ['name' => null, 'orig' => null]];
  $storedPaths = [];   // absolute paths of files moved into place (unlinked if the DB insert has to fall back)
  $fileNote = false;   // true when a folder/write problem meant one or more files were skipped
  $upDir  = __DIR__ . '/uploads/applications';
  $finfo  = function_exists('finfo_open') ? finfo_open(FILEINFO_MIME_TYPE) : null;
  foreach ($fileMap as $field => $slot) {
    if (!isset($_FILES[$field])) continue;
    $ferr = $_FILES[$field]['error'] ?? UPLOAD_ERR_NO_FILE;
    if ($ferr === UPLOAD_ERR_NO_FILE) continue;                        // optional — not provided
    if ($ferr === UPLOAD_ERR_INI_SIZE || $ferr === UPLOAD_ERR_FORM_SIZE) { if ($finfo) finfo_close($finfo); foreach ($storedPaths as $pp) @unlink($pp); $fail('filesize'); }
    if ($ferr !== UPLOAD_ERR_OK) { $fileNote = true; continue; }       // partial / no tmp dir — skip gracefully
    $tmp  = (string) ($_FILES[$field]['tmp_name'] ?? '');
    $orig = (string) ($_FILES[$field]['name'] ?? '');
    $size = (int) ($_FILES[$field]['size'] ?? 0);
    if ($tmp === '' || !is_uploaded_file($tmp)) { $fileNote = true; continue; }
    if ($size <= 0 || $size > $maxBytes) { if ($finfo) finfo_close($finfo); foreach ($storedPaths as $pp) @unlink($pp); $fail('filesize'); }
    $ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
    if (!isset($allowedExt[$ext])) { if ($finfo) finfo_close($finfo); foreach ($storedPaths as $pp) @unlink($pp); $fail('filetype'); }
    $mime = $finfo ? (string) @finfo_file($finfo, $tmp) : '';
    if ($mime !== '' && !in_array($mime, $allowedExt[$ext], true)) { if ($finfo) finfo_close($finfo); foreach ($storedPaths as $pp) @unlink($pp); $fail('filetype'); }
    if (!is_dir($upDir)) @mkdir($upDir, 0755, true);
    $rand = bin2hex(random_bytes(16)) . '.' . $ext;                    // never the user's filename
    $dest = $upDir . '/' . $rand;
    if (!is_dir($upDir) || !is_writable($upDir) || !@move_uploaded_file($tmp, $dest)) { $fileNote = true; continue; }
    @chmod($dest, 0640);
    $label = mb_substr(preg_replace('/[\x00-\x1F\x7F]/u', '', $orig), 0, 200);
    $stored[$slot] = ['name' => $rand, 'orig' => ($label !== '' ? $label : $rand)];
    $storedPaths[] = $dest;
  }
  if ($finfo) finfo_close($finfo);

  // Insert WITH the file columns; on a pre-upgrade DB (no file columns) fall back
  // to the text-only insert and drop the just-stored files so nothing is orphaned.
  $insDone = false;
  try {
    db()->prepare('INSERT INTO applications(job_title,first_name,last_name,phone,email,location,relocate,message,video_url,page,is_spam,file1_name,file1_orig,file2_name,file2_orig,file3_name,file3_orig) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')
      ->execute([$jobT, $first, $last, $phone, $email, $loc, $relocate, $message, $video, $pageFrom, $spam,
                 $stored[1]['name'], $stored[1]['orig'], $stored[2]['name'], $stored[2]['orig'], $stored[3]['name'], $stored[3]['orig']]);
    $insDone = true;
  } catch (\Throwable $e) {
    try {
      db()->prepare('INSERT INTO applications(job_title,first_name,last_name,phone,email,location,relocate,message,video_url,page,is_spam) VALUES(?,?,?,?,?,?,?,?,?,?,?)')
        ->execute([$jobT, $first, $last, $phone, $email, $loc, $relocate, $message, $video, $pageFrom, $spam]);
      $insDone = true;
      foreach ($storedPaths as $pp) @unlink($pp);                      // columns missing → don't orphan files
      if ($storedPaths) $fileNote = true;
    } catch (\Throwable $e2) { $insDone = false; }
  }
  if (!$insDone) { foreach ($storedPaths as $pp) @unlink($pp); $fail('server'); }
  captcha_rotate();   // a solved sum can't be replayed
  // Best-effort email notification to the careers inbox (the DB row above is the
  // record of truth; this just tells the team by email too).
  ea_notify_application([
    'job' => $jobT, 'first' => $first, 'last' => $last, 'email' => $email, 'phone' => $phone,
    'video' => $video, 'message' => $message, 'page' => $pageFrom, 'spam' => $spam,
    'file1' => $stored[1]['orig'], 'file2' => $stored[2]['orig'], 'file3' => $stored[3]['orig'],
  ]);
  echo json_encode($fileNote ? ['ok' => true, 'filenote' => 1] : ['ok' => true]);
  exit;
}

// ─── Newsletter signup capture (homepage popup → fetch POST) ───
// Hardened, all server-side (the client checks are courtesy only):
//   • honeypot empty            • session-bound single-use token
//   • sum captcha (random operands, validated here only)
//   • time-trap (reject < 2s after the popup rendered)
//   • strict email + length cap + disposable-domain blocklist (flag, don't drop)
//   • per-IP rate limit (5/hour, shared login-style throttle)
// Errors are deliberately generic so a bot learns nothing about which gate it hit.
if (($_GET['capture'] ?? '') === 'subscribe' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
  header('Content-Type: application/json');
  $ok = false; $spam = 0;
  $want    = (int) ($_SESSION['sub_a'] ?? 0) + (int) ($_SESSION['sub_b'] ?? 0);
  $tokenOk = ($_SESSION['csrf'] ?? '') !== '' && hash_equals((string) $_SESSION['csrf'], (string) ($_POST['_token'] ?? ''));
  $openedAt = (int) ($_SESSION['sub_time'] ?? 0);
  $fastBot  = $openedAt <= 0 || (time() - $openedAt) < 2;    // time-trap
  $sumOk    = $want > 0 && (int) ($_POST['sum'] ?? -1) === $want;
  $email    = strtolower(trim((string) ($_POST['email'] ?? '')));
  $email    = preg_replace('/[\x00-\x1F\x7F]/u', '', $email);
  $emailOk  = $email !== '' && mb_strlen($email) <= 190 && filter_var($email, FILTER_VALIDATE_EMAIL);
  if (empty($_POST['company_website']) && $tokenOk && $sumOk && $emailOk && !$fastBot
      && !form_throttled('subscribe', 5, 3600)) {
    if (email_domain_blocked($email)) $spam = 1;   // keep it, but flag as spam
    $pageFrom = mb_substr(trim((string) ($_POST['_page'] ?? $path)), 0, 160);
    try {
      try {
        db()->prepare('INSERT INTO subscribers(email,page,is_spam) VALUES(?,?,?) ON DUPLICATE KEY UPDATE email=email')
          ->execute([$email, $pageFrom, $spam]);
      } catch (\Throwable $e) { // pre-upgrade DB without is_spam — degrade gracefully
        db()->prepare('INSERT IGNORE INTO subscribers(email,page) VALUES(?,?)')->execute([$email, $pageFrom]);
      }
      $ok = true;
      unset($_SESSION['sub_a'], $_SESSION['sub_b'], $_SESSION['sub_time']); // single-use — no replay
    } catch (\Throwable $e) { $ok = false; }
  }
  echo json_encode(['ok' => $ok]);
  exit;
}

// ─── Certificate verification API (GET /verify?id=EA-IN-2026-0720) ───
if ($path === 'verify' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'GET') {
  header('Content-Type: application/json');
  $cert_id = (string) ($_GET['id'] ?? '');
  if ($cert_id === '') {
    http_response_code(400);
    echo json_encode(['ok' => false, 'error' => 'missing_id']);
    exit;
  }
  $cert = find_certificate($cert_id);
  if (!$cert) {
    http_response_code(404);
    echo json_encode(['ok' => false, 'error' => 'not_found']);
    exit;
  }
  echo json_encode(['ok' => true, 'certificate' => [
    'id' => (string) $cert['certificate_id'],
    'name' => (string) $cert['internee_name'],
    'role' => (string) $cert['role'],
    'company' => (string) $cert['company'],
    'location' => (string) ($cert['location'] ?? ''),
    'start_date' => (string) $cert['start_date'],
    'end_date' => (string) $cert['end_date'],
    'issued_date' => (string) $cert['issued_date'],
    'education' => (string) ($cert['education'] ?? ''),
    'institution' => (string) ($cert['institution'] ?? ''),
    'description' => (string) ($cert['description'] ?? ''),
  ]]);
  exit;
}

// ─── Certificate verification HTML page (GET /verify-certificate or similar) ───
if ($path === 'verify-certificate') {
  $cert_id = (string) ($_GET['cert'] ?? '');
  $cert = $cert_id !== '' ? find_certificate($cert_id) : null;
  $page = find_page('verify-certificate', true);
  if (!$page) {
    http_response_code(404);
    $p = [
      'slug' => '404', 'title' => 'Page not found', 'meta_title' => '404 · Not found | ' . setting('site_name'),
      'meta_description' => null, 'canonical' => null, 'og_image' => null,
      'robots' => 'noindex,follow',
    ];
    echo render_document($p,
        '<section class="sec rv" style="min-height:56vh;display:grid;place-items:center;text-align:center">'
      . '<div class="wrap"><p class="stn">Error 404</p>'
      . '<h1 class="d1" style="margin-top:14px">Page not found<span class="aim" aria-hidden="true"></span></h1>'
      . '<p class="lede" style="margin:20px auto 28px;max-width:46ch">The page you&rsquo;re looking for doesn&rsquo;t exist or has moved.</p>'
      . '<a class="btn btn-acc" href="' . base_url() . '">Back to home</a></div></section>');
    exit;
  }
  $certDisplay = '';
  if ($cert) {
    $certDisplay = sprintf(
      '<div class="cert-result" style="background:#f5f5f5;border:1px solid #ddd;border-radius:8px;padding:24px;margin-top:24px">'
      . '<p style="color:#090;font-weight:600;margin-bottom:12px">✓ Certificate Verified</p>'
      . '<p><strong>%s</strong></p>'
      . '<p style="color:#666;font-size:14px;margin-top:8px">%s to %s</p>'
      . '<p style="color:#666;font-size:14px">%s, %s</p>'
      . '<p style="color:#666;font-size:14px">%s</p>'
      . '<p style="color:#666;font-size:12px;margin-top:16px">Certificate ID: %s | Issued: %s</p>'
      . '</div>',
      e($cert['internee_name']),
      date('M d, Y', strtotime($cert['start_date'])),
      date('M d, Y', strtotime($cert['end_date'])),
      e($cert['role']),
      e($cert['company']),
      e($cert['education'] ?? ''),
      e($cert['certificate_id']),
      date('M d, Y', strtotime($cert['issued_date']))
    );
  } elseif ($cert_id !== '') {
    $certDisplay = '<div style="background:#fee;border:1px solid #fcc;border-radius:8px;padding:24px;margin-top:24px"><p style="color:#c00;margin:0"><strong>✗ Certificate not found</strong></p><p style="color:#666;font-size:14px;margin-top:8px">No certificate found for ID: ' . e($cert_id) . '</p></div>';
  }
  $content = render_sections($page['id']);
  if ($certDisplay !== '') {
    $content .= '<section class="sec rv"><div class="wrap">' . $certDisplay . '</div></section>';
  }
  echo render_document($page, $content);
  exit;
}

// ─── Regular page ───  ('' and 'index' both map to the Home page)
$slug = ($path === '' || $path === 'index') ? 'home' : $path;
$page = find_page($slug, true);

if (!$page) {
  http_response_code(404);
  $p = [
    'slug' => '404', 'title' => 'Page not found', 'meta_title' => '404 · Not found | ' . setting('site_name'),
    'meta_description' => null, 'canonical' => null, 'og_image' => null,
    'robots' => 'noindex,follow',
  ];
  echo render_document($p,
      '<section class="sec rv" style="min-height:56vh;display:grid;place-items:center;text-align:center">'
    . '<div class="wrap"><p class="stn">Error 404</p>'
    . '<h1 class="d1" style="margin-top:14px">Page not found<span class="aim" aria-hidden="true"></span></h1>'
    . '<p class="lede" style="margin:20px auto 28px;max-width:46ch">The page you&rsquo;re looking for doesn&rsquo;t exist or has moved.</p>'
    . '<a class="btn btn-acc" href="' . base_url() . '">Back to home</a></div></section>');
  exit;
}

// Hosting page turned OFF in the CMS → show the old-style fallback that links to expertaims.xyz.
if ($slug === 'hosting' && function_exists('hosting_page_enabled') && !hosting_page_enabled()) {
  $ext = function_exists('hosting_external_url') ? hosting_external_url() : setting('hosting_url', 'https://expertaims.xyz');
  $page['robots'] = 'noindex,follow';   // off = don't index the placeholder
  echo render_document($page,
      '<section class="sec rv" style="min-height:52vh;display:grid;place-items:center;text-align:center">'
    . '<div class="wrap"><p class="stn">Web hosting</p>'
    . '<h1 class="d1" style="margin-top:14px">Our hosting lives here<span class="aim" aria-hidden="true"></span></h1>'
    . '<p class="lede" style="margin:20px auto 28px;max-width:48ch">Browse plans, domains and business email, and manage your account on our hosting site.</p>'
    . '<a class="btn btn-acc" href="' . e($ext) . '" target="_blank" rel="noopener">Go to our hosting site &#8594;</a></div></section>');
  exit;
}

if (feature_on('visits')) track_visit($slug);   // first-party visit counter (admin dashboard) — 404s above are never counted

echo render_document($page, render_sections($page['id']));
