<?php
/**
 * Regenerate inc/data/name-model.php — the character-trigram table the bot
 * shield uses to tell real names from random letters.
 *
 * The model is a bitmap of every 3-character sequence that occurs in a corpus
 * of known-real names plus an English dictionary. A name containing sequences
 * that appear nowhere in that corpus ("xnqfrt", "wvhpwcr") is machine-made.
 *
 * USAGE
 *   Export from the wpdeploy fleet database (sbitserver4), then build:
 *
 *     # Verified humans: registered AND came back to log in or bid.
 *     mysql -uwpdeploy -p<pass> wpdeploy -N -e \
 *       "SELECT DISTINCT first_name, last_name, email FROM user_tracking
 *         WHERE last_event_type IN ('login','bid') AND event_count >= 2
 *           AND first_name<>'' AND last_name<>''" > verified.tsv
 *
 *     # Everyone else: filtered by the email-match heuristic below.
 *     mysql -uwpdeploy -p<pass> wpdeploy -N -e \
 *       "SELECT first_name, last_name, email FROM people
 *         WHERE first_name<>'' AND last_name<>''" > people.tsv
 *
 *     php bin/build-name-model.php --verified verified.tsv \
 *         --filtered people.tsv --dict /usr/share/dict/words
 *
 * --verified rows are trusted outright (returning users are not bots).
 * --filtered rows are trusted only when the local part of the email contains
 * the first or last name (4+ characters); bots pair random names with
 * unrelated harvested addresses, so this keeps them out without a hand-curated
 * blocklist.
 *
 * A curated international supplement is always included. The fleet skews
 * UK/US, and without it Welsh, Czech, Ukrainian and Nordic names score as
 * machine-generated — measured, not hypothetical.
 *
 * Re-run after a large influx of genuinely new name shapes (a new market, a
 * new language). Adding words only ever makes the filter more permissive.
 */

if (PHP_SAPI !== 'cli') {
    die("CLI only\n");
}

$verifiedFiles = array();
$filteredFiles = array();
$dictPath      = '/usr/share/dict/words';

for ($i = 1; $i < $argc; $i++) {
    switch ($argv[$i]) {
        case '--verified': $verifiedFiles[] = $argv[++$i]; break;
        case '--filtered': $filteredFiles[] = $argv[++$i]; break;
        case '--dict':     $dictPath        = $argv[++$i]; break;
        default:
            // Bare argument keeps the original one-file invocation working.
            if ($argv[$i][0] !== '-') $filteredFiles[] = $argv[$i];
    }
}

if (!$verifiedFiles && !$filteredFiles) {
    fwrite(STDERR, "usage: php bin/build-name-model.php [--verified f.tsv] [--filtered f.tsv] [--dict path]\n");
    exit(1);
}

const ALPHABET_SIZE = 28; // a-z, plus ^ (word start) and $ (word end)

function bs_symbol($ch) {
    if ($ch === '^') return 26;
    if ($ch === '$') return 27;
    $o = ord($ch) - 97;
    return ($o >= 0 && $o < 26) ? $o : -1;
}

function bs_words($name) {
    $parts = preg_split('/[^a-z]+/', strtolower($name), -1, PREG_SPLIT_NO_EMPTY);
    return $parts ?: array();
}

$corpus = array();

$addWords = function ($words) use (&$corpus) {
    foreach ($words as $w) {
        if (strlen($w) >= 2) $corpus[$w] = true;
    }
};

// ---- verified humans: trusted outright -----------------------------------
$people = 0; $trusted = 0;
foreach ($verifiedFiles as $path) {
    if (!is_readable($path)) { fwrite(STDERR, "skipping unreadable $path\n"); continue; }
    foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
        $cols = explode("\t", $line);
        if (count($cols) < 2) continue;
        $people++; $trusted++;
        $addWords(array_merge(bs_words($cols[0]), bs_words($cols[1])));
    }
}

