<?php
/**
 * gate_admin.php — CLI admin tool for gate.php allowlist management
 *
 * Commands:
 *   php gate_admin.php list                     List all allowlisted emails
 *   php gate_admin.php list-banned              List only banned emails
 *   php gate_admin.php add      <email>         Add email as valid
 *   php gate_admin.php invalidate <email>       Ban email (denies access, kills session)
 *   php gate_admin.php restore  <email>         Restore a banned email to valid
 *   php gate_admin.php delete   <email>         Remove email from allowlist entirely
 *   php gate_admin.php import   <file.csv>      Import emails from CSV (added as valid)
 *   php gate_admin.php export   <file.csv>      Export full allowlist to CSV
 *
 * CSV format (import):  one email per line, or  email,status  with header row.
 * CSV format (export):  email,status,added_at,last_login,login_ip,login_page
 *
 * Requirements: PHP 7.4+, pdo_sqlite.
 * Drop alongside gate.php and gate_emails.sqlite.
 */

// ── Config ────────────────────────────────────────────────────────────────────
define('GATE_DB_PATH', __DIR__ . '/gate_emails.sqlite');
// ─────────────────────────────────────────────────────────────────────────────

if (php_sapi_name() !== 'cli') {
    http_response_code(403);
    exit("This script must be run from the command line.\n");
}

// ═════════════════════════════════════════════════════════════════════════════
// DATABASE
// ═════════════════════════════════════════════════════════════════════════════

function db(): PDO {
    static $pdo = null;
    if ($pdo !== null) return $pdo;

    if (!file_exists(GATE_DB_PATH)) {
        // Create fresh DB if it doesn't exist yet (first-time setup)
        out("Database not found — creating new one at: " . GATE_DB_PATH);
    }

    $pdo = new PDO('sqlite:' . GATE_DB_PATH);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->exec("PRAGMA journal_mode=WAL");

    // Create or migrate to the current schema
    $pdo->exec("CREATE TABLE IF NOT EXISTS gate_emails (
        id            INTEGER  PRIMARY KEY AUTOINCREMENT,
        email         TEXT     NOT NULL UNIQUE COLLATE NOCASE,
        status        TEXT     NOT NULL DEFAULT 'valid'
                               CHECK(status IN ('valid','banned')),
        session_token TEXT     DEFAULT NULL,
        added_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
        last_login    DATETIME DEFAULT NULL,
        login_ip      TEXT     DEFAULT NULL,
        login_page    TEXT     DEFAULT NULL
    )");

    // ── Migrate from the old multi-row schema (v1/v2) if present ─────────────
    $cols = array_column(
        $pdo->query("PRAGMA table_info(gate_emails)")->fetchAll(PDO::FETCH_ASSOC),
        'name'
    );

    $needs_migration = in_array('ip', $cols, true) && !in_array('status', $cols, true);

    if ($needs_migration) {
        out("Detected old schema — migrating to allowlist format...");

        // Pull distinct emails, preserving ban state
        $rows = $pdo->query(
            "SELECT email,
                    MAX(CASE WHEN invalidated = 1 THEN 1 ELSE 0 END) AS ever_banned,
                    MIN(visited) AS first_seen
               FROM gate_emails
              GROUP BY email COLLATE NOCASE"
        )->fetchAll(PDO::FETCH_ASSOC);

        // Rename old table, create new one, migrate data
        $pdo->exec("ALTER TABLE gate_emails RENAME TO gate_emails_legacy");
        $pdo->exec("CREATE TABLE gate_emails (
            id            INTEGER  PRIMARY KEY AUTOINCREMENT,
            email         TEXT     NOT NULL UNIQUE COLLATE NOCASE,
            status        TEXT     NOT NULL DEFAULT 'valid'
                                   CHECK(status IN ('valid','banned')),
            session_token TEXT     DEFAULT NULL,
            added_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
            last_login    DATETIME DEFAULT NULL,
            login_ip      TEXT     DEFAULT NULL,
            login_page    TEXT     DEFAULT NULL
        )");

        $ins = $pdo->prepare(
            "INSERT OR IGNORE INTO gate_emails (email, status, added_at)
             VALUES (:e, :s, :a)"
        );
        foreach ($rows as $r) {
            $ins->execute([
                ':e' => strtolower(trim($r['email'])),
                ':s' => $r['ever_banned'] ? 'banned' : 'valid',
                ':a' => $r['first_seen'],
            ]);
        }

        $migrated = count($rows);
        out("Migration complete — $migrated email(s) moved. Legacy table kept as 'gate_emails_legacy'.");
    }

    return $pdo;
}

