<?php

declare(strict_types=1);

/**
 * Message import (SPEC-IMPORT §3): six screens, in order. Start picks
 * the source and the files; Preview proves the parse before anything
 * commits; Mapping decides where every source area lands; Confirm
 * gates on the backup checkbox; Progress drives the chunked engine by
 * polling; Import History lists every batch with Resume and Undo.
 * Nothing runs unattended: the browser drives every chunk.
 */

require __DIR__ . '/_lib.php';
$adminUser = admin_require();
$db = atdt_db();
$settings = atdt_settings();

use Atdt\Import\Batch;
use Atdt\Import\Normalizer;
use Atdt\Import\Sources;

const IMPORT_DIR = ATDT_ROOT . '/data/import';

/** The newest batch that is not finished with life. */
function import_live(\Atdt\Db $db): ?array
{
    return $db->one(
        "SELECT * FROM import_batches
          WHERE status IN ('scanning', 'preview', 'staging', 'running', 'paused')
          ORDER BY id DESC LIMIT 1"
    );
}

/** Extract an uploaded zip with the §16.1 traversal guard. */
function import_extract_zip(string $zipPath, string $dest): ?string
{
    if (!class_exists(\ZipArchive::class)) {
        return 'This PHP has no zip extension; use the server-folder option instead.';
    }
    $z = new ZipArchive();
    if ($z->open($zipPath) !== true) {
        return 'That file would not open as a zip.';
    }
    for ($i = 0; $i < $z->numFiles; $i++) {
        $name = (string)$z->getNameIndex($i);
        if (str_contains($name, '..') || str_starts_with($name, '/') || preg_match('#^[A-Za-z]:#', $name) === 1) {
            $z->close();
            return 'The zip holds an unsafe path and was refused.';
        }
    }
    if (!@mkdir($dest, 0755, true) && !is_dir($dest)) {
        $z->close();
        return 'Could not create data/import/. Check permissions.';
    }
    if (!$z->extractTo($dest)) {
        $z->close();
        return 'Could not extract the zip into data/import/.';
    }
    $z->close();
    // People zip the folder, not its contents: descend one lonely dir.
    $entries = array_values(array_filter(glob($dest . '/*') ?: [], fn($f) => basename($f) !== '.'));
    if (count($entries) === 1 && is_dir($entries[0])) {
        return null;
    }
    return null;
}

/** The directory a batch should actually parse (post-extract). */
function import_resolve_dir(string $dest): string
{
    foreach ([$dest, ...array_filter(glob($dest . '/*') ?: [], 'is_dir')] as $cand) {
        foreach (['manifest.json', 'MESSAGES.DAT', 'messages.dat', 'MSGHDR.BBS', 'message.sqlite3'] as $marker) {
            if (is_file($cand . '/' . $marker)) {
                return $cand;
            }
        }
        foreach (['jhr', 'sqd', 'shd', 'msg'] as $ext) {
            foreach (glob($cand . '/*') ?: [] as $f) {
                if (is_file($f) && preg_match('/\.' . $ext . '$/i', $f) === 1) {
                    return $cand;
                }
            }
        }
    }
    $subs = array_values(array_filter(glob($dest . '/*') ?: [], 'is_dir'));
    return count($subs) === 1 ? $subs[0] : $dest;
}

/** Decode a stored raw sample with the batch's current choices. */
function import_render_sample(?array $s, string $encoding, string $tz): ?array
{
    if ($s === null) {
        return null;
    }
    $n = new Normalizer($encoding, $tz);
    return [
        'from' => $n->handle(base64_decode((string)($s['from'] ?? ''), true) ?: ''),
        'to' => $n->toHandle(base64_decode((string)($s['to'] ?? ''), true) ?: ''),
        'subject' => $n->subject(base64_decode((string)($s['subject'] ?? ''), true) ?: ''),
        'body' => $n->body(base64_decode((string)($s['body'] ?? ''), true) ?: ''),
        'date' => (string)($s['date'] ?? ''),
    ];
}

// ---------------------------------------------------------------------------
// Ajax: the polling loop that drives every chunk (§15.2).

$ajax = (string)($_GET['ajax'] ?? '');
if ($ajax !== '') {
    header('Content-Type: application/json; charset=utf-8');
    $id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
    if ($ajax === 'step' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
        $r = Batch::step($db, $settings, $id);
        $b = $r['batch'] ?? null;
        // Progress that tells the truth per phase: the write counter
        // freezes at the total once writing ends, and the reply-wiring
        // pass used to sit behind it for minutes looking finished.
        $phase = (string)($b['phase'] ?? '?');
        $total = (int)($b['total_messages'] ?? 0);
        $done = (int)($b['done_messages'] ?? 0);
        if ($b !== null && $phase === 'stage') {
            $done = (int)$db->val('SELECT COUNT(*) FROM import_staging WHERE batch_id = ?', [$id]);
        } elseif ($b !== null && $phase === 'link') {
            $done = (int)$db->val(
                'SELECT COUNT(*) FROM import_staging WHERE batch_id = ? AND written = 2', [$id]);
        } elseif ($phase === 'finish') {
            $done = $total;
        }
        echo json_encode([
            'ok' => (bool)$r['ok'],
            'error' => (string)($r['error'] ?? ($b['error'] ?? '')),
            'status' => (string)($b['status'] ?? '?'),
            'phase' => $phase,
            'total' => $total,
            'done' => $done,
        ]);
        exit;
    }
    if ($ajax === 'undostep' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
        $r = Batch::undo($db, $settings, $id);
        echo json_encode(['ok' => (bool)$r['ok'], 'done' => (bool)($r['done'] ?? false),
            'error' => (string)($r['error'] ?? '')]);
        exit;
    }
    if ($ajax === 'pause' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
        Batch::pause($db, $settings, $id);
        echo json_encode(['ok' => true]);
        exit;
    }
    if ($ajax === 'checkfolder') {
        $path = trim((string)($_GET['path'] ?? ''));
        $full = str_starts_with($path, '/') ? $path : ATDT_ROOT . '/' . ltrim($path, './');
        $real = realpath($full);
        if ($real === false || !is_dir($real)) {
            echo json_encode(['ok' => false, 'error' => 'ATDT cannot see a folder at that path.']);
            exit;
        }
        $names = [];
        foreach (array_slice(glob($real . '/*') ?: [], 0, 40) as $f) {
            $names[] = basename($f) . (is_dir($f) ? '/' : ' (' . number_format((int)filesize($f)) . ' bytes)');
        }
        echo json_encode(['ok' => true, 'entries' => $names, 'resolved' => $real]);
        exit;
    }
    echo json_encode(['ok' => false, 'error' => 'unknown call']);
    exit;
}

