<?php
declare(strict_types=1);


/*
|--------------------------------------------------------------------------
| ABDL MAP 2.15 – DYNAMISCHE SITEMAP
|--------------------------------------------------------------------------
|
| Enthält:
|
| - wichtige öffentliche statische Seiten
| - öffentliche freigegebene Community-Profile
| - öffentliche aktive laufende und zukünftige Events
|
| Enthält NICHT:
|
| - Bearbeitungsseiten
| - private Events
| - Tokens
| - Filter-/Suchseiten
| - Pagination
| - Admin-/Debug-Seiten
| - technische Endpunkte und Erstellungsformulare
|
|--------------------------------------------------------------------------
*/


require_once __DIR__ . '/includes/bootstrap.php';
require_once __DIR__ . '/includes/event_recurrence.php';


/*
|--------------------------------------------------------------------------
| Header
|--------------------------------------------------------------------------
*/

header(
    'Content-Type: application/xml; charset=utf-8'
);

header(
    'Cache-Control: public, max-age=3600'
);

header(
    'X-Content-Type-Options: nosniff'
);


/*
|--------------------------------------------------------------------------
| Basis-URL
|--------------------------------------------------------------------------
*/

$baseUrl =
    defined('APP_URL')
        ? rtrim((string) APP_URL, '/')
        : 'https://abdl-map.4lima.de';


/*
|--------------------------------------------------------------------------
| XML Escape
|--------------------------------------------------------------------------
*/

function sitemapXml(
    string $value
): string {

    return
        htmlspecialchars(
            $value,
            ENT_XML1
            |
            ENT_QUOTES,
            'UTF-8'
        );
}


/*
|--------------------------------------------------------------------------
| Datum formatieren
|--------------------------------------------------------------------------
*/

function sitemapDate(
    mixed $value
): ?string {

    if (
        !is_string($value)
        ||
        trim($value) === ''
    ) {
        return null;
    }


    try {

        $date =
            new DateTimeImmutable(
                $value
            );


        return
            $date->format(
                DATE_ATOM
            );


    } catch (Throwable) {

        return null;
    }
}


/*
|--------------------------------------------------------------------------
| URL-Eintrag
|--------------------------------------------------------------------------
*/

function sitemapWriteUrl(
    string $url,
    ?string $lastmod = null
): void {

    echo "  <url>\n";

    echo
        '    <loc>'
        .
        sitemapXml($url)
        .
        "</loc>\n";


    if (
        $lastmod !== null
        &&
        $lastmod !== ''
    ) {

        echo
            '    <lastmod>'
            .
            sitemapXml($lastmod)
            .
            "</lastmod>\n";
    }


    echo "  </url>\n";
}


/*
|--------------------------------------------------------------------------
| XML starten
|--------------------------------------------------------------------------
*/

echo
    '<?xml version="1.0" encoding="UTF-8"?>'
    .
    "\n";


echo
    '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
    .
    "\n";


/*
|--------------------------------------------------------------------------
| Statische Seiten
|--------------------------------------------------------------------------
|
| Nur Seiten, die tatsächlich indexiert werden sollen.
|
| Bewusst NICHT:
|
| - eintragen.php
| - event_eintragen.php
| - link_eintragen.php
| - edit_*.php
| - private/tokenisierte Seiten
|--------------------------------------------------------------------------
*/

$staticPages = [

    '/',

    '/karte.php',

    '/eintraege.php',

    '/neueste_eintraege.php',

    '/kategorien.php',

    '/events.php',

    '/links.php',

    '/krabbelgruppe.php',

    '/wackenfreunde.php',

    '/guestbook.php',

    '/feedback.php',

    '/aboutus.php',

    '/impressum.php',

    '/datenschutz.php',

    '/jugendschutz.php'
];


foreach (
    $staticPages
    as $path
) {

    sitemapWriteUrl(
        $baseUrl
        .
        $path
    );
}


/*
|--------------------------------------------------------------------------
| Öffentliche Community-Profile
|--------------------------------------------------------------------------
|
| Gleicher Public-Gate-Gedanke wie im restlichen 2.10-System.
|
| Einträge müssen:
|
| - public
| - approved
| - Standort anonymisiert
|
| sein.
|--------------------------------------------------------------------------
*/

