<?php

declare(strict_types=1);

/**
 * Fundstücke für die Vereinschronik 2028 einsammeln und ablegen.
 *
 * Holt zu einer Web-Adresse Überschrift, Datum, Teaser, Bildnachweis und das Aufmacherbild,
 * legt beides im richtigen Jahrzehnt-Ordner unter Vereinshistorie/ ab, schreibt einen
 * Quellen-Beleg daneben und eine Zeile ins Register (docs/chronik/register.csv).
 *
 * Warum überhaupt ein Skript und kein „Rechtsklick, Bild speichern": das Bild allein ist
 * in fünf Jahren wertlos. Woher es stammt, wann es entstand und wem es gehört, steht nirgends
 * im JPEG — und genau das braucht man, wenn die Chronik in den Druck geht. Das Skript hält
 * den Zusammenhang fest, in dem Moment, in dem er noch bekannt ist.
 *
 * Es wird NICHTS massenhaft abgegriffen: pro Aufruf eine Seite, mit Pause zwischen den
 * Abrufen. Die Presseportale haben keine Übersichtsseite mehr, über die man den Verein
 * vollständig abfragen könnte — die Fundstellen kommen aus gezielter Suche, nicht aus
 * einem Crawler.
 *
 * NUR CLI, nie im Request-Pfad (wie alle bin/-Skripte).
 *
 * Aufruf:
 *   php bin/chronik-add.php <URL> [--datum=1972-07-15] [--titel="…"] [--art=Zeitungsartikel]
 *                                 [--notiz="…"] [--urheber="…"] [--ohne-bild] [--nur-register]
 *   php bin/chronik-add.php --liste=urls.txt      Zeilen: URL | Datum | Titel   (| optional)
 *   php bin/chronik-add.php --datei=<pfad> --datum=1983-06-11 --titel="Festzug 75 Jahre"
 *                           [--quelle="Album Familie Rausch"] [--urheber="…"] [--art=Foto]
 *                           [--rechte-klaeren]
 *   php bin/chronik-add.php --rechte              nur 00_RECHTE-ZU-KLAEREN.txt neu schreiben
 *   php bin/chronik-add.php --bericht             Registerübersicht auf stdout
 *
 * Optionen --datum/--titel gewinnen immer gegen das, was die Seite selbst angibt.
 * --datei benennt die Datei um und verschiebt sie in den passenden Jahrzehnt-Ordner.
 */
if (PHP_SAPI !== 'cli') {
    exit(1);
}

require dirname(__DIR__) . '/app/bootstrap.php';
require __DIR__ . '/chronik-lib.php';

$opts = [];
$positional = [];
foreach (array_slice($argv, 1) as $arg) {
    if (preg_match('/^--([a-z-]+)(?:=(.*))?$/s', $arg, $m)) {
        $opts[$m[1]] = $m[2] ?? true;
    } else {
        $positional[] = $arg;
    }
}

$root = chronik_root();
if (!is_dir($root)) {
    fwrite(STDERR, "Vereinshistorie/ fehlt — erst 'php bin/chronik-init.php' laufen lassen.\n");
    exit(1);
}

// --- Nebenmodi -------------------------------------------------------------

if (isset($opts['rechte'])) {
    $n = chronik_write_rights_todo();
    echo "00_RECHTE-ZU-KLAEREN.txt geschrieben — {$n} offene Posten.\n";
    exit(0);
}

if (isset($opts['bericht'])) {
    chronik_report();
    exit(0);
}

// --- HTTP ------------------------------------------------------------------

/**
 * Browser-typische Header. Ohne sie antworten mehrere der Quellen mit 403 —
 * dieselbe Erfahrung wie beim BFV-Widget (dort HTTP 418), siehe matchcenter-sync.php.
 */
function chronik_fetch(string $url, ?string $referer = null): array
{
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 5,
        CURLOPT_TIMEOUT        => 25,
        CURLOPT_ENCODING       => '',
        CURLOPT_USERAGENT      => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 '
                                . '(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
        CURLOPT_HTTPHEADER     => array_filter([
            'Accept: text/html,application/xhtml+xml,image/avif,image/webp,*/*;q=0.8',
            'Accept-Language: de-DE,de;q=0.9',
            $referer !== null ? 'Referer: ' . $referer : null,
        ]),
    ]);
    $body = curl_exec($ch);
    $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $type = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
    $final = (string) curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);

    return [
        'ok'     => $body !== false && $status === 200,
        'status' => $status,
        'body'   => $body === false ? '' : $body,
        'type'   => $type,
        'url'    => $final !== '' ? $final : $url,
    ];
}