/** Fetch the single row for an email or null. */
function db_get(string $email): ?array {
    $stmt = db()->prepare(
        "SELECT * FROM gate_emails WHERE email = :e COLLATE NOCASE LIMIT 1"
    );
    $stmt->execute([':e' => strtolower(trim($email))]);
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    return $row ?: null;
}

// ═════════════════════════════════════════════════════════════════════════════
// OUTPUT HELPERS
// ═════════════════════════════════════════════════════════════════════════════

const COL_WIDTH = 110;

function out(string $msg = ''): void   { echo $msg . PHP_EOL; }
function fail(string $msg): never      { fwrite(STDERR, "ERROR: $msg\n"); exit(1); }
function hr(): void                    { out(str_repeat('─', COL_WIDTH)); }

function prompt_confirm(string $question): bool {
    fwrite(STDOUT, $question . ' [y/N] ');
    return strtolower(trim(fgets(STDIN))) === 'y';
}

function normalize_email(string $raw): string {
    $email = strtolower(trim($raw));
    if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
        fail("'$raw' is not a valid email address.");
    }
    return $email;
}

function status_label(string $status): string {
    return $status === 'banned' ? '🚫 banned' : '✓  valid';
}

function print_table(array $rows): void {
    if (empty($rows)) {
        out("(no records)");
        return;
    }
    hr();
    printf("%-4s  %-38s  %-10s  %-19s  %-19s  %-15s  %s\n",
        'ID', 'Email', 'Status', 'Added', 'Last Login', 'Login IP', 'Login Page');
    hr();
    foreach ($rows as $r) {
        printf("%-4s  %-38s  %-10s  %-19s  %-19s  %-15s  %s\n",
            $r['id'],
            $r['email'],
            status_label($r['status']),
            $r['added_at']    ?? '—',
            $r['last_login']  ?? 'never',
            $r['login_ip']    ?? '—',
            $r['login_page']  ?? '—'
        );
    }
    hr();
}

// ═════════════════════════════════════════════════════════════════════════════
// COMMANDS
// ═════════════════════════════════════════════════════════════════════════════

/** List all emails (or only banned ones). */
function cmd_list(bool $banned_only = false): void {
    $sql  = $banned_only
        ? "SELECT * FROM gate_emails WHERE status = 'banned' ORDER BY email"
        : "SELECT * FROM gate_emails ORDER BY email";
    $rows = db()->query($sql)->fetchAll(PDO::FETCH_ASSOC);
    $n    = count($rows);
    $label = $banned_only ? "Banned emails" : "All allowlisted emails";
    out("$label ($n record" . ($n !== 1 ? 's' : '') . "):");
    print_table($rows);
}

/** Add a new email as valid (no-op if already present). */
function cmd_add(string $email): void {
    $email = normalize_email($email);

    $existing = db_get($email);
    if ($existing !== null) {
        out("'$email' is already in the allowlist (status: {$existing['status']}).");
        out("Use 'restore' to re-enable a banned email, or 'invalidate' to ban it.");
        return;
    }

    $stmt = db()->prepare(
        "INSERT INTO gate_emails (email, status) VALUES (:e, 'valid')"
    );
    $stmt->execute([':e' => $email]);
    out("✓ Added '$email' as valid.");
}

/** Ban an email — kills its active session token, denies all future access. */
function cmd_invalidate(string $email): void {
    $email  = normalize_email($email);
    $record = db_get($email);

    if ($record === null) {
        fail("'$email' is not in the allowlist. Use 'add' first.");
    }
    if ($record['status'] === 'banned') {
        out("'$email' is already banned.");
        return;
    }

    db()->prepare(
        "UPDATE gate_emails
            SET status        = 'banned',
                session_token = NULL
          WHERE email = :e COLLATE NOCASE"
    )->execute([':e' => $email]);

    out("✓ Banned '$email'.");
    out("  Their session token has been cleared — the next page load will deny them.");
}

/** Restore a banned email to valid status. */
function cmd_restore(string $email): void {
    $email  = normalize_email($email);
    $record = db_get($email);

    if ($record === null) {
        fail("'$email' is not in the allowlist.");
    }
    if ($record['status'] === 'valid') {
        out("'$email' is already valid. Nothing to restore.");
        return;
    }

    db()->prepare(
        "UPDATE gate_emails SET status = 'valid' WHERE email = :e COLLATE NOCASE"
    )->execute([':e' => $email]);

    out("✓ Restored '$email' to valid status.");
    out("  They may now pass through the gate again.");
}

