<?php

declare(strict_types=1);

/**
 * Community desk (owner round 2): everything callers write that the sysop
 * curates: bulletins (add/edit/delete), oneliners and rumors (delete),
 * the voting booth (create/close/delete questions), and the BBS directory
 * (edit/delete entries). One page, five tabs.
 */

require __DIR__ . '/_lib.php';
$me = admin_require();
$db = atdt_db();

// Alphabetical by label (owner request, 1.6.0); Auto-message greets first.
const TABS = [
    'automsg' => 'Auto-message',
    'bbslist' => 'BBS list',
    'bulletins' => 'Bulletins',
    'boards' => 'Message boards',
    'oneliners' => 'Oneliners',
    'rumors' => 'Rumors',
    'voting' => 'Voting booth',
];

$tab = (string)($_GET['tab'] ?? 'automsg');
if (!isset(TABS[$tab])) {
    $tab = 'automsg';
}

/** Bounce back to a tab after a POST (kept plain for old PHP parsers). */
function back_tab(string $t): never
{
    admin_redirect('community.php?tab=' . $t);
}

/**
 * Wrap one input line the way the caller editor does (§7.5): 79 columns,
 * breaking at spaces, hard-splitting a word longer than the whole row.
 * @return list<string>
 */
function msg_wrap79(string $line): array
{
    $out = [];
    while (mb_strlen($line) > 79) {
        $cut = mb_substr($line, 0, 79);
        $sp = mb_strrpos($cut, ' ');
        if ($sp === false || $sp < 1) {
            $sp = 79; // one unbroken word: split it, like the editor
        }
        $out[] = rtrim(mb_substr($line, 0, $sp));
        $line = ltrim(mb_substr($line, $sp));
    }
    $out[] = $line;
    return $out;
}

/** A board message with its area kind, or null; mail never qualifies. */
function board_msg(\Atdt\Db $db, int $id): ?array
{
    $m = $db->one(
        'SELECT m.*, a.kind AS area_kind, a.name AS area_name
         FROM messages m JOIN message_areas a ON a.id = m.area_id WHERE m.id = ?',
        [$id]
    );
    // Private mail is a caller's own drawer: the console curates the
    // BOARDS, it does not read anyone's letters (1.6.0).
    return $m === null || (string)$m['area_kind'] === 'mail' ? null : $m;
}