// ---------------------------------------------------------------------------
// POST actions.

if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
    switch ((string)($_POST['act'] ?? '')) {
        case 'start':
            if (import_live($db) !== null) {
                flash_set('err', 'An import is already in flight. Finish, resume, or undo it first.');
                admin_redirect('import.php');
            }
            $pick = (string)($_POST['source'] ?? '');
            $sub = (string)($_POST['sub'] ?? '');
            $format = Sources::resolve($pick, $sub);
            $entry = Sources::entry($pick);
            if ($format === null || $entry === null) {
                flash_set('err', 'Choose your old BBS software (and its message store, where it asks).');
                admin_redirect('import.php');
            }
            $label = '';
            $dir = '';
            if ((string)($_POST['how'] ?? 'zip') === 'folder') {
                $path = trim((string)($_POST['folder'] ?? 'data/import/'));
                $full = str_starts_with($path, '/') ? $path : ATDT_ROOT . '/' . ltrim($path, './');
                $real = realpath($full);
                if ($real === false || !is_dir($real)) {
                    flash_set('err', 'ATDT cannot see a folder at "' . $path . '". Use Check folder to look around.');
                    admin_redirect('import.php');
                }
                $dir = import_resolve_dir($real);
                $label = $path;
            } else {
                $up = $_FILES['zip'] ?? null;
                if (!is_array($up) || (int)($up['error'] ?? 1) !== UPLOAD_ERR_OK) {
                    $code = (int)($up['error'] ?? -1);
                    flash_set('err', $code === UPLOAD_ERR_INI_SIZE || $code === UPLOAD_ERR_FORM_SIZE
                        ? 'That file is bigger than your hosting allows for uploads ('
                            . (string)ini_get('upload_max_filesize')
                            . '). Put the files in data/import/ on the server instead.'
                        : 'The upload did not arrive. Try again.');
                    admin_redirect('import.php');
                }
                @mkdir(IMPORT_DIR, 0755, true);
                $stamp = gmdate('Ymd-His');
                $zipPath = IMPORT_DIR . '/upload-' . $stamp . '.zip';
                if (!move_uploaded_file((string)$up['tmp_name'], $zipPath)) {
                    flash_set('err', 'Could not save the upload into data/import/. Check permissions.');
                    admin_redirect('import.php');
                }
                $dest = IMPORT_DIR . '/batch-' . $stamp;
                $err = import_extract_zip($zipPath, $dest);
                @unlink($zipPath);
                if ($err !== null) {
                    atdt_log_event('security', 'import', 'Zip refused: ' . $err);
                    flash_set('err', $err);
                    admin_redirect('import.php');
                }
                $dir = import_resolve_dir($dest);
                $label = (string)($up['name'] ?? 'upload.zip');
            }
            $r = Batch::open($db, $settings, (int)$adminUser['id'],
                (string)($entry['title'] ?? $pick), $format, $label, $dir);
            if (!$r['ok']) {
                flash_set('err', (string)$r['error']);
                admin_redirect('import.php');
            }
            admin_log('Import started: ' . ($entry['title'] ?? $pick) . " ($format) from $label");
            admin_redirect('import.php?s=progress');

        case 'rerender':
            $id = (int)($_POST['id'] ?? 0);
            Batch::setRendering($db, $id,
                (string)($_POST['encoding'] ?? 'cp437'), (string)($_POST['timezone'] ?? 'UTC'));
            admin_redirect('import.php?s=preview');

        case 'tomapping':
            $id = (int)($_POST['id'] ?? 0);
            Batch::setRendering($db, $id,
                (string)($_POST['encoding'] ?? 'cp437'), (string)($_POST['timezone'] ?? 'UTC'));
            admin_redirect('import.php?s=mapping');

        case 'mapping':
            $id = (int)($_POST['id'] ?? 0);
            $b = Batch::row($db, $id);
            if ($b === null || (string)$b['status'] !== 'preview') {
                flash_set('err', 'That batch is not waiting at the mapping screen.');
                admin_redirect('import.php?s=history');
            }
            $plan = [];
            foreach ((array)($_POST['m'] ?? []) as $key => $row) {
                if (!is_array($row)) {
                    continue;
                }
                $plan[(string)$key] = [
                    'action' => (string)($row['action'] ?? 'skip'),
                    'area_id' => (int)($row['area_id'] ?? 0),
                    'tag' => (string)($row['tag'] ?? ''),
                    'name' => (string)($row['name'] ?? ''),
                    'descr' => (string)($row['descr'] ?? ''),
                    'acs_read' => (string)($row['acs_read'] ?? ''),
                    'acs_write' => (string)($row['acs_write'] ?? ''),
                    'category_id' => (int)($_POST['category_id'] ?? 0),
                ];
            }
            // "Create new category..." (§3.3): one name, made once.
            $catNew = trim((string)($_POST['category_new'] ?? ''));
            if ((string)($_POST['category_id'] ?? '') === 'new' && $catNew !== '') {
                $catId = (int)$db->insert('message_categories', [
                    'name' => mb_strimwidth($catNew, 0, 30),
                    'sort' => 10 + (int)$db->val('SELECT COALESCE(MAX(sort), 0) FROM message_categories'),
                ]);
                foreach ($plan as &$p) {
                    $p['category_id'] = $catId;
                }
                unset($p);
            }
            $_SESSION['import_plan'] = $plan;
            admin_redirect('import.php?s=confirm');

        case 'run':
            $id = (int)($_POST['id'] ?? 0);
            if ((string)($_POST['backedup'] ?? '') !== '1') {
                flash_set('err', 'Tick the box that says you backed up data/ first. It matters.');
                admin_redirect('import.php?s=confirm');
            }
            $plan = (array)($_SESSION['import_plan'] ?? []);
            $r = Batch::confirm($db, $settings, $id, $plan);
            if (!$r['ok']) {
                flash_set('err', (string)$r['error']);
                admin_redirect('import.php?s=mapping');
            }
            unset($_SESSION['import_plan']);
            admin_log("Import batch #$id confirmed and running");
            admin_redirect('import.php?s=progress');

        case 'resume':
            $id = (int)($_POST['id'] ?? 0);
            $r = Batch::resume($db, $settings, $id);
            if (!$r['ok']) {
                flash_set('err', (string)$r['error']);
                admin_redirect('import.php?s=history');
            }
            admin_log("Import batch #$id resumed");
            admin_redirect('import.php?s=progress');

        case 'pause':
            $id = (int)($_POST['id'] ?? 0);
            Batch::pause($db, $settings, $id);
            admin_log("Import batch #$id paused");
            admin_redirect('import.php?s=history');

        case 'undo':
            $id = (int)($_POST['id'] ?? 0);
            if ((string)($_POST['confirm'] ?? '') !== 'UNDO') {
                flash_set('err', 'Type UNDO, in capitals, to take an import back out.');
                admin_redirect('import.php?s=history');
            }
            admin_log("Import batch #$id undo started");
            admin_redirect('import.php?s=history&undo=' . $id);
    }
}