try {

    $markerStmt =
        $pdo->query(
            "
            SELECT
                id,
                created_at

            FROM markers

            WHERE visibility = 'public'

              AND status = 'approved'

              AND location_anonymized = 1

            ORDER BY
                id ASC
            "
        );


    while (
        $marker =
            $markerStmt->fetch(
                PDO::FETCH_ASSOC
            )
    ) {

        $id =
            (int) (
                $marker['id']
                ??
                0
            );


        if ($id <= 0) {
            continue;
        }


        $lastmod =
            sitemapDate(
                $marker['created_at']
                ??
                null
            );


        sitemapWriteUrl(
            $baseUrl
            .
            '/eintrag.php?id='
            .
            $id,
            $lastmod
        );
    }


} catch (Throwable $e) {

    if (
        function_exists('appLog')
    ) {

        appLog(
            'warning',
            'Sitemap konnte Community-Profile nicht laden.',
            [
                'exception' =>
                    $e->getMessage()
            ]
        );
    }
}


/*
|--------------------------------------------------------------------------
| Öffentliche Events
|--------------------------------------------------------------------------
|
| Nur:
|
| - visibility = public
| - status = active
| - laufend oder zukünftig
|
| Private, gelöschte und abgesagte Events werden nicht aufgenommen.
|--------------------------------------------------------------------------
*/

try {

    $eventStmt =
        $pdo->query(
            "
            SELECT
                id,
                event_date,
                event_end_date,
                recurrence_type,
                nth_week,
                weekday,
                month_filter,
                recurrence_count

            FROM events

            WHERE visibility = 'public'

              AND status = 'active'

              AND (
                    COALESCE(event_end_date, event_date) >= CURDATE()
                    OR recurrence_type IN ('weekly', 'biweekly', 'monthly')
                  )

            ORDER BY
                event_date ASC,
                id ASC
            "
        );


    $today =
        new DateTimeImmutable(
            'today'
        );


    while (
        $event =
            $eventStmt->fetch(
                PDO::FETCH_ASSOC
            )
    ) {

        $id =
            (int) (
                $event['id']
                ??
                0
            );


        if ($id <= 0) {
            continue;
        }


        $baseDate =
            trim(
                (string) (
                    $event['event_date']
                    ??
                    ''
                )
            );


        if (
            preg_match(
                '/^\d{4}-\d{2}-\d{2}$/',
                $baseDate
            )
            !== 1
        ) {
            continue;
        }


        try {

            $baseStart =
                new DateTimeImmutable(
                    $baseDate
                );


            $baseEndDate =
                trim(
                    (string) (
                        $event['event_end_date']
                        ??
                        ''
                    )
                );


            $baseEnd =
                $baseEndDate !== ''
                    ? new DateTimeImmutable(
                        $baseEndDate
                    )
                    : $baseStart;


            if ($baseEnd < $baseStart) {
                $baseEnd = $baseStart;
            }


            $durationDays =
                (int)
                $baseStart
                    ->diff(
                        $baseEnd
                    )
                    ->days;


        } catch (Throwable) {

            continue;
        }


        $isRecurring =
            eventRecurrenceType(
                $event
            )
            !== 'none';


        if ($isRecurring) {

            /*
             * Bei mehrtägigen Serien kann eine Wiederholung bereits laufen.
             * Deshalb wird die Suche um die Eventdauer zurückgesetzt.
             */

            $referenceDate =
                $durationDays > 0
                    ? $today->modify(
                        '-' . $durationDays . ' days'
                    )
                    : $today;


            $occurrenceDate =
                eventRecurrenceNextDate(
                    $event,
                    $referenceDate
                );


            if ($occurrenceDate === null) {
                continue;
            }


            try {

                $occurrenceStart =
                    new DateTimeImmutable(
                        $occurrenceDate
                    );


                $occurrenceEnd =
                    $occurrenceStart->modify(
                        '+' . $durationDays . ' days'
                    );


            } catch (Throwable) {

                continue;
            }


            if ($occurrenceEnd < $today) {
                continue;
            }


        } elseif ($baseEnd < $today) {

            continue;
        }


        $lastmod =
            sitemapDate(
                $event['event_date']
                ??
                null
            );


        /*
         * Event-Detailseiten verwenden für Serien bewusst dieselbe
         * kanonische Basis-URL. occurrence= ist eine Terminansicht,
         * kein eigener kanonischer Sitemap-Eintrag.
         */

        sitemapWriteUrl(
            $baseUrl
            .
            '/event.php?id='
            .
            $id,
            $lastmod
        );
    }


} catch (Throwable $e) {

    if (
        function_exists('appLog')
    ) {

        appLog(
            'warning',
            'Sitemap konnte Events nicht laden.',
            [
                'exception' =>
                    $e->getMessage()
            ]
        );
    }
}


/*
|--------------------------------------------------------------------------
| Ende
|--------------------------------------------------------------------------
*/

echo "</urlset>\n";