if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
    $id = (int)($_POST['id'] ?? 0);
    switch ((string)($_POST['action'] ?? '')) {
        case 'msg_save':
            // Everything except WHO wrote it and WHERE it sits in the
            // thread: subject, body, and the timestamp are the sysop's
            // to fix (1.6.0). Author and reply linkage never change.
            $m = board_msg($db, $id);
            if ($m === null) {
                flash_set('err', 'No such board message.');
                back_tab('boards');
            }
            $subject = mb_strimwidth(trim((string)($_POST['subject'] ?? '')), 0, 72);
            $body = str_replace("\r", '', (string)($_POST['body'] ?? ''));
            if ($subject === '' || trim($body) === '') {
                flash_set('err', 'A message keeps a subject and a body.');
                admin_redirect('community.php?tab=boards&area=' . (int)$m['area_id'] . '&msg=' . $id);
            }
            $utc = (string)$m['posted_at'];
            $when = trim((string)($_POST['posted_at'] ?? ''));
            if ($when !== '') {
                try {
                    // datetime-local arrives board-local; store UTC.
                    $tz = new DateTimeZone((string)(atdt_settings()->get('timezone', 'UTC') ?? 'UTC'));
                    $utc = (new DateTimeImmutable($when, $tz))
                        ->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d\TH:i:s\Z');
                } catch (\Throwable $e) {
                    // an unparseable stamp keeps the original
                }
            }
            $db->q(
                'UPDATE messages SET subject = ?, body = ?, posted_at = ? WHERE id = ?',
                [$subject, $body, $utc, $id]
            );
            admin_log('Edited message #' . (int)$m['num'] . ' in ' . (string)$m['area_name']);
            flash_set('ok', 'Message saved. The author and its place in the thread never change.');
            admin_redirect('community.php?tab=boards&area=' . (int)$m['area_id'] . '&msg=' . $id);

        case 'msg_del':
            $m = board_msg($db, $id);
            if ($m === null) {
                flash_set('err', 'No such board message.');
                back_tab('boards');
            }
            $db->q('UPDATE messages SET deleted = 1 WHERE id = ?', [$id]);
            admin_log('Deleted message #' . (int)$m['num'] . ' in ' . (string)$m['area_name']);
            flash_set('ok', 'Message #' . (int)$m['num'] . ' is off the board.');
            admin_redirect('community.php?tab=boards&area=' . (int)$m['area_id']);

        case 'msg_post':
            // The sysop joins the conversation from the console (1.6.0):
            // post or reply through the SAME door callers use -
            // Messages::post threads it, numbers it, and stamps it - with
            // the body wrapped at the caller editor's 79 columns so a
            // console-written message never paints past the terminal edge.
            $areaId = (int)($_POST['area'] ?? 0);
            $area = $db->one("SELECT * FROM message_areas WHERE id = ? AND kind <> 'mail'", [$areaId]);
            if ($area === null) {
                flash_set('err', 'No such board.');
                back_tab('boards');
            }
            $replyTo = null;
            $rid = (int)($_POST['reply_to'] ?? 0);
            if ($rid > 0) {
                $replyTo = board_msg($db, $rid);
                if ($replyTo === null || (int)$replyTo['area_id'] !== $areaId) {
                    flash_set('err', 'That message is not on this board.');
                    back_tab('boards');
                }
            }
            $subject = trim((string)($_POST['subject'] ?? ''));
            $bodyIn = str_replace("\r", '', (string)($_POST['body'] ?? ''));
            if ($subject === '' || trim($bodyIn) === '') {
                flash_set('err', 'A message keeps a subject and a body.');
                admin_redirect('community.php?tab=boards&area=' . $areaId . ($rid > 0 ? '&msg=' . $rid : ''));
            }
            $lines = [];
            foreach (explode("\n", $bodyIn) as $ln) {
                foreach (msg_wrap79(rtrim($ln)) as $w) {
                    $lines[] = $w;
                }
            }
            $res = \Atdt\Messages::post(
                $db,
                atdt_settings(),
                $areaId,
                $me,
                'All',
                null,
                $subject,
                $lines,
                $replyTo
            );
            $db->q('UPDATE users SET posts = posts + 1 WHERE id = ?', [(int)$me['id']]);
            admin_log(($replyTo !== null
                ? 'Replied to #' . (int)$replyTo['num'] . ' with #' . (int)$res['num']
                : 'Posted #' . (int)$res['num']) . ' in ' . (string)$area['name']);
            flash_set('ok', 'Filed as message #' . (int)$res['num'] . ' in ' . (string)$area['name'] . '.');
            admin_redirect('community.php?tab=boards&area=' . $areaId . '&msg=' . (int)$res['id']);

        case 'bull_save':
            $title = trim((string)($_POST['title'] ?? ''));
            $body = str_replace("\r", '', trim((string)($_POST['body'] ?? '')));
            $art = trim((string)($_POST['art'] ?? ''));
            if (!preg_match('/^[a-z0-9_-]{0,32}$/i', $art)) {
                $art = '';
            }
            if ($title === '' || mb_strlen($title) > 60 || ($body === '' && $art === '')) {
                flash_set('err', 'A bulletin needs a title (≤ 60 chars) and a body or an ANSI art slot.');
                back_tab('bulletins');
            }
            $fields = [
                'title' => $title,
                'body' => $body,
                'acs' => trim((string)($_POST['acs'] ?? '')),
                'sort' => (int)($_POST['sort'] ?? 0),
                'updated_at' => now_utc(),
                'art' => $art,
            ];
            if ($id > 0 && $db->one('SELECT id FROM bulletins WHERE id = ?', [$id]) !== null) {
                $db->q(
                    'UPDATE bulletins SET title = ?, body = ?, acs = ?, sort = ?, updated_at = ?, art = ? WHERE id = ?',
                    [...array_values($fields), $id]
                );
                admin_log("Edited bulletin \"$title\"");
                flash_set('ok', 'Bulletin saved. Callers see it at their next logon.');
            } else {
                $db->insert('bulletins', $fields);
                admin_log("Posted bulletin \"$title\"");
                flash_set('ok', 'Bulletin posted. Fresh ink shows at every next logon.');
            }
            back_tab('bulletins');

        case 'bull_delete':
            $b = $db->one('SELECT * FROM bulletins WHERE id = ?', [$id]);
            if ($b !== null) {
                $db->q('DELETE FROM bulletins WHERE id = ?', [$id]);
                admin_log("Deleted bulletin \"{$b['title']}\"");
                flash_set('ok', 'Bulletin gone.');
            }
            back_tab('bulletins');

        case 'one_delete':
            $db->q('DELETE FROM oneliners WHERE id = ?', [$id]);
            admin_log('Deleted a oneliner');
            flash_set('ok', 'Scraped off the wall.');
            back_tab('oneliners');

        case 'rumor_delete':
            $db->q('DELETE FROM rumors WHERE id = ?', [$id]);
            admin_log('Deleted a rumor');
            flash_set('ok', 'That rumor dies here.');
            back_tab('rumors');

        case 'automsg_save':
            // Same shape the caller-side wall enforces: 8 rows, 70 columns.
            $settings = atdt_settings();
            $lines = array_slice(array_map(
                fn($l) => mb_strimwidth(\Atdt\Chat::clean((string)$l, 70), 0, 70),
                explode("\n", str_replace("\r", '', (string)($_POST['body'] ?? '')))
            ), 0, 8);
            $body = trim(implode("\n", $lines));
            $settings->set('automsg_body', $body);
            if ($body === '') {
                $settings->set('automsg_by', '');
                $settings->set('automsg_at', '');
                admin_log('Cleared the auto-message wall');
                flash_set('ok', 'The wall is blank again.');
            } else {
                $settings->set('automsg_by', (string)(admin_user()['handle'] ?? 'SysOp'));
                $settings->set('automsg_at', now_utc());
                admin_log('Repainted the auto-message wall');
                flash_set('ok', 'The wall is repainted. Every caller sees it at logon.');
            }
            back_tab('automsg');

        case 'vote_add':
            $question = trim((string)($_POST['question'] ?? ''));
            $opts = array_values(array_filter(array_map(
                fn($l) => mb_strimwidth(trim((string)$l), 0, 50),
                explode("\n", (string)($_POST['options'] ?? ''))
            ), fn($l) => $l !== ''));
            if ($question === '' || mb_strlen($question) > 100 || count($opts) < 2) {
                flash_set('err', 'A question needs text and at least two options (one per line).');
                back_tab('voting');
            }
            $qId = $db->insert('voting_q', [
                'question' => $question,
                'author' => (string)(admin_user()['handle'] ?? 'SysOp'),
                'acs' => trim((string)($_POST['acs'] ?? '')),
                'created_at' => now_utc(),
                'open' => 1,
            ]);
            foreach (array_slice($opts, 0, 10) as $label) {
                $db->insert('voting_opt', ['q_id' => $qId, 'label' => $label, 'votes' => 0]);
            }
            admin_log("Opened voting question \"$question\"");
            flash_set('ok', 'The booth has a new question.');
            back_tab('voting');

        case 'vote_toggle':
            $q = $db->one('SELECT * FROM voting_q WHERE id = ?', [$id]);
            if ($q !== null) {
                $db->q('UPDATE voting_q SET open = ? WHERE id = ?', [(int)$q['open'] === 1 ? 0 : 1, $id]);
                admin_log(((int)$q['open'] === 1 ? 'Closed' : 'Reopened') . " voting question \"{$q['question']}\"");
            }
            back_tab('voting');

        case 'vote_delete':
            $q = $db->one('SELECT * FROM voting_q WHERE id = ?', [$id]);
            if ($q !== null) {
                $db->q('DELETE FROM voting_cast WHERE q_id = ?', [$id]);
                $db->q('DELETE FROM voting_opt WHERE q_id = ?', [$id]);
                $db->q('DELETE FROM voting_q WHERE id = ?', [$id]);
                admin_log("Deleted voting question \"{$q['question']}\"");
                flash_set('ok', 'Question, options, and ballots removed.');
            }
            back_tab('voting');

        case 'bbs_save':
            $entry = $db->one('SELECT * FROM bbslist WHERE id = ?', [$id]);
            $name = mb_strimwidth(trim((string)($_POST['name'] ?? '')), 0, 40);
            $host = trim((string)($_POST['host'] ?? ''));
            if ($entry === null || $name === '' || !\Atdt\Screens\BbsListScreen::validHost($host)) {
                flash_set('err', 'Name and a valid address (https://…, telnet://…, or host:port) required.');
                back_tab('bbslist');
            }
            $db->q(
                'UPDATE bbslist SET name = ?, host = ?, sysop = ?, notes = ? WHERE id = ?',
                [
                    $name,
                    $host,
                    mb_strimwidth(trim((string)($_POST['sysop'] ?? '')), 0, 20),
                    mb_strimwidth(trim((string)($_POST['notes'] ?? '')), 0, 100),
                    $id,
                ]
            );
            admin_log("Edited BBS-list entry \"$name\"");
            flash_set('ok', 'Directory entry saved.');
            back_tab('bbslist');

        case 'bbs_delete':
            $entry = $db->one('SELECT * FROM bbslist WHERE id = ?', [$id]);
            if ($entry !== null) {
                $db->q('DELETE FROM bbslist WHERE id = ?', [$id]);
                admin_log("Removed BBS-list entry \"{$entry['name']}\"");
                flash_set('ok', 'Entry removed from the directory.');
            }
            back_tab('bbslist');
    }
    back_tab($tab);
}