/** Ersten Treffer eines Musters liefern, HTML-Entities aufgelöst. */
function chronik_match(string $html, string $pattern): ?string
{
    if (!preg_match($pattern, $html, $m)) {
        return null;
    }
    $val = html_entity_decode(trim($m[1]), ENT_QUOTES | ENT_HTML5, 'UTF-8');
    return $val !== '' ? $val : null;
}

/**
 * Fließtext einer Seite herausschälen.
 *
 * Grob und absichtlich: Skripte und Styles raus, Tags raus, alles behalten, was länger als
 * ein Navigationspunkt ist. Für saubere Extraktion bräuchte es pro Portal eine eigene Regel —
 * das lohnt hier nicht, weil der Text als Beleg dient und nicht als Layout.
 */
function chronik_body_text(string $html): string
{
    $html = (string) preg_replace('#<(script|style|nav|header|footer|form)\b.*?</\1>#is', ' ', $html);
    $html = (string) preg_replace('#<(br|/p|/div|/h[1-6]|/li|/td|/tr)[^>]*>#i', "\n", $html);
    $text = html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8');

    $keep = [];
    $seen = [];
    foreach (explode("\n", $text) as $line) {
        $line = trim((string) preg_replace('/[ \t\x{00A0}]+/u', ' ', $line));
        // 45 Zeichen trennt Fließtext zuverlässig von Menüpunkten und Buttons.
        if (mb_strlen($line) < 45 || isset($seen[$line])) {
            continue;
        }
        $seen[$line] = true;
        $keep[] = $line;
    }

    return implode("\n\n", $keep);
}

/** Seite auswerten: was an Metadaten drinsteht, ohne Layout-Annahmen. */
function chronik_parse(string $html): array
{
    $strip = static fn(string $s): string => trim((string) preg_replace('/\s+/', ' ', strip_tags($s)));

    $titel = chronik_match($html, '/<meta property="og:title" content="([^"]+)"/i')
        ?? chronik_match($html, '/<h1[^>]*>(.*?)<\/h1>/is')
        ?? chronik_match($html, '/<title[^>]*>(.*?)<\/title>/is');
    if ($titel !== null) {
        $titel = $strip($titel);
        // Seitentitel tragen oft " | Portalname" mit — für den Dateinamen stört das nur.
        $titel = (string) preg_replace('/\s*[|–-]\s*(inFranken\.de|BFV|FUSSBALL\.DE|Deutsche Turnliga)\s*$/iu', '', $titel);
    }

    $datum = chronik_match($html, '/<meta property="article:published_time" content="(\d{4}-\d{2}-\d{2})/i')
        ?? chronik_match($html, '/"datePublished"\s*:\s*"(\d{4}-\d{2}-\d{2})/i')
        ?? chronik_match($html, '/datetime="(\d{4}-\d{2}-\d{2})/i');

    $teaser = chronik_match($html, '/<meta property="og:description" content="([^"]+)"/i')
        ?? chronik_match($html, '/<meta name="description" content="([^"]+)"/i');

    $bild = chronik_match($html, '/<meta property="og:image" content="([^"]+)"/i');

    // Bildnachweis steht bei Presseseiten fast immer in der Bildunterschrift
    // („Foto: Max Mustermann") — genau die Angabe, die später fehlt.
    $urheber = null;
    if (preg_match_all('/<figcaption[^>]*>(.*?)<\/figcaption>/is', $html, $caps)) {
        foreach ($caps[1] as $cap) {
            $cap = $strip(html_entity_decode($cap, ENT_QUOTES | ENT_HTML5, 'UTF-8'));
            if (preg_match('/\b(Foto|Bild|Quelle|©)\s*:?\s*(.{2,80})$/ui', $cap, $m)) {
                $urheber = trim($m[2], " .;");
                break;
            }
        }
    }
    $urheber ??= chronik_match($html, '/<meta name="author" content="([^"]+)"/i');

    return compact('titel', 'datum', 'teaser', 'bild', 'urheber');
}

// --- Ein Fundstück verarbeiten ---------------------------------------------