// ---------------------------------------------------------------------------
// Which screen?

$live = import_live($db);
$s = (string)($_GET['s'] ?? '');
if ($s === '') {
    $s = $live === null ? 'start' : match ((string)$live['status']) {
        'scanning', 'staging', 'running' => 'progress',
        'preview' => 'preview',
        default => 'start',
    };
}
$title = 'Import messages';
admin_page_start($title, 'import');
$catList = $db->all('SELECT * FROM message_categories ORDER BY sort, id');
$stats = $live === null ? [] : (json_decode((string)$live['stats'], true) ?: []);

// ---------------------------------------------------------------------------
if ($s === 'start'): ?>

<div class="cols"><div>
  <div class="card">
    <p>This reads the message history out of another bulletin board system and adds
    it to this board's areas. Nothing here replaces or rewrites what your callers
    have already posted.</p>
    <form method="post" enctype="multipart/form-data" id="startform">
      <?= csrf_field() ?>
      <input type="hidden" name="act" value="start">
      <label>Where are your messages coming from?
        <select name="source" id="srcpick">
          <option value="">Choose your old BBS software...</option>
          <optgroup label="Common BBS packages">
            <?php foreach (Sources::PACKAGES as $key => $p): ?>
            <option value="<?= h($key) ?>"><?= h((string)$p['title']) ?></option>
            <?php endforeach; ?>
          </optgroup>
          <optgroup label="I know my format">
            <?php foreach (Sources::FORMATS as $key => $p): ?>
            <option value="<?= h($key) ?>"><?= h((string)$p['title']) ?></option>
            <?php endforeach; ?>
          </optgroup>
          <optgroup label="Anything else">
            <?php foreach (Sources::OTHER as $key => $p): ?>
            <option value="<?= h($key) ?>"><?= h((string)$p['title']) ?></option>
            <?php endforeach; ?>
          </optgroup>
        </select>
      </label>
      <div id="subwrap" style="display:none">
        <label>Which message store did it use?
          <select name="sub" id="subpick"></select>
        </label>
      </div>
      <div class="card" id="helppanel" style="display:none">
        <h3>What to find</h3>
        <p><strong id="hp0"></strong></p>
        <p id="hp1"></p>
        <p class="hint" id="hp2"></p>
        <p class="hint" id="hp3"></p>
      </div>
      <label>How will you provide the files?</label>
      <label class="radio"><input type="radio" name="how" value="zip" checked>
        Upload a .zip <span class="hint">(your hosting allows uploads up to
        <?= h((string)ini_get('upload_max_filesize')) ?>)</span></label>
      <label class="radio"><input type="radio" name="how" value="folder">
        Read from a folder on the server
        <span class="hint">(the path for big imports: copy the files up with your
        hosting file manager first)</span></label>
      <div id="zipwrap">
        <label>The zip <input type="file" name="zip" accept=".zip,.qwk,application/zip"></label>
      </div>
      <div id="folderwrap" style="display:none">
        <label>Folder <input type="text" name="folder" id="folderpath" value="data/import/"></label>
        <button type="button" class="btn small secondary" id="checkfolder">Check folder</button>
        <pre class="hint" id="folderlist" style="display:none"></pre>
      </div>
      <br><button class="btn" id="parsebtn" disabled>Parse and preview</button>
    </form>
  </div>