admin_page_start('Community', 'community');

echo '<p>';
foreach (TABS as $key => $label) {
    printf(
        '<a class="btn small%s" href="community.php?tab=%s">%s</a> ',
        $key === $tab ? '' : ' secondary',
        $key,
        h($label)
    );
}
echo '</p>';

if ($tab === 'boards'):
    $areaId = (int)($_GET['area'] ?? 0);
    $msgId = (int)($_GET['msg'] ?? 0);
    $areas = $db->all("SELECT * FROM message_areas WHERE kind <> 'mail' ORDER BY category_id, sort, id");
    $areasById = [];
    foreach ($areas as $a) {
        $areasById[(int)$a['id']] = $a;
    }
    if ($msgId > 0 && ($viewMsg = board_msg($db, $msgId)) !== null):
        // One message: read it whole, fix what needs fixing.
        $tzName = (string)(atdt_settings()->get('timezone', 'UTC') ?? 'UTC');
        try {
            $local = (new DateTimeImmutable((string)$viewMsg['posted_at']))
                ->setTimezone(new DateTimeZone($tzName))->format('Y-m-d\TH:i');
        } catch (\Throwable $e) {
            $local = '';
        }
        $replyTo = (int)($viewMsg['reply_to_id'] ?? 0) > 0
            ? $db->one('SELECT num, subject FROM messages WHERE id = ?', [(int)$viewMsg['reply_to_id']])
            : null;