/** Permanently remove an email from the allowlist. */
function cmd_delete(string $email): void {
    $email  = normalize_email($email);
    $record = db_get($email);

    if ($record === null) {
        fail("'$email' is not in the allowlist.");
    }

    if (!prompt_confirm("Permanently remove '$email' from the allowlist. Confirm?")) {
        out("Aborted — nothing changed.");
        exit(0);
    }

    db()->prepare("DELETE FROM gate_emails WHERE email = :e COLLATE NOCASE")
        ->execute([':e' => $email]);

    out("✓ Deleted '$email' from the allowlist.");
}

// ─────────────────────────────────────────────────────────────────────────────
// CSV IMPORT
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Import emails from a CSV file.
 *
 * Accepted formats:
 *   A) Single-column — one email per line (with or without header "email"):
 *        user@example.com
 *
 *   B) Two-column — email + status (imported status respected):
 *        email,status
 *        user@example.com,valid
 *        bad@example.com,banned
 *
 * Rules:
 *   - New emails are inserted with status from the file (default: valid).
 *   - Existing emails: status is updated only if the CSV explicitly provides one
 *     AND it differs — otherwise the existing record is left untouched.
 *   - Blank lines and lines starting with # are skipped.
 *   - Invalid email addresses are reported and skipped.
 */