</div><div>
  <div class="card">
    <h3>Before you start</h3>
    <p class="hint">The import can be undone from Import History afterward, but the
    real safety net is a copy of your <code>data/</code> folder. Make one.</p>
    <p class="hint">Running the same file twice imports every message twice: there
    is no duplicate detection, on purpose. Undo the first batch instead.</p>
    <p class="hint">Big import on a live board? The
    <a href="settings.php#offline">offline switch</a> turns new callers away
    with a notice of your choosing until you flip it back.</p>
    <p><a class="btn small secondary" href="import.php?s=history">Import History</a></p>
  </div>
</div></div>

<script>
(function () {
  var entries = <?= json_encode([
      'packages' => Sources::PACKAGES, 'formats' => Sources::FORMATS, 'other' => Sources::OTHER,
  ]) ?>;
  var pick = document.getElementById('srcpick');
  var subw = document.getElementById('subwrap');
  var sub = document.getElementById('subpick');
  var help = document.getElementById('helppanel');
  var btn = document.getElementById('parsebtn');
  function entry(key) {
    return entries.packages[key] || entries.formats[key] || entries.other[key] || null;
  }
  function refresh() {
    var e = entry(pick.value);
    subw.style.display = 'none';
    sub.innerHTML = '';
    if (!e) { help.style.display = 'none'; btn.disabled = true; return; }
    if (e.ask) {
      subw.style.display = '';
      Object.keys(e.ask).forEach(function (k) {
        var o = document.createElement('option');
        o.value = k; o.textContent = e.ask[k];
        sub.appendChild(o);
      });
    }
    var hp = e.help || ['', '', '', ''];
    document.getElementById('hp0').textContent = hp[0];
    document.getElementById('hp1').textContent = 'Files needed: ' + hp[1];
    document.getElementById('hp2').textContent = 'Where they usually live: ' + hp[2];
    document.getElementById('hp3').textContent = hp[3];
    document.getElementById('hp3').style.display = hp[3] ? '' : 'none';
    help.style.display = '';
    btn.disabled = false;
  }
  pick.addEventListener('change', refresh);
  document.querySelectorAll('input[name=how]').forEach(function (r) {
    r.addEventListener('change', function () {
      document.getElementById('zipwrap').style.display = r.value === 'zip' && r.checked ? '' : 'none';
      document.getElementById('folderwrap').style.display = r.value === 'folder' && r.checked ? '' : 'none';
    });
  });
  document.getElementById('checkfolder').addEventListener('click', function () {
    var p = document.getElementById('folderpath').value;
    fetch('import.php?ajax=checkfolder&path=' + encodeURIComponent(p), {credentials: 'same-origin'})
      .then(function (r) { return r.json(); })
      .then(function (j) {
        var out = document.getElementById('folderlist');
        out.style.display = '';
        out.textContent = j.ok ? (j.entries.length ? j.entries.join('\n') : '(the folder is empty)') : j.error;
      });
  });
})();
</script>

<?php elseif ($s === 'preview' && $live !== null && (string)$live['status'] === 'preview'):
    $survey = (array)($stats['survey'] ?? []);
    $areasMeta = (array)($stats['areas'] ?? []);
    $samples = (array)($stats['samples'] ?? []);
    $enc = (string)$live['encoding'];
    $tz = (string)$live['timezone'];
    $first = null;
    $last = null;
    foreach ($survey as $sv) {
        if (($sv['first'] ?? null) !== null && ($first === null || $sv['first'] < $first)) {
            $first = $sv['first'];
        }
        if (($sv['last'] ?? null) !== null && ($last === null || $sv['last'] > $last)) {
            $last = $sv['last'];
        }
    }
?>

<div class="card">
  <p><strong><?= h((string)$live['source_package']) ?></strong>
    · format <?= h((string)$live['source_format']) ?>
    · parsed <code><?= h((string)$live['source_label']) ?></code></p>
  <p><strong><?= count($survey) ?></strong> area(s) found,
    <strong><?= number_format((int)$live['total_messages']) ?></strong> messages<?=
        $first !== null ? ', dated ' . h(substr((string)$first, 0, 10)) . ' to ' . h(substr((string)$last, 0, 10)) : '' ?>.
    <?php $sk = (int)($stats['skipped'] ?? 0); if ($sk > 0): ?>
      <span class="hint"><?= number_format($sk) ?> unparseable record(s) skipped.</span>
    <?php endif; ?>
  </p>
  <table class="list">
    <tr><th>Source area</th><th>Tag</th><th>Messages</th><th>Earliest</th><th>Latest</th></tr>
    <?php foreach ($survey as $key => $sv): $meta = (array)($areasMeta[$key] ?? []); ?>
    <tr>
      <td><?= h((string)($meta['name'] ?? "Area $key")) ?></td>
      <td><code><?= h((string)($meta['tag'] ?? '')) ?></code></td>
      <td><?= number_format((int)$sv['count']) ?></td>
      <td><?= h(substr((string)($sv['first'] ?? ''), 0, 10)) ?></td>
      <td><?= h(substr((string)($sv['last'] ?? ''), 0, 10)) ?></td>
    </tr>
    <?php endforeach; ?>
  </table>