?>
<div class="cols">
<div>
  <div class="card">
    <h2>Message #<?= (int)$viewMsg['num'] ?> in <?= h((string)$viewMsg['area_name']) ?></h2>
    <p class="hint">From <strong><?= h((string)$viewMsg['from_name']) ?></strong>
      to <?= h((string)$viewMsg['to_name']) ?>
      <?= $replyTo !== null ? '· a reply to #' . (int)$replyTo['num'] . ' "' . h((string)$replyTo['subject']) . '"' : '' ?>
      - the author and the reply chain are facts of the record and cannot be edited.</p>
    <form method="post">
      <?= csrf_field() ?>
      <input type="hidden" name="action" value="msg_save"><input type="hidden" name="id" value="<?= (int)$viewMsg['id'] ?>">
      <label>Subject <input type="text" name="subject" maxlength="72" size="60" value="<?= h((string)$viewMsg['subject']) ?>"></label>
      <label>Posted <span class="hint">(board time, <?= h($tzName) ?>)</span>
        <input type="datetime-local" name="posted_at" value="<?= h($local) ?>"></label>
      <label>Body <span class="hint">(pipe color codes show as typed)</span>
        <textarea name="body" rows="16" cols="80"><?= h((string)$viewMsg['body']) ?></textarea></label>
      <button class="btn">Save message</button>
      <a class="btn secondary" href="community.php?tab=boards&amp;area=<?= (int)$viewMsg['area_id'] ?>">Back to the board</a>
    </form>
    <form method="post" class="inline" style="margin-top:.6rem" onsubmit="return confirm('Take message #<?= (int)$viewMsg['num'] ?> off the board?');">
      <?= csrf_field() ?>
      <input type="hidden" name="action" value="msg_del"><input type="hidden" name="id" value="<?= (int)$viewMsg['id'] ?>">
      <button class="btn small danger">Delete this message</button>
    </form>
  </div>

  <div class="card">
    <?php
        $reSubj = (string)$viewMsg['subject'];
        if (!str_starts_with(strtolower($reSubj), 're:')) {
            $reSubj = 'Re: ' . $reSubj;
        }
        $reSubj = mb_strimwidth($reSubj, 0, 70);
    ?>
    <h2>Reply as <?= h((string)$me['handle']) ?></h2>
    <p class="hint">Threads under message #<?= (int)$viewMsg['num'] ?>, numbered like any caller
      reply. Long lines wrap at the board's 79 columns on save.</p>
    <form method="post">
      <?= csrf_field() ?>
      <input type="hidden" name="action" value="msg_post">
      <input type="hidden" name="area" value="<?= (int)$viewMsg['area_id'] ?>">
      <input type="hidden" name="reply_to" value="<?= (int)$viewMsg['id'] ?>">
      <label>Subject <input type="text" name="subject" maxlength="70" size="60" value="<?= h($reSubj) ?>"></label>
      <label>Body
        <textarea name="body" rows="8" cols="80"></textarea></label>
      <button class="btn">Post reply</button>
    </form>
  </div>
</div>
</div>
<?php elseif ($areaId > 0 && isset($areasById[$areaId])):
        $mp = max(1, (int)($_GET['p'] ?? 1));
        $per = 25;
        // The caller's view, exactly (1.6.0): the same thread tree, roots
        // in date order - newest first by default - replies nested beneath
        // their parent. A fixed timestamp re-files a thread here too.
        $msort = (string)($_GET['sort'] ?? 'new') === 'old' ? 'old' : 'new';
        $all = \Atdt\Messages::threadList($db, $areaId, $msort);
        $more = count($all) > $mp * $per;
        $msgs = array_slice($all, ($mp - 1) * $per, $per);
        $qsBase = 'community.php?tab=boards&amp;area=' . $areaId . '&amp;sort=' . $msort;