function chronik_add(string $url, array $opts, array $override = []): bool
{
    $root = chronik_root();
    $heute = date('Y-m-d');

    if (chronik_register_has_url($url) && !isset($opts['erneut'])) {
        echo "·  schon im Register: {$url}\n";
        return true;
    }

    // Bei Wayback-Adressen zählt für Rechte und Quellenkürzel die ARCHIVIERTE Seite,
    // nicht das Archiv — abgerufen wird trotzdem der Schnappschuss.
    $quellUrl = chronik_origin_url($url);
    $host = (string) (parse_url($quellUrl, PHP_URL_HOST) ?: '');
    if ($host === '') {
        fwrite(STDERR, "✗  Keine gültige Adresse: {$url}\n");
        return false;
    }

    $rights = chronik_rights($host);

    $meta = ['titel' => null, 'datum' => null, 'teaser' => null, 'bild' => null, 'urheber' => null];
    $volltext = '';
    if (!isset($opts['nur-register'])) {
        $res = chronik_fetch($url);
        if (!$res['ok']) {
            fwrite(STDERR, "✗  HTTP {$res['status']}: {$url}\n");
            log_write('chronik', 'WARN', "Abruf fehlgeschlagen (HTTP {$res['status']}): {$url}");
            return false;
        }
        $meta = chronik_parse($res['body']);
        // Die Rechtelage entscheidet, wie viel Text bleibt: eigenes Vereinsmaterial sichern
        // wir vollständig (sonst ist es weg — die alten Vereinsseiten sind offline), fremde
        // Presseartikel nur als Nachweis mit Überschrift und Anriss.
        if (!$rights['klaeren']) {
            $volltext = chronik_body_text($res['body']);
        }
    }

    $titel   = (string) ($override['titel'] ?? $opts['titel'] ?? $meta['titel'] ?? 'Ohne Titel');
    $datum   = $override['datum'] ?? $opts['datum'] ?? $meta['datum'] ?? null;
    $urheber = (string) ($opts['urheber'] ?? $meta['urheber'] ?? '');
    $art     = (string) ($opts['art'] ?? 'Zeitungsartikel');
    $notiz   = (string) ($opts['notiz'] ?? '');
    if ($quellUrl !== $url) {
        $notiz = trim($notiz . ' Archivfassung (Internet Archive), Original nicht mehr online: ' . $quellUrl);
    }

    $tag    = chronik_source_tag($host);
    $bucket = chronik_bucket(is_string($datum) ? $datum : null);
    $dnorm  = chronik_date_norm(is_string($datum) ? $datum : null);
    $base   = $dnorm . '_' . chronik_slug($titel) . '_' . $tag;

    $dir = $root . '/' . $bucket;
    if (!is_dir($dir)) {
        mkdir($dir, 0775, true);
    }

    // Namenskollision auflösen. Passiert wirklich: zwei Seiten „Bundesliga" und
    // „Bundesliga-1" mit gleicher Überschrift und gleichem Jahr ergeben denselben Namen —
    // ohne Suffix überschreibt der zweite Fund den ersten stillschweigend.
    // Anker ist die .quelle.txt, die zu jedem Fundstück entsteht.
    if (is_file($dir . '/' . $base . '.quelle.txt')) {
        $n = 2;
        while (is_file($dir . '/' . $base . '-' . $n . '.quelle.txt')) {
            $n++;
        }
        $base .= '-' . $n;
    }

    // --- Aufmacherbild ---
    $bildDatei = '';
    if (!isset($opts['ohne-bild']) && !isset($opts['nur-register']) && is_string($meta['bild'])) {
        $img = chronik_fetch($meta['bild'], $url);
        $ext = match (true) {
            str_contains($img['type'], 'png')  => 'png',
            str_contains($img['type'], 'webp') => 'webp',
            str_contains($img['type'], 'gif')  => 'gif',
            default => 'jpg',
        };
        // 6 KB Untergrenze: Portale liefern bei fehlendem Aufmacher ein Platzhalter-Pixel aus.
        if ($img['ok'] && strlen($img['body']) > 6144) {
            $bildDatei = $base . '.' . $ext;
            file_put_contents($dir . '/' . $bildDatei, $img['body']);
        }
    }

    // --- Textbeleg: das Fundstück bleibt auffindbar, auch wenn die Seite verschwindet ---
    $belegDatei = $base . '.artikel.txt';
    $beleg = implode("\n", array_filter([
        $titel,
        str_repeat('=', min(72, max(8, strlen($titel)))),
        '',
        $dnorm !== '0000-00-00' ? 'Erschienen: ' . $dnorm : null,
        'Quelle:     ' . $host,
        'URL:        ' . $url,
        'Abgerufen:  ' . $heute,
        '',
        // Bei Volltext ist der Teaser nur dessen erster Absatz — nicht doppelt hinschreiben.
        ($meta['teaser'] !== null && $volltext === '') ? wordwrap((string) $meta['teaser'], 88, "\n") : null,
        $volltext === '' ? '' : null,
        '--',
        $volltext !== ''
            ? "Eigenes Vereinsmaterial — vollständig gesichert, weil die Quellseite offline ist."
            : 'Nur Überschrift, Datum und Anrisstext festgehalten — der vollständige Artikeltext',
        $volltext !== ''
            ? ''
            : 'gehört dem Verlag und wird hier nicht gespeichert. Für die Chronik über die URL',
        $volltext !== '' ? '' : 'oder das Zeitungsarchiv im Original nachlesen.',
        $volltext !== '' ? "\n" . wordwrap($volltext, 88, "\n") : null,
    ], static fn($l): bool => $l !== null));
    file_put_contents($dir . '/' . $belegDatei, $beleg . "\n");

    // --- Register + Beleg-Sidecar ---
    $row = [
        'datei'     => $bucket . '/' . ($bildDatei !== '' ? $bildDatei : $belegDatei),
        'datum'     => $dnorm,
        'titel'     => $titel,
        'art'       => $art,
        'quelle'    => $host,
        'url'       => $url,
        'urheber'   => $urheber,
        'rechte'    => $rights['status'],
        'klaeren'   => $rights['klaeren'] ? 'ja' : 'nein',
        'abgerufen' => $heute,
        'notiz'     => $notiz,
    ];
    chronik_register_append($row);
    file_put_contents($dir . '/' . $base . '.quelle.txt', chronik_sidecar_text($row, $rights['hinweis']));

    $flag = $rights['klaeren'] ? ' [Rechte klären]' : '';
    echo "✓  {$bucket}/{$base}" . ($bildDatei !== '' ? ' (+Bild)' : ' (nur Text)') . $flag . "\n";
    log_write('chronik', 'INFO', "Fundstück aufgenommen: {$bucket}/{$base} ({$host})");

    return true;
}