</div>

<div class="card">
  <h3>Three messages, as they will be stored</h3>
  <p class="hint">From the largest area: the earliest, one from the middle, and the
  latest. If accents or box characters look wrong here, change the encoding and
  re-render before importing anything.</p>
  <?php foreach (['first' => 'Earliest', 'mid' => 'Middle', 'last' => 'Latest'] as $slot => $label):
      $sm = import_render_sample($samples[$slot] ?? null, $enc, $tz);
      if ($sm === null) {
          continue;
      }
  ?>
  <h3><?= h($label) ?></h3>
  <pre class="sample">From: <?= h($sm['from']) ?><?= $sm['to'] !== null ? '   To: ' . h($sm['to']) : '' ?>   Date: <?= h(substr($sm['date'], 0, 16)) ?>

Subj: <?= h($sm['subject']) ?>

<?= h($sm['body']) ?></pre>
  <?php endforeach; ?>
  <?php $subs = (int)($stats['substitutions'] ?? 0); if ($subs > 0): ?>
    <p class="hint"><?= number_format($subs) ?> character(s) had no CP437 home during the
    scan and became question marks. A high count usually means the wrong encoding.</p>
  <?php endif; ?>
  <form method="post" class="row">
    <?= csrf_field() ?>
    <input type="hidden" name="act" value="rerender">
    <input type="hidden" name="id" value="<?= (int)$live['id'] ?>">
    <label>Source character encoding
      <select name="encoding">
        <?php foreach (Normalizer::ENCODINGS as $e): ?>
        <option value="<?= h($e) ?>" <?= $e === $enc ? 'selected' : '' ?>><?= h($e) ?></option>
        <?php endforeach; ?>
      </select></label>
    <label>Source timezone
      <select name="timezone">
        <option value="UTC" <?= $tz === 'UTC' ? 'selected' : '' ?>>Timestamps are already UTC</option>
        <?php foreach (DateTimeZone::listIdentifiers() as $z): if ($z === 'UTC') { continue; } ?>
        <option value="<?= h($z) ?>" <?= $z === $tz ? 'selected' : '' ?>><?= h($z) ?></option>
        <?php endforeach; ?>
      </select></label>
    <button class="btn secondary">Re-render samples</button>
  </form>
</div>

<div class="card">
  <form method="post" class="row">
    <?= csrf_field() ?>
    <input type="hidden" name="act" value="tomapping">
    <input type="hidden" name="id" value="<?= (int)$live['id'] ?>">
    <input type="hidden" name="encoding" value="<?= h($enc) ?>">
    <input type="hidden" name="timezone" value="<?= h($tz) ?>">
    <a class="btn secondary" href="import.php?s=history">Back</a>
    <button class="btn">Continue to mapping</button>
  </form>
</div>

<?php elseif ($s === 'mapping' && $live !== null && (string)$live['status'] === 'preview'):
    $survey = (array)($stats['survey'] ?? []);
    $areasMeta = (array)($stats['areas'] ?? []);
    $existing = $db->all("SELECT * FROM message_areas WHERE kind <> 'mail' ORDER BY sort, id");
    $taken = [];
?>