function cmd_import(string $path): void {
    if (!file_exists($path)) {
        fail("File not found: $path");
    }

    $handle = fopen($path, 'r');
    if (!$handle) fail("Cannot open: $path");

    $added   = 0;
    $updated = 0;
    $skipped = 0;
    $invalid = 0;
    $line_no = 0;

    // Peek at the first non-blank, non-comment line to detect format
    $has_status_col = false;
    $first_data     = null;
    $rewind_lines   = [];

    while (!feof($handle)) {
        $raw = fgets($handle);
        $line_no++;
        $trimmed = trim($raw);
        if ($trimmed === '' || str_starts_with($trimmed, '#')) {
            $rewind_lines[] = $raw;
            continue;
        }
        $cols = str_getcsv($trimmed);
        if (count($cols) >= 2) {
            $h0 = strtolower(trim($cols[0]));
            $h1 = strtolower(trim($cols[1]));
            // If it looks like a header row, note format and skip it
            if ($h0 === 'email' && in_array($h1, ['status', 'banned', 'valid'], true)) {
                $has_status_col = true;
                break;
            }
            // Data row with two cols — infer status column present
            if (filter_var(trim($cols[0]), FILTER_VALIDATE_EMAIL)
                && in_array($h1, ['valid', 'banned'], true)) {
                $has_status_col = true;
            }
        }
        $first_data = $raw;
        break;
    }

    // Restart from beginning
    rewind($handle);
    $line_no = 0;

    $db   = db();
    $ins  = $db->prepare(
        "INSERT INTO gate_emails (email, status) VALUES (:e, :s)
         ON CONFLICT(email) DO NOTHING"
    );
    $upd  = $db->prepare(
        "UPDATE gate_emails SET status = :s WHERE email = :e COLLATE NOCASE"
    );
    $chk  = $db->prepare(
        "SELECT status FROM gate_emails WHERE email = :e COLLATE NOCASE LIMIT 1"
    );

    $db->beginTransaction();

    try {
        while (!feof($handle)) {
            $raw = fgets($handle);
            if ($raw === false) break;
            $line_no++;
            $trimmed = trim($raw);

            // Skip blanks and comments
            if ($trimmed === '' || str_starts_with($trimmed, '#')) continue;

            $cols        = str_getcsv($trimmed);
            $raw_email   = trim($cols[0] ?? '');
            $raw_status  = strtolower(trim($cols[1] ?? ''));

            // Skip header row
            if (strtolower($raw_email) === 'email' && in_array($raw_status, ['status','valid','banned',''], true)) {
                continue;
            }

            // Validate email
            $email = strtolower($raw_email);
            if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
                out("  Line $line_no: skipping invalid address — '$raw_email'");
                $invalid++;
                continue;
            }

            // Determine intended status
            $status = in_array($raw_status, ['valid', 'banned'], true) ? $raw_status : 'valid';

            // Check existing record
            $chk->execute([':e' => $email]);
            $existing_status = $chk->fetchColumn();

            if ($existing_status === false) {
                // New email — insert
                $ins->execute([':e' => $email, ':s' => $status]);
                $added++;
            } elseif ($has_status_col && $raw_status !== '' && $existing_status !== $status) {
                // Existing email with explicit new status — update
                $upd->execute([':s' => $status, ':e' => $email]);
                $updated++;
            } else {
                $skipped++;
            }
        }

        $db->commit();
    } catch (Throwable $ex) {
        $db->rollBack();
        fail("Import failed at line $line_no: " . $ex->getMessage());
    }

    fclose($handle);

    out("✓ Import complete from: $path");
    out("  Added:   $added");
    out("  Updated: $updated");
    out("  Skipped (already present, no change): $skipped");
    if ($invalid > 0) {
        out("  Invalid (skipped): $invalid");
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// CSV EXPORT
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Export the full allowlist to a CSV file.
 *
 * Output columns: email, status, added_at, last_login, login_ip, login_page
 * The session_token column is intentionally excluded for security.
 */
function cmd_export(string $path): void {
    if (file_exists($path) && !prompt_confirm("'$path' already exists. Overwrite?")) {
        out("Aborted — nothing changed.");
        exit(0);
    }

    $handle = fopen($path, 'w');
    if (!$handle) fail("Cannot write to: $path");

    $rows = db()->query(
        "SELECT email, status, added_at, last_login, login_ip, login_page
           FROM gate_emails
          ORDER BY email"
    )->fetchAll(PDO::FETCH_ASSOC);

    // Header
    fputcsv($handle, ['email', 'status', 'added_at', 'last_login', 'login_ip', 'login_page']);

    foreach ($rows as $r) {
        fputcsv($handle, [
            $r['email'],
            $r['status'],
            $r['added_at']   ?? '',
            $r['last_login'] ?? '',
            $r['login_ip']   ?? '',
            $r['login_page'] ?? '',
        ]);
    }

    fclose($handle);

    $n = count($rows);
    out("✓ Exported $n email" . ($n !== 1 ? 's' : '') . " to: $path");
}

// ═════════════════════════════════════════════════════════════════════════════
// DISPATCH
// ═════════════════════════════════════════════════════════════════════════════

$argv = $_SERVER['argv'] ?? [];
$cmd  = $argv[1] ?? '';
$arg  = $argv[2] ?? '';

function usage(): void {
    out("gate_admin.php — manage the gate.php email allowlist");
    out("");
    out("Commands:");
    out("  php gate_admin.php list                    List all allowlisted emails");
    out("  php gate_admin.php list-banned             List only banned emails");
    out("  php gate_admin.php add      <email>        Add email to allowlist as valid");
    out("  php gate_admin.php invalidate <email>      Ban email (kills session, denies access)");
    out("  php gate_admin.php restore  <email>        Restore a banned email to valid");
    out("  php gate_admin.php delete   <email>        Remove email from allowlist entirely");
    out("  php gate_admin.php import   <file.csv>     Import emails from CSV (added as valid)");
    out("  php gate_admin.php export   <file.csv>     Export allowlist to CSV");
    out("");
    out("CSV import format (two supported variants):");
    out("  Simple:   one email per line");
    out("  Extended: email,status  (header row optional; status = valid|banned)");
    out("");
    out("CSV export columns: email, status, added_at, last_login, login_ip, login_page");
    out("");
    out("Ban lifecycle:");
    out("  add        → email joins allowlist as valid");
    out("  invalidate → status = banned, session_token cleared (denied on next request)");
    out("  restore    → status = valid (user can log in again)");
    out("  delete     → row removed entirely (user treated as unknown on next visit)");
}

match ($cmd) {
    'list'        => cmd_list(false),
    'list-banned' => cmd_list(true),
    'add'         => $arg ? cmd_add($arg)        : fail("Usage: php gate_admin.php add <email>"),
    'invalidate'  => $arg ? cmd_invalidate($arg)  : fail("Usage: php gate_admin.php invalidate <email>"),
    'restore'     => $arg ? cmd_restore($arg)     : fail("Usage: php gate_admin.php restore <email>"),
    'delete'      => $arg ? cmd_delete($arg)      : fail("Usage: php gate_admin.php delete <email>"),
    'import'      => $arg ? cmd_import($arg)      : fail("Usage: php gate_admin.php import <file.csv>"),
    'export'      => $arg ? cmd_export($arg)      : fail("Usage: php gate_admin.php export <file.csv>"),
    default       => usage(),
};