?>
<div class="cols">
<div>
  <div class="card">
    <h2><?= h((string)$areasById[$areaId]['name']) ?> <span class="hint">(<?= h((string)$areasById[$areaId]['tag']) ?>)</span>
      <a class="btn small secondary" style="float:right" href="community.php?tab=boards&amp;area=<?= $areaId ?>&amp;sort=<?= $msort === 'old' ? 'new' : 'old' ?>">
        <?= $msort === 'old' ? 'Oldest first - flip' : 'Newest first - flip' ?></a></h2>
    <table class="list">
      <tr><th>#</th><th>From</th><th>To</th><th>Subject</th><th>Posted</th><th></th></tr>
      <?php if ($msgs === []): ?><tr><td colspan="6" class="hint">This board is empty.</td></tr><?php endif; ?>
      <?php foreach ($msgs as $m):
            $depth = (int)($m['depth'] ?? 0);
            $tree = $depth === 0 ? '' : str_repeat('&nbsp;&nbsp;', $depth - 1)
                . (($m['last'] ?? false) ? '└─' : '├─');
      ?>
      <tr>
        <td><?= (int)$m['num'] ?></td>
        <td><?= h((string)$m['from_name']) ?></td>
        <td><?= h((string)$m['to_name']) ?></td>
        <td><?= $tree !== '' ? '<span class="hint" style="font-family:monospace">' . $tree . '</span>' : '' ?><a href="community.php?tab=boards&amp;area=<?= $areaId ?>&amp;msg=<?= (int)$m['id'] ?>"><?= h((string)$m['subject']) ?></a></td>
        <td class="nowrap"><?= h(admin_stamp((string)$m['posted_at'], 'd-M-y H:i')) ?></td>
        <td class="nowrap">
          <a class="btn small secondary" href="community.php?tab=boards&amp;area=<?= $areaId ?>&amp;msg=<?= (int)$m['id'] ?>">Open</a>
        </td>
      </tr>
      <?php endforeach; ?>
    </table>
    <p>
      <?php if ($mp > 1): ?><a class="btn small secondary" href="<?= $qsBase ?>&amp;p=<?= $mp - 1 ?>">&laquo; <?= $msort === 'old' ? 'Earlier' : 'Newer' ?></a><?php endif; ?>
      <?php if ($more): ?><a class="btn small secondary" href="<?= $qsBase ?>&amp;p=<?= $mp + 1 ?>"><?= $msort === 'old' ? 'Later' : 'Older' ?> &raquo;</a><?php endif; ?>
      <a class="btn small secondary" href="community.php?tab=boards">All boards</a>
    </p>
  </div>

  <div class="card">
    <h2>Post to this board</h2>
    <p class="hint">Goes out as <strong><?= h((string)$me['handle']) ?></strong>, threaded and numbered
      like any caller post. Long lines wrap at the board's 79 columns on save.</p>
    <form method="post">
      <?= csrf_field() ?>
      <input type="hidden" name="action" value="msg_post">
      <input type="hidden" name="area" value="<?= $areaId ?>">
      <input type="hidden" name="reply_to" value="0">
      <label>Subject <input type="text" name="subject" maxlength="70" size="60"></label>
      <label>Body
        <textarea name="body" rows="8" cols="80"></textarea></label>
      <button class="btn">Post message</button>
    </form>
  </div>
</div>
</div>
<?php else:
        $cats = $db->all('SELECT * FROM message_categories ORDER BY sort, name');
        $counts = [];
        foreach ($db->all('SELECT area_id, COUNT(*) AS c FROM messages WHERE deleted = 0 GROUP BY area_id') as $r) {
            $counts[(int)$r['area_id']] = (int)$r['c'];
        }
        $byCat = [];
        foreach ($areas as $a) {
            $byCat[(int)$a['category_id']][] = $a;
        }
        $catNames = [0 => 'Uncategorized'];
        foreach ($cats as $cRow) {
            $catNames[(int)$cRow['id']] = (string)$cRow['name'];
        }
