<?php
/**
 * whatthatfood-notes.php — tiny self-hosted comment store for the food glossary.
 *
 * Put this file in the SAME folder as food-glossary.html.
 * Comments are saved to whatthatfood-notes-data.json in this folder, so the folder
 * (or at least that file) must be writable by the web server.
 *
 * GET  whatthatfood-notes.php?term=osso%20buco   -> JSON list of notes for that word
 * POST whatthatfood-notes.php  {term, name, text} -> saves it, returns the updated list
 *
 * For you, the owner (set the password below, then open whatthatfood-notes-review.php):
 * POST whatthatfood-notes.php  {action:"list", key}        -> the newest notes across all words
 * POST whatthatfood-notes.php  {action:"delete", key, id}  -> removes one note
 */

// Set this to the same kind of password you used for photos, then use it on the review page.
$ADMIN_PASSWORD = '111222';

header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');

// String helpers that work with or without the mbstring extension.
function lc($s)            { return function_exists('mb_strtolower') ? mb_strtolower($s, 'UTF-8') : strtolower($s); }
function cut($s, $n)       { return function_exists('mb_substr') ? mb_substr($s, 0, $n, 'UTF-8') : substr($s, 0, $n); }
function slen($s)          { return function_exists('mb_strlen') ? mb_strlen($s, 'UTF-8') : strlen($s); }

// Lock so two simultaneous writes can't overwrite each other.
function lock_file($file)   { $fp = fopen($file . '.lock', 'c'); if ($fp) flock($fp, LOCK_EX); return $fp; }
function unlock_file($fp)   { if ($fp) { flock($fp, LOCK_UN); fclose($fp); } }

// Real visitor address even behind Cloudflare or a proxy (otherwise everyone shares one rate limit).
function client_ip() {
    foreach (['HTTP_CF_CONNECTING_IP', 'HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR'] as $h) {
        if (!empty($_SERVER[$h])) { $ip = trim(explode(',', $_SERVER[$h])[0]); if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip; }
    }
    return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
}

$DATA_FILE   = __DIR__ . '/whatthatfood-notes-data.json';
$MAX_TEXT    = 600;   // characters per comment
$MAX_NAME    = 40;
$MAX_PER_TERM = 200;  // keep the newest N per word
$RATE_SECONDS = 20;   // one post per IP per N seconds

function fail($msg, $code = 400) {
    http_response_code($code);
    echo json_encode(['error' => $msg]);
    exit;
}

function term_key($t) {
    $t = lc(trim($t));
    $t = preg_replace('/\s+/u', ' ', $t);
    return cut($t, 80);
}

function load_all($file) {
    if (!file_exists($file)) return ['comments' => [], 'last_post' => []];
    $raw = file_get_contents($file);
    $data = json_decode($raw, true);
    if (!is_array($data)) $data = [];
    $data += ['comments' => [], 'last_post' => []];
    return $data;
}

function save_all($file, $data) {
    $tmp = $file . '.tmp';
    if (file_put_contents($tmp, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX) === false) {
        fail('Could not write comments file. Is the folder writable?', 500);
    }
    rename($tmp, $file);
}

$method = $_SERVER['REQUEST_METHOD'];

if ($method === 'GET') {
    $key = term_key($_GET['term'] ?? '');
    if ($key === '') fail('Missing term');
    $data = load_all($DATA_FILE);
    echo json_encode($data['comments'][$key] ?? [], JSON_UNESCAPED_UNICODE);
    exit;
}

function is_admin($key) {
    global $ADMIN_PASSWORD;
    return $ADMIN_PASSWORD !== 'CHANGE-THIS-PASSWORD' && is_string($key) && hash_equals($ADMIN_PASSWORD, $key);
}