/**
 * Eine Datei aufnehmen, die schon da ist — Scan aus einer Festschrift, Foto aus einem
 * privaten Album, per Hand gespeichertes Pressebild.
 *
 * Genau derselbe Weg wie bei einer Web-Adresse: umbenennen nach Konvention, ins richtige
 * Jahrzehnt einsortieren, Beleg daneben, Zeile ins Register. Ohne das landen die Scans aus
 * dem Sammelaufruf als IMG_4711.jpg im Ordner und sind in einem Jahr nicht mehr zuzuordnen.
 */
function chronik_add_file(string $path, array $opts): bool
{
    if (!is_file($path)) {
        fwrite(STDERR, "✗  Datei nicht gefunden: {$path}\n");
        return false;
    }

    $root  = chronik_root();
    $heute = date('Y-m-d');

    $datum   = is_string($opts['datum'] ?? null) ? $opts['datum'] : null;
    $titel   = (string) ($opts['titel'] ?? pathinfo($path, PATHINFO_FILENAME));
    $quelle  = (string) ($opts['quelle'] ?? 'Vereinsbestand');
    $urheber = (string) ($opts['urheber'] ?? '');
    $art     = (string) ($opts['art'] ?? 'Foto');
    $notiz   = (string) ($opts['notiz'] ?? '');

    // Ohne --quelle gilt: es kommt aus dem Verein und ist nutzbar. Mit --quelle=<Host>
    // greift dieselbe Einschätzung wie bei einem Abruf aus dem Netz.
    $rights = str_contains($quelle, '.')
        ? chronik_rights($quelle)
        : ['status' => 'Vereinsbestand — Herkunft ' . $quelle, 'klaeren' => false,
           'hinweis' => 'Aus dem Bestand des Vereins bzw. von Mitgliedern. Bei erkennbaren Personen '
                      . 'die Einwilligung zur Veröffentlichung einholen, bei Kindern die Eltern fragen.'];
    if (isset($opts['rechte-klaeren'])) {
        $rights['klaeren'] = true;
    }

    $tag    = str_contains($quelle, '.') ? chronik_source_tag($quelle) : strtoupper(chronik_slug($quelle, 12));
    $bucket = chronik_bucket($datum);
    $dnorm  = chronik_date_norm($datum);
    $ext    = strtolower(pathinfo($path, PATHINFO_EXTENSION)) ?: 'bin';
    $base   = $dnorm . '_' . chronik_slug($titel) . '_' . $tag;

    $dir = $root . '/' . $bucket;
    if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) {
        fwrite(STDERR, "✗  Zielordner nicht anlegbar: {$bucket}\n");
        return false;
    }

    $ziel = $dir . '/' . $base . '.' . $ext;
    if (realpath($path) !== realpath($ziel) && !rename($path, $ziel)) {
        fwrite(STDERR, "✗  Verschieben fehlgeschlagen: {$path}\n");
        return false;
    }

    $row = [
        'datei'     => $bucket . '/' . $base . '.' . $ext,
        'datum'     => $dnorm,
        'titel'     => $titel,
        'art'       => $art,
        'quelle'    => $quelle,
        'url'       => (string) ($opts['url'] ?? ''),
        'urheber'   => $urheber,
        'rechte'    => $rights['status'],
        'klaeren'   => $rights['klaeren'] ? 'ja' : 'nein',
        'abgerufen' => $heute,
        'notiz'     => $notiz,
    ];
    chronik_register_append($row);
    file_put_contents($dir . '/' . $base . '.quelle.txt', chronik_sidecar_text($row, $rights['hinweis']));

    echo "✓  {$bucket}/{$base}.{$ext}" . ($rights['klaeren'] ? ' [Rechte klären]' : '') . "\n";
    return true;
}