?>
<div class="cols">
<div>
  <?php foreach ($catNames as $cid => $cname): if (!isset($byCat[$cid])) { continue; } ?>
  <div class="card">
    <h2><?= h($cname) ?></h2>
    <table class="list">
      <tr><th>Tag</th><th>Board</th><th>Messages</th><th></th></tr>
      <?php foreach ($byCat[$cid] as $a): ?>
      <tr>
        <td><code><?= h((string)$a['tag']) ?></code></td>
        <td><a href="community.php?tab=boards&amp;area=<?= (int)$a['id'] ?>"><?= h((string)$a['name']) ?></a>
          <span class="hint"><?= h((string)$a['descr']) ?></span></td>
        <td><?= $counts[(int)$a['id']] ?? 0 ?></td>
        <td><a class="btn small secondary" href="community.php?tab=boards&amp;area=<?= (int)$a['id'] ?>">Browse</a></td>
      </tr>
      <?php endforeach; ?>
    </table>
  </div>
  <?php endforeach; ?>
  <p class="hint">The boards as callers see them, category by category.
  Open any message to fix its subject, body, or timestamp, or to take it
  off the board - who wrote it and what it replies to are facts of the
  record and never change. Private mail is not curatable here or anywhere:
  the console runs the boards, it does not read anyone's letters.
  Boards themselves (tags, order, access) live under
  <a href="areas.php">Areas</a>.</p>
</div>
</div>
<?php endif; ?>

<?php elseif ($tab === 'bulletins'):
    // Newest first (1.5.5), matching what callers see under [B]ulletins.
    $rows = $db->all('SELECT * FROM bulletins ORDER BY sort, id DESC');
    $edit = isset($_GET['edit']) ? $db->one('SELECT * FROM bulletins WHERE id = ?', [(int)$_GET['edit']]) : null;
    // Bulletin art comes from the board-wide gallery (1.1.0).
    $artSlots = \Atdt\Art::pieces();
?>
<div class="cols">
<div>
  <div class="card">
    <table class="list">
      <tr><th>Title</th><th>ACS</th><th>Art</th><th>Updated</th><th></th></tr>
      <?php if ($rows === []): ?><tr><td colspan="5" class="hint">No bulletins yet.</td></tr><?php endif; ?>
      <?php foreach ($rows as $b): ?>
      <tr>
        <td><?= h((string)$b['title']) ?></td>
        <td><code><?= h((string)$b['acs']) ?: '<span class="hint">everyone</span>' ?></code></td>
        <td class="hint"><?= trim((string)($b['art'] ?? '')) !== '' ? '<code>' . h((string)$b['art']) . '</code>' : '' ?></td>
        <td class="hint nowrap"><?= h(admin_stamp((string)$b['updated_at'], 'Y-m-d')) ?></td>
        <td class="nowrap">
          <a href="community.php?tab=bulletins&amp;edit=<?= (int)$b['id'] ?>">edit</a>
          <form method="post" class="inline" onsubmit="return confirm('Delete this bulletin?');">
            <?= csrf_field() ?>
            <input type="hidden" name="action" value="bull_delete"><input type="hidden" name="id" value="<?= (int)$b['id'] ?>">
            <button class="btn small danger">×</button></form>
        </td>
      </tr>
      <?php endforeach; ?>
    </table>
  </div>
  <p class="hint">Anything updated since a
  caller's last visit is shown to them automatically at logon.</p>
</div>
<div>
  <div class="card drawer">
    <h2><?= $edit === null ? 'Post a bulletin' : 'Edit: ' . h((string)$edit['title']) ?></h2>
    <form method="post">
      <?= csrf_field() ?>
      <input type="hidden" name="action" value="bull_save">
      <?php if ($edit !== null): ?><input type="hidden" name="id" value="<?= (int)$edit['id'] ?>"><?php endif; ?>
      <label>Title <input type="text" name="title" maxlength="60" value="<?= h((string)($edit['title'] ?? '')) ?>"></label>
      <label>Body <span class="hint">(pipe colors like |14 render on the board; long lines word-wrap at 80 columns)</span>
        <textarea name="body" rows="10" style="width:100%"><?= h((string)($edit['body'] ?? '')) ?></textarea></label>
      <label>ANSI art <span class="hint">(draw one in the <a href="editor.php">editor</a>; when set, callers see the art full-screen instead of the body)</span>
        <select name="art">
          <option value="">none (show the text body)</option>
          <?php foreach ($artSlots as $slot): ?>
          <option value="<?= h($slot) ?>"<?= (string)($edit['art'] ?? '') === $slot ? ' selected' : '' ?>><?= h($slot) ?></option>
          <?php endforeach; ?>
        </select></label>
      <div class="row">
        <label>ACS <span class="hint">(blank = everyone)</span>
          <input type="text" name="acs" value="<?= h((string)($edit['acs'] ?? '')) ?>"></label>
        <label>Sort <input type="text" name="sort" size="6" value="<?= (int)($edit['sort'] ?? 0) ?>"></label>
      </div>
      <button class="btn"><?= $edit === null ? 'Post bulletin' : 'Save bulletin' ?></button>
      <?php if ($edit !== null): ?><a class="btn secondary" href="community.php?tab=bulletins">Close</a><?php endif; ?>
    </form>
  </div>