<div class="card">
  <p>Decide, per source area, where its messages land. <strong>Skip</strong> leaves
  an area behind; <strong>Import into existing</strong> adds to a board you already
  run; <strong>Create new</strong> makes a fresh area. A blank tag needs your
  attention: ATDT suggests but never invents.</p>
  <form method="post" id="mapform">
    <?= csrf_field() ?>
    <input type="hidden" name="act" value="mapping">
    <input type="hidden" name="id" value="<?= (int)$live['id'] ?>">
    <label>Category for newly created areas
      <select name="category_id" id="catpick">
        <option value="0">Uncategorized</option>
        <?php foreach ($catList as $c): ?>
        <option value="<?= (int)$c['id'] ?>"><?= h((string)$c['name']) ?></option>
        <?php endforeach; ?>
        <option value="new">Create new category...</option>
      </select></label>
    <input type="text" name="category_new" id="catnew" placeholder="New category name (30 max)"
           maxlength="30" style="display:none">
    <div class="row" style="margin:.6rem 0">
      <button type="button" class="btn small secondary" id="skipall">Skip all</button>
      <button type="button" class="btn small secondary" id="createall">Create all as new</button>
      <input type="text" id="bulkread" placeholder="Read ACS" size="8">
      <button type="button" class="btn small secondary" id="applyread">Set read ACS for all</button>
      <input type="text" id="bulkwrite" placeholder="Write ACS" size="8">
      <button type="button" class="btn small secondary" id="applywrite">Set write ACS for all</button>
    </div>
    <table class="list" id="maptable">
      <tr><th>Source area</th><th>Msgs</th><th>Action</th><th>Existing area</th>
          <th>Tag</th><th>Name</th><th>Description</th><th>Read ACS</th><th>Write ACS</th></tr>
      <?php foreach ($survey as $key => $sv):
          $meta = (array)($areasMeta[$key] ?? []);
          $suggest = Batch::suggestTag($db, (string)($meta['tag'] ?? ''), (string)($meta['name'] ?? "Area $key"), $taken);
          if ($suggest !== '') {
              $taken[$suggest] = true;
          }
      ?>
      <tr data-key="<?= h((string)$key) ?>">
        <td><?= h((string)($meta['name'] ?? "Area $key")) ?>
            <?php if ((string)($meta['tag'] ?? '') !== ''): ?><code><?= h((string)$meta['tag']) ?></code><?php endif; ?></td>
        <td><?= number_format((int)$sv['count']) ?></td>
        <td><select name="m[<?= h((string)$key) ?>][action]" class="mact">
          <option value="skip">Skip</option>
          <option value="existing">Import into existing area</option>
          <option value="create" <?= $suggest !== '' ? '' : '' ?>>Create new area</option>
        </select></td>
        <td><select name="m[<?= h((string)$key) ?>][area_id]" class="marea" style="display:none">
          <?php foreach ($existing as $a): ?>
          <option value="<?= (int)$a['id'] ?>"><?= h((string)$a['tag'] . ' - ' . (string)$a['name']) ?></option>
          <?php endforeach; ?>
        </select></td>
        <td><input type="text" name="m[<?= h((string)$key) ?>][tag]" class="mtag" maxlength="12" size="9"
                   value="<?= h($suggest) ?>" style="display:none"></td>
        <td><input type="text" name="m[<?= h((string)$key) ?>][name]" class="mname" maxlength="40" size="18"
                   value="<?= h(mb_strimwidth((string)($meta['name'] ?? "Area $key"), 0, 40)) ?>" style="display:none"></td>
        <td><input type="text" name="m[<?= h((string)$key) ?>][descr]" class="mdescr" maxlength="120" size="18"
                   value="<?= h(mb_strimwidth((string)($meta['descr'] ?? ($meta['name'] ?? '')), 0, 120)) ?>" style="display:none"></td>
        <td><input type="text" name="m[<?= h((string)$key) ?>][acs_read]" class="mread" size="6" style="display:none"></td>
        <td><input type="text" name="m[<?= h((string)$key) ?>][acs_write]" class="mwrite" size="6" style="display:none"></td>
      </tr>
      <?php endforeach; ?>
    </table>
    <p class="hint" id="mapwarn" style="display:none"></p>
    <div class="row">
      <a class="btn secondary" href="import.php?s=preview">Back</a>
      <button class="btn" id="mapgo">Continue</button>
    </div>
  </form>
</div>

<script>
(function () {
  var existingTags = <?= json_encode(array_map(fn($a) => strtoupper((string)$a['tag']), $existing)) ?>;
  function rows() { return Array.prototype.slice.call(document.querySelectorAll('#maptable tr[data-key]')); }
  function refreshRow(tr) {
    var act = tr.querySelector('.mact').value;
    tr.querySelector('.marea').style.display = act === 'existing' ? '' : 'none';
    ['.mtag', '.mname', '.mdescr', '.mread', '.mwrite'].forEach(function (sel) {
      tr.querySelector(sel).style.display = act === 'create' ? '' : 'none';
    });
    validate();
  }
  function validate() {
    var seen = {};
    var bad = 0;
    rows().forEach(function (tr) {
      var act = tr.querySelector('.mact').value;
      var tagEl = tr.querySelector('.mtag');
      tagEl.style.background = '';
      tagEl.style.borderColor = '';
      if (act !== 'create') { return; }
      var tag = tagEl.value.toUpperCase().trim();
      var ok = /^[A-Z0-9_]{2,12}$/.test(tag) && existingTags.indexOf(tag) < 0 && !seen[tag];
      if (tag) { seen[tag] = true; }
      if (!ok) { tagEl.style.background = '#fde2e2'; tagEl.style.borderColor = '#b91c1c'; bad++; }
    });
    var anyImport = rows().some(function (tr) { return tr.querySelector('.mact').value !== 'skip'; });
    var warn = document.getElementById('mapwarn');
    warn.style.display = bad || !anyImport ? '' : 'none';
    warn.textContent = bad
      ? bad + ' row(s) need attention: a tag must be 2-12 of A-Z 0-9 _ and unused.'
      : (!anyImport ? 'Every row is set to Skip; there is nothing to import.' : '');
    document.getElementById('mapgo').disabled = !!bad || !anyImport;
    return !bad && anyImport;
  }
  rows().forEach(function (tr) {
    tr.querySelector('.mact').addEventListener('change', function () { refreshRow(tr); });
    tr.querySelector('.mtag').addEventListener('input', validate);
    refreshRow(tr);
  });
  document.getElementById('skipall').addEventListener('click', function () {
    rows().forEach(function (tr) { tr.querySelector('.mact').value = 'skip'; refreshRow(tr); });
  });
  document.getElementById('createall').addEventListener('click', function () {
    rows().forEach(function (tr) { tr.querySelector('.mact').value = 'create'; refreshRow(tr); });
  });
  document.getElementById('applyread').addEventListener('click', function () {
    var v = document.getElementById('bulkread').value;
    rows().forEach(function (tr) { tr.querySelector('.mread').value = v; });
  });
  document.getElementById('applywrite').addEventListener('click', function () {
    var v = document.getElementById('bulkwrite').value;
    rows().forEach(function (tr) { tr.querySelector('.mwrite').value = v; });
  });
  document.getElementById('catpick').addEventListener('change', function () {
    document.getElementById('catnew').style.display = this.value === 'new' ? '' : 'none';
  });
  document.getElementById('mapform').addEventListener('submit', function (ev) {
    if (!validate()) { ev.preventDefault(); }
  });
  validate();
})();
</script>