/** Registerübersicht — was haben wir, wo klafft die Lücke. */
function chronik_report(): void
{
    $path = chronik_register_path();
    if (!is_file($path)) {
        echo "Register ist noch leer.\n";
        return;
    }
    $fh = fopen($path, 'r');
    fgetcsv($fh, 0, ';', '"', '\\');

    $perBucket = array_fill_keys(array_merge(array_values(CHRONIK_BUCKETS), [CHRONIK_DOKUMENTE, CHRONIK_UNDATIERT]), 0);
    $perQuelle = [];
    $offen = 0;
    $gesamt = 0;
    while (($r = fgetcsv($fh, 0, ';', '"', '\\')) !== false) {
        $gesamt++;
        $bucket = explode('/', (string) ($r[0] ?? ''))[0];
        if (isset($perBucket[$bucket])) {
            $perBucket[$bucket]++;
        }
        $perQuelle[$r[4] ?? '?'] = ($perQuelle[$r[4] ?? '?'] ?? 0) + 1;
        if (($r[8] ?? '') === 'ja') {
            $offen++;
        }
    }
    fclose($fh);

    echo "Chronik-Register — {$gesamt} Fundstücke, {$offen} mit offenen Rechten\n\n";
    echo "Nach Zeitraum:\n";
    foreach ($perBucket as $b => $n) {
        printf("  %-28s %3d %s\n", $b, $n, $n === 0 ? '← Lücke' : str_repeat('▌', min(40, $n)));
    }
    echo "\nNach Quelle:\n";
    arsort($perQuelle);
    foreach ($perQuelle as $q => $n) {
        printf("  %-28s %3d\n", $q, $n);
    }
}

// --- Hauptlauf -------------------------------------------------------------

if (isset($opts['datei'])) {
    $ok = chronik_add_file((string) $opts['datei'], $opts);
    $offen = chronik_write_rights_todo();
    echo "\n{$offen} Fundstücke mit offenen Rechten — siehe Vereinshistorie/" . CHRONIK_RECHTE . "\n";
    exit($ok ? 0 : 1);
}

$jobs = [];
if (isset($opts['liste'])) {
    $listFile = (string) $opts['liste'];
    if (!is_file($listFile)) {
        fwrite(STDERR, "Liste nicht gefunden: {$listFile}\n");
        exit(1);
    }
    foreach (file($listFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
        $line = trim($line);
        if ($line === '' || str_starts_with($line, '#')) {
            continue;
        }
        $parts = array_map('trim', explode('|', $line));
        $jobs[] = [
            'url'   => $parts[0],
            'datum' => ($parts[1] ?? '') !== '' ? $parts[1] : null,
            'titel' => ($parts[2] ?? '') !== '' ? $parts[2] : null,
        ];
    }
} elseif ($positional !== []) {
    $jobs[] = ['url' => $positional[0], 'datum' => null, 'titel' => null];
} else {
    fwrite(STDERR, "Aufruf: php bin/chronik-add.php <URL> | --liste=urls.txt | --rechte | --bericht\n");
    exit(1);
}

$ok = 0;
$fehler = 0;
foreach ($jobs as $i => $job) {
    if ($i > 0) {
        // Pause zwischen den Abrufen: das hier ist Recherche, kein Crawl.
        usleep(1_500_000);
    }
    $override = array_filter(['datum' => $job['datum'], 'titel' => $job['titel']], static fn($v): bool => $v !== null);
    chronik_add($job['url'], $opts, $override) ? $ok++ : $fehler++;
}

$offen = chronik_write_rights_todo();

echo "\n{$ok} aufgenommen" . ($fehler > 0 ? ", {$fehler} fehlgeschlagen" : '') . ".\n";
echo "{$offen} Fundstücke mit offenen Rechten — siehe Vereinshistorie/" . CHRONIK_RECHTE . "\n";
exit($fehler > 0 ? 1 : 0);