</div>
</div>

<?php elseif ($tab === 'oneliners'):
    $rows = $db->all('SELECT * FROM oneliners ORDER BY id DESC LIMIT 100');
?>
<div class="card">
  <table class="list">
    <tr><th class="nowrap">When</th><th>Caller</th><th>Line</th><th></th></tr>
    <?php if ($rows === []): ?><tr><td colspan="4" class="hint">The wall is bare.</td></tr><?php endif; ?>
    <?php foreach ($rows as $r): ?>
    <tr>
      <td class="hint nowrap"><?= h(admin_stamp((string)$r['at'], 'Y-m-d')) ?></td>
      <td><?= h((string)$r['handle']) ?></td>
      <td><?= h((string)$r['body']) ?></td>
      <td>
        <form method="post" class="inline"><?= csrf_field() ?>
          <input type="hidden" name="action" value="one_delete"><input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
          <button class="btn small danger">×</button></form>
      </td>
    </tr>
    <?php endforeach; ?>
  </table>
</div>
<p class="hint">Callers add one line per day; the caller screen shows the latest 15.</p>

<?php elseif ($tab === 'rumors'):
    $rows = $db->all('SELECT * FROM rumors ORDER BY id DESC LIMIT 100');
?>
<div class="card">
  <table class="list">
    <tr><th class="nowrap">When</th><th>Started by</th><th>Rumor</th><th></th></tr>
    <?php if ($rows === []): ?><tr><td colspan="4" class="hint">Nobody has heard anything.</td></tr><?php endif; ?>
    <?php foreach ($rows as $r): ?>
    <tr>
      <td class="hint nowrap"><?= h(admin_stamp((string)$r['at'], 'Y-m-d')) ?></td>
      <td><?= h((string)$r['handle']) ?></td>
      <td><?= h((string)$r['body']) ?></td>
      <td>
        <form method="post" class="inline"><?= csrf_field() ?>
          <input type="hidden" name="action" value="rumor_delete"><input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
          <button class="btn small danger">×</button></form>
      </td>
    </tr>
    <?php endforeach; ?>
  </table>
</div>
<p class="hint">One random rumor greets every caller at logon. Prune the duds.</p>

<?php elseif ($tab === 'automsg'):
    $amSettings = atdt_settings();
    $amBody = (string)($amSettings->get('automsg_body', '') ?? '');
    $amBy = trim((string)($amSettings->get('automsg_by', '') ?? ''));
    $amAt = trim((string)($amSettings->get('automsg_at', '') ?? ''));
?>
<div class="cols">
<div>
  <div class="card">
    <h2>The wall right now</h2>
    <?php if (trim($amBody) === ''): ?>
    <p class="hint">The wall is blank. Somebody paint it.</p>
    <?php else: ?>
    <pre class="cronline"><?= h($amBody) ?></pre>
    <p class="hint">Painted by <strong><?= h($amBy === '' ? '?' : $amBy) ?></strong><?=
        $amAt !== '' ? ' on ' . h(admin_stamp($amAt)) : '' ?>.</p>
    <?php endif; ?>
  </div>
  <p class="hint">One board-wide message, shown to every caller at logon.
  Callers can repaint it from the Community menu (1.5.0); the menu item's ACS
  decides who. Whatever was there before is painted over.</p>
</div>
<div>
  <div class="card drawer">
    <h2>Repaint the wall</h2>
    <form method="post">
      <?= csrf_field() ?>
      <input type="hidden" name="action" value="automsg_save">
      <label>Message <span class="hint">(up to 8 lines of 70 columns; pipe colors like <code>|14</code> render if caller colors are on. Save it empty to blank the wall)</span>
        <textarea name="body" rows="8" cols="70" style="width:100%;font-family:monospace"><?= h($amBody) ?></textarea></label>
      <button class="btn">Save the wall</button>
    </form>
  </div>
</div>
</div>

<?php elseif ($tab === 'voting'):
    $questions = $db->all('SELECT * FROM voting_q ORDER BY id');