if ($method === 'POST') {
    $body = json_decode(file_get_contents('php://input'), true);
    if (!is_array($body)) fail('Bad request body');

    // ---- owner's review page -------------------------------------------
    if (isset($body['action'])) {
        if (!is_admin($body['key'] ?? '')) fail('Wrong password, or no password set in whatthatfood-notes.php', 403);
        $data = load_all($DATA_FILE);

        if ($body['action'] === 'list') {
            $rows = [];
            foreach ($data['comments'] as $termKey => $list) {
                foreach ($list as $c) {
                    $rows[] = [
                        'id'   => $c['id'] ?? '',
                        'term' => $termKey,
                        'name' => $c['name'] ?? 'Anonymous',
                        'text' => $c['text'] ?? '',
                        'date' => $c['date'] ?? '',
                    ];
                }
            }
            usort($rows, function ($a, $b) { return strcmp($b['date'], $a['date']); });   // newest first
            $limit = max(1, min(500, (int)($body['limit'] ?? 100)));
            echo json_encode(['total' => count($rows), 'notes' => array_slice($rows, 0, $limit)], JSON_UNESCAPED_UNICODE);
            exit;
        }

        if ($body['action'] === 'delete') {
            $id = (string)($body['id'] ?? '');
            if ($id === '') fail('Missing id');
            $lock = lock_file($DATA_FILE);
            $data = load_all($DATA_FILE);
            $removed = false;
            foreach ($data['comments'] as $termKey => $list) {
                $kept = array_values(array_filter($list, function ($c) use ($id, &$removed) {
                    if (($c['id'] ?? '') === $id) { $removed = true; return false; }
                    return true;
                }));
                if ($kept) $data['comments'][$termKey] = $kept; else unset($data['comments'][$termKey]);
            }
            if (!$removed) { unlock_file($lock); fail('Not found', 404); }
            save_all($DATA_FILE, $data);
            unlock_file($lock);
            echo json_encode(['ok' => true]);
            exit;
        }

        fail('Unknown action');
    }

    // Honeypot: real users never see this field, bots fill it in.
    if (!empty($body['website'])) { echo json_encode([]); exit; }

    $key  = term_key($body['term'] ?? '');
    $text = trim((string)($body['text'] ?? ''));
    $name = trim((string)($body['name'] ?? ''));

    if ($key === '')  fail('Missing term');
    if ($text === '') fail('Write something first');
    if (slen($text) > $MAX_TEXT) fail("Keep it under $MAX_TEXT characters");
    if (slen($name) > $MAX_NAME) $name = cut($name, $MAX_NAME);
    // Strip anything that isn't plain text (no tags, no control chars).
    $text = strip_tags($text);
    $name = strip_tags($name);
    $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/u', '', $text);

    $lock = lock_file($DATA_FILE);
    $data = load_all($DATA_FILE);

    // Simple rate limit per IP.
    $ip = client_ip();
    $ipKey = hash('sha256', $ip);
    $now = time();
    if (isset($data['last_post'][$ipKey]) && $now - $data['last_post'][$ipKey] < $RATE_SECONDS) {
        unlock_file($lock);
        fail('Slow down a little — try again in a few seconds', 429);
    }
    $data['last_post'][$ipKey] = $now;
    // Forget IPs older than a day so the file doesn't grow forever.
    foreach ($data['last_post'] as $k => $t) if ($now - $t > 86400) unset($data['last_post'][$k]);

    $data['comments'][$key][] = [
        'id'   => bin2hex(random_bytes(8)),
        'name' => $name === '' ? 'Anonymous' : $name,
        'text' => $text,
        'date' => gmdate('c', $now),
    ];
    if (count($data['comments'][$key]) > $MAX_PER_TERM) {
        $data['comments'][$key] = array_slice($data['comments'][$key], -$MAX_PER_TERM);
    }

    save_all($DATA_FILE, $data);
    unlock_file($lock);
    echo json_encode($data['comments'][$key], JSON_UNESCAPED_UNICODE);
    exit;
}

fail('Method not allowed', 405);