<?php elseif ($s === 'confirm' && $live !== null && (string)$live['status'] === 'preview'):
    $plan = (array)($_SESSION['import_plan'] ?? []);
    $survey = (array)($stats['survey'] ?? []);
    $creates = 0;
    $appends = 0;
    $total = 0;
    $appendNames = [];
    foreach ($plan as $key => $p) {
        $count = (int)($survey[$key]['count'] ?? 0);
        if (($p['action'] ?? 'skip') === 'create') {
            $creates++;
            $total += $count;
        } elseif (($p['action'] ?? '') === 'existing') {
            $appends++;
            $total += $count;
            $a = $db->one('SELECT * FROM message_areas WHERE id = ?', [(int)$p['area_id']]);
            if ($a !== null && (int)$db->val('SELECT COUNT(*) FROM messages WHERE area_id = ?', [(int)$a['id']]) > 0) {
                $appendNames[] = (string)$a['name'];
            }
        }
    }
?>

<div class="card">
  <p><strong><?= $creates ?></strong> new area(s) will be created.<br>
     <strong><?= $appends ?></strong> existing area(s) will receive messages.<br>
     <strong><?= number_format($total) ?></strong> messages will be imported.</p>
  <?php if ($appendNames !== []): ?>
  <p class="hint">These areas already contain messages. Imported messages will be
  added to them: <?= h(implode(', ', $appendNames)) ?>.</p>
  <?php endif; ?>
  <?php if ($total > 50000): ?>
  <p class="hint">That is a big one: the import will take a while and data/ will
  grow substantially. Leave the progress page open while it runs.</p>
  <?php endif; ?>
  <div class="flash bad">This can be undone from Import History, or by restoring
  your data/ backup. There is no other undo.</div>
  <form method="post">
    <?= csrf_field() ?>
    <input type="hidden" name="act" value="run">
    <input type="hidden" name="id" value="<?= (int)$live['id'] ?>">
    <label class="radio"><input type="checkbox" name="backedup" value="1" id="backedup">
      I have backed up my <code>data/</code> directory.</label>
    <div class="row">
      <a class="btn secondary" href="import.php?s=mapping">Back</a>
      <button class="btn danger" id="runbtn" disabled>Start import</button>
    </div>
  </form>
</div>
<script>
document.getElementById('backedup').addEventListener('change', function () {
  document.getElementById('runbtn').disabled = !this.checked;
});
</script>

<?php elseif ($s === 'progress' && $live !== null):
    $phaseWords = ['scan' => 'Reading the source', 'stage' => 'Normalizing into staging',
        'write' => 'Writing messages', 'link' => 'Wiring reply threads', 'finish' => 'Finishing up'];
?>

<div class="card">
  <h3 id="pphase"><?= h($phaseWords[(string)$live['phase']] ?? 'Working') ?></h3>
  <div style="background:#22262e;border-radius:6px;overflow:hidden;height:18px">
    <div id="pbar" style="background:#1f6feb;height:18px;width:0"></div>
  </div>
  <p><span id="pcount">...</span> · elapsed <span id="pelapsed">0:00</span></p>
  <pre class="hint" id="plog" style="max-height:12em;overflow-y:auto"></pre>
  <p class="hint">You can leave this page. The import will pause and can be resumed
  from Import History.</p>
  <div class="row">
    <form method="post"><?= csrf_field() ?>
      <input type="hidden" name="act" value="pause">
      <input type="hidden" name="id" value="<?= (int)$live['id'] ?>">
      <button class="btn small secondary">Pause</button></form>
    <a class="btn small secondary" href="import.php?s=history">Import History</a>
  </div>
</div>

<script>
(function () {
  var id = <?= (int)$live['id'] ?>;
  var csrf = <?= json_encode(admin_csrf_token()) ?>;
  var words = <?= json_encode($phaseWords) ?>;
  var t0 = Date.now();
  var log = [];
  var stopped = false;
  var lastLine = '';
  function line(s) {
    log.push(s);
    if (log.length > 10) { log.shift(); }
    document.getElementById('plog').textContent = log.join('\n');
  }
  function tick() {
    if (stopped) { return; }
    var fd = new FormData();
    fd.append('csrf', csrf);
    fd.append('id', id);
    fetch('import.php?ajax=step', {method: 'POST', body: fd, credentials: 'same-origin'})
      .then(function (r) { return r.json(); })
      .then(function (j) {
        var el = Math.floor((Date.now() - t0) / 1000);
        document.getElementById('pelapsed').textContent = Math.floor(el / 60) + ':' + ('0' + el % 60).slice(-2);
        if (!j.ok) { line(j.error || 'error'); stopped = true; return; }
        var pct = j.total > 0 ? Math.min(100, Math.round(100 * j.done / j.total)) : 5;
        document.getElementById('pbar').style.width = pct + '%';
        document.getElementById('pphase').textContent = words[j.phase] || 'Working';
        document.getElementById('pcount').textContent = j.total > 0
          ? j.done.toLocaleString() + ' of ' + j.total.toLocaleString() + ' messages · ' + j.phase
          : j.done.toLocaleString() + ' messages read so far';
        // One log line per change of pace, not one per poll: a quiet
        // stretch used to fill the pane with identical lines.
        var now = j.status + ' / ' + j.phase + ' · ' + j.done + ' done';
        if (now !== lastLine) { line(now); lastLine = now; }
        if (j.status === 'preview') { window.location = 'import.php?s=preview'; return; }
        if (j.status === 'complete') { window.location = 'import.php?s=history&done=' + id; return; }
        if (j.status === 'failed') { line('FAILED: ' + (j.error || '')); stopped = true; return; }
        if (j.status === 'paused') { stopped = true; return; }
        setTimeout(tick, 60);
      })
      .catch(function () { setTimeout(tick, 1500); });
  }
  window.addEventListener('pagehide', function () {
    stopped = true;
    var fd = new FormData();
    fd.append('csrf', csrf);
    fd.append('id', id);
    navigator.sendBeacon && navigator.sendBeacon('import.php?ajax=pause', fd);
  });
  tick();
})();
</script>