?>
<div class="cols">
<div>
  <?php foreach ($questions as $q):
      $opts = $db->all('SELECT * FROM voting_opt WHERE q_id = ? ORDER BY id', [(int)$q['id']]);
      $total = max(1, array_sum(array_map(fn($o) => (int)$o['votes'], $opts)));
  ?>
  <div class="card">
    <h2><?= h((string)$q['question']) ?>
      <span class="hint"><?= (int)$q['open'] === 1 ? 'open' : 'closed' ?>
        · <?= array_sum(array_map(fn($o) => (int)$o['votes'], $opts)) ?> ballots</span></h2>
    <table class="list">
      <?php foreach ($opts as $o): $v = (int)$o['votes']; ?>
      <tr>
        <td><?= h((string)$o['label']) ?></td>
        <td class="nowrap"><?= $v ?> (<?= (int)round($v / $total * 100) ?>%)</td>
      </tr>
      <?php endforeach; ?>
    </table>
    <form method="post" class="inline"><?= csrf_field() ?>
      <input type="hidden" name="action" value="vote_toggle"><input type="hidden" name="id" value="<?= (int)$q['id'] ?>">
      <button class="btn small secondary"><?= (int)$q['open'] === 1 ? 'Close question' : 'Reopen' ?></button></form>
    <form method="post" class="inline" onsubmit="return confirm('Delete this question and all its ballots?');">
      <?= csrf_field() ?>
      <input type="hidden" name="action" value="vote_delete"><input type="hidden" name="id" value="<?= (int)$q['id'] ?>">
      <button class="btn small danger">Delete</button></form>
  </div>
  <?php endforeach; ?>
</div>
<div>
  <div class="card drawer">
    <h2>New question</h2>
    <form method="post">
      <?= csrf_field() ?>
      <input type="hidden" name="action" value="vote_add">
      <label>Question <input type="text" name="question" maxlength="100"></label>
      <label>Options <span class="hint">(one per line, 2-10)</span>
        <textarea name="options" rows="6" style="width:100%"></textarea></label>
      <label>ACS <span class="hint">(blank = everyone may vote)</span>
        <input type="text" name="acs"></label>
      <button class="btn">Open the question</button>
    </form>
    <p class="hint">Callers vote once each; results show only after they cast.
    Questions cannot be edited once ballots exist. Close them and open a
    fresh one instead.</p>
  </div>
</div>
</div>

<?php else:
    $rows = $db->all('SELECT * FROM bbslist ORDER BY id');
    $edit = isset($_GET['edit']) ? $db->one('SELECT * FROM bbslist WHERE id = ?', [(int)$_GET['edit']]) : null;
?>
<div class="cols">
<div>
  <div class="card">
    <table class="list">
      <tr><th>Board</th><th>Address</th><th>SysOp</th><th>Added by</th><th></th></tr>
      <?php if ($rows === []): ?><tr><td colspan="5" class="hint">The directory is empty.</td></tr><?php endif; ?>
      <?php foreach ($rows as $b): ?>
      <tr>
        <td><?= h((string)$b['name']) ?>
          <?php if (trim((string)$b['notes']) !== ''): ?><div class="hint"><?= h((string)$b['notes']) ?></div><?php endif; ?></td>
        <td><code><?= h((string)$b['host']) ?></code></td>
        <td><?= h((string)$b['sysop']) ?></td>
        <td class="hint"><?= h((string)$b['added_by']) ?></td>
        <td class="nowrap">
          <a href="community.php?tab=bbslist&amp;edit=<?= (int)$b['id'] ?>">edit</a>
          <form method="post" class="inline" onsubmit="return confirm('Remove <?= h((string)$b['name']) ?> from the directory?');">
            <?= csrf_field() ?>
            <input type="hidden" name="action" value="bbs_delete"><input type="hidden" name="id" value="<?= (int)$b['id'] ?>">
            <button class="btn small danger">×</button></form>
        </td>
      </tr>
      <?php endforeach; ?>
    </table>
  </div>
</div>
<div>
  <?php if ($edit !== null): ?>
  <div class="card drawer">
    <h2>Edit: <?= h((string)$edit['name']) ?></h2>
    <form method="post">
      <?= csrf_field() ?>
      <input type="hidden" name="action" value="bbs_save">
      <input type="hidden" name="id" value="<?= (int)$edit['id'] ?>">
      <label>Board name <input type="text" name="name" maxlength="40" value="<?= h((string)$edit['name']) ?>"></label>
      <label>Address <span class="hint">(https://…, telnet://…, or host:port)</span>
        <input type="text" name="host" value="<?= h((string)$edit['host']) ?>"></label>
      <label>SysOp <input type="text" name="sysop" maxlength="20" value="<?= h((string)$edit['sysop']) ?>"></label>
      <label>Notes <input type="text" name="notes" maxlength="100" value="<?= h((string)$edit['notes']) ?>"></label>
      <button class="btn">Save entry</button>
      <a class="btn secondary" href="community.php?tab=bbslist">Close</a>
    </form>
  </div>
  <?php else: ?>
  <p class="hint">Validated callers add entries from the caller side; edit or
  prune them here.</p>
  <?php endif; ?>
</div>
</div>
<?php endif; ?>

<?php admin_page_end(); ?>