// ---- everyone else: name must be echoed in the email ---------------------
foreach ($filteredFiles as $path) {
    if (!is_readable($path)) { fwrite(STDERR, "skipping unreadable $path\n"); continue; }
    foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
        $cols = explode("\t", $line);
        if (count($cols) < 3) continue;
        $people++;
        list($first, $last, $email) = array(trim($cols[0]), trim($cols[1]), trim($cols[2]));

        $local = preg_replace('/[^a-z]/', '', strtolower((string) strtok($email, '@')));
        if ($local === '') continue;

        $words = array_merge(bs_words($first), bs_words($last));
        $match = false;
        foreach ($words as $w) {
            if (strlen($w) >= 4 && strpos($local, $w) !== false) { $match = true; break; }
        }
        if (!$match) continue;

        $trusted++;
        $addWords($words);
    }
}

// ---- curated international supplement ------------------------------------
$supplement = __DIR__ . '/name-supplement.txt';
$suppWords = 0;
if (is_readable($supplement)) {
    foreach (file($supplement, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
        $line = trim($line);
        if ($line === '' || $line[0] === '#') continue;
        foreach (bs_words($line) as $w) {
            if (strlen($w) >= 2 && !isset($corpus[$w])) { $corpus[$w] = true; $suppWords++; }
        }
    }
}
$nameWords = count($corpus);

// ---- dictionary ----------------------------------------------------------
$dictWords = 0;
if (is_readable($dictPath)) {
    foreach (file($dictPath, FILE_IGNORE_NEW_LINES) as $w) {
        $w = preg_replace('/[^a-z]/', '', strtolower(trim($w)));
        if (strlen($w) >= 3 && !isset($corpus[$w])) { $corpus[$w] = true; $dictWords++; }
    }
}

// ---- bitmap --------------------------------------------------------------
$bits = str_repeat("\0", (int) ceil(pow(ALPHABET_SIZE, 3) / 8));
$trigrams = 0;
foreach (array_keys($corpus) as $word) {
    $padded = '^' . $word . '$';
    $len = strlen($padded);
    for ($i = 0; $i < $len - 2; $i++) {
        $a = bs_symbol($padded[$i]);
        $b = bs_symbol($padded[$i + 1]);
        $c = bs_symbol($padded[$i + 2]);
        if ($a < 0 || $b < 0 || $c < 0) continue;
        $idx  = ($a * ALPHABET_SIZE + $b) * ALPHABET_SIZE + $c;
        $byte = $idx >> 3;
        $mask = 1 << ($idx & 7);
        if (!(ord($bits[$byte]) & $mask)) {
            $bits[$byte] = chr(ord($bits[$byte]) | $mask);
            $trigrams++;
        }
    }
}

$out = "<?php\n"
     . "/**\n"
     . " * Character-trigram table for the AuctionForge bot shield.\n"
     . " *\n"
     . " * GENERATED FILE — do not hand-edit. Rebuild with:\n"
     . " *   php bin/build-name-model.php <people.tsv> /usr/share/dict/words\n"
     . " *\n"
     . " * Bitmap of every 3-character sequence (over a-z plus ^ start and \$ end\n"
     . " * markers) seen in the corpus. Bit index = ((a*28)+b)*28+c.\n"
     . " */\n\n"
     . "return array(\n"
     . "    'built'         => '" . gmdate('Y-m-d') . "',\n"
     . "    'corpus_words'  => " . count($corpus) . ",\n"
     . "    'name_words'    => " . $nameWords . ",\n"
     . "    'supplement'    => " . $suppWords . ",\n"
     . "    'dict_words'    => " . $dictWords . ",\n"
     . "    'trusted_people'=> " . $trusted . " ,\n"
     . "    'trigrams'      => " . $trigrams . ",\n"
     . "    'bitmap'        => '" . base64_encode($bits) . "',\n"
     . ");\n";

$target = __DIR__ . '/../inc/data/name-model.php';
if (!is_dir(dirname($target))) mkdir(dirname($target), 0755, true);
file_put_contents($target, $out);

printf("people read      : %d\n", $people);
printf("trusted (matched): %d\n", $trusted);
printf("supplement words : %d\n", $suppWords);
printf("name words       : %d\n", $nameWords);
printf("dictionary words : %d\n", $dictWords);
printf("corpus words     : %d\n", count($corpus));
printf("distinct trigrams: %d\n", $trigrams);
printf("written          : %s (%d bytes)\n", realpath($target), filesize($target));