<?php else:
    // ---- Screen 6: Import History (the permanent page) -------------------
    $doneId = (int)($_GET['done'] ?? 0);
    $undoId = (int)($_GET['undo'] ?? 0);
    $batches = $db->all('SELECT * FROM import_batches ORDER BY id DESC LIMIT 50');
?>

<?php if ($doneId > 0): $b = Batch::row($db, $doneId); $st = $b === null ? [] : (json_decode((string)$b['stats'], true) ?: []); ?>
<div class="flash ok">Batch #<?= $doneId ?> complete:
  <?= number_format((int)($b['done_messages'] ?? 0)) ?> messages imported.
  <?= (int)($st['skipped'] ?? 0) ?> skipped ·
  <?= (int)($st['substitutions'] ?? 0) ?> character substitutions ·
  <?= (int)($st['undated'] ?? 0) ?> undated ·
  <?= (int)($st['orphans'] ?? 0) ?> orphan replies became roots ·
  <?= (int)($st['cycles'] ?? 0) ?> reply cycles broken.
  Everything imported is already marked read for your current callers.</div>
<?php endif; ?>

<div class="card">
  <h3>Import History</h3>
  <?php if ($batches === []): ?><p class="hint">No imports yet.</p><?php else: ?>
  <table class="list">
    <tr><th>#</th><th>Date</th><th>Source</th><th>Areas</th><th>Messages</th><th>Status</th><th></th></tr>
    <?php foreach ($batches as $b):
        $created = count(json_decode((string)$b['created_areas'], true) ?: []);
        $bstats = json_decode((string)$b['stats'], true) ?: [];
        $touched = count((array)($bstats['touched'] ?? [])) ?: null;
    ?>
    <tr>
      <td><?= (int)$b['id'] ?></td>
      <td><?= h(substr((string)$b['created_at'], 0, 16)) ?></td>
      <td><?= h((string)$b['source_package']) ?> <span class="hint">(<?= h((string)$b['source_format']) ?>)</span><br>
          <span class="hint"><?= h((string)$b['source_label']) ?></span></td>
      <td><?= $created ?> created<?= $touched !== null ? ', ' . max(0, $touched - $created) . ' appended' : '' ?></td>
      <td><?= number_format((int)$b['done_messages']) ?><?= (int)$b['total_messages'] > 0
          ? ' / ' . number_format((int)$b['total_messages']) : '' ?></td>
      <td><?= h((string)$b['status']) ?><?= (string)$b['error'] !== '' && $b['error'] !== null
          ? '<br><span class="hint">' . h((string)$b['error']) . '</span>' : '' ?></td>
      <td class="nowrap">
        <?php if (in_array((string)$b['status'], ['paused', 'failed', 'scanning', 'staging', 'running'], true)): ?>
        <form method="post" class="inline"><?= csrf_field() ?>
          <input type="hidden" name="act" value="resume">
          <input type="hidden" name="id" value="<?= (int)$b['id'] ?>">
          <button class="btn small secondary">Resume</button></form>
        <?php endif; ?>
        <?php if (in_array((string)$b['status'], ['complete', 'failed', 'paused'], true)): ?>
        <form method="post" class="inline"
              onsubmit="return this.confirm.value === 'UNDO' || (alert('Type UNDO, in capitals.'), false);">
          <?= csrf_field() ?>
          <input type="hidden" name="act" value="undo">
          <input type="hidden" name="id" value="<?= (int)$b['id'] ?>">
          <input type="text" name="confirm" placeholder="UNDO" size="5">
          <button class="btn small danger">Undo</button></form>
        <?php endif; ?>
      </td>
    </tr>
    <?php endforeach; ?>
  </table>
  <?php endif; ?>
  <p><a class="btn small secondary" href="import.php">Start an import</a></p>
</div>

<?php if ($undoId > 0): ?>
<div class="card"><h3>Undoing batch #<?= $undoId ?></h3>
  <p id="undostate">Deleting imported messages...</p></div>
<script>
(function () {
  var csrf = <?= json_encode(admin_csrf_token()) ?>;
  function tick() {
    var fd = new FormData();
    fd.append('csrf', csrf);
    fd.append('id', <?= $undoId ?>);
    fetch('import.php?ajax=undostep', {method: 'POST', body: fd, credentials: 'same-origin'})
      .then(function (r) { return r.json(); })
      .then(function (j) {
        if (!j.ok) { document.getElementById('undostate').textContent = j.error || 'error'; return; }
        if (j.done) { window.location = 'import.php?s=history'; return; }
        setTimeout(tick, 40);
      });
  }
  tick();
})();
</script>
<?php endif; ?>

<?php endif; ?>

<?php admin_page_end(); ?>
