<?php
defined('ABSPATH') || exit;

class Termin_Kalender_Event_Repository
{
    private static $table_name = 'termin_kalender_events';

    public static function get_table_name()
    {
        global $wpdb;
        return $wpdb->prefix . self::$table_name;
    }

    /**
     * Reads the db_fallback_mode flag from the central options array.
     * Returns null when the key has never been set (flag not yet determined).
     */
    private static function get_fallback_flag(): ?bool
    {
        $options = get_option('termin_kalender_options', []);
        if (!isset($options['db_fallback_mode'])) {
            return null;
        }
        return (bool) $options['db_fallback_mode'];
    }

    /**
     * Writes the db_fallback_mode flag into the central options array.
     */
    private static function set_fallback_flag(bool $value): void
    {
        $options = get_option('termin_kalender_options', []);
        $options['db_fallback_mode'] = $value;
        update_option('termin_kalender_options', $options);
    }

    public static function table_exists(): bool
    {
        global $wpdb;
        $table = self::get_table_name();
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        return $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) === $table;
    }

    /**
     * Returns true when the custom table cannot be used and the options
     * fallback must be used instead. Two cases:
     *  1. is_legacy_mode(): PRO plugin is older than the table-based version
     *  2. The table was never created because the server does not allow it
     *
     * This is intentionally private — callers use Repository methods only.
     */
    private static function table_not_available(): bool
    {
        if (self::is_legacy_mode()) {
            return true;
        }

        // Use the flag persisted by create_table() to avoid a SHOW TABLES on every request.
        $fallback_flag = self::get_fallback_flag();
        if ($fallback_flag !== null) {
            return $fallback_flag;
        }

        // Flag is null: create_table() has not run yet (e.g. edge-case where the
        // activation hook did not fire). Do a live check but only cache the POSITIVE
        // result (table exists). Never write true here — only create_table() is
        // authoritative about whether the server restricts table creation.
        $exists = self::table_exists();
        if ($exists) {
            self::set_fallback_flag(false);
            return false;
        }

        // Table does not exist and flag was never set — gracefully degrade without
        // locking into fallback mode permanently.
        return true;
    }

    /**
     * Legacy-mode check: returns true when the PRO plugin is too old to
     * support the custom table. Kept unchanged for backward compatibility.
     */
    public static function is_legacy_mode()
    {
        if (defined('Termin_Kalender_PRO_Plugin::TER_KAL_PRO_TK_VERSION')) {
            // Wenn PRO Plugin installiert ist, prüfe ob es älter als die Version mit der Table-Anbindung ist
            if (version_compare(Termin_Kalender_PRO_Plugin::TER_KAL_PRO_TK_VERSION, '1.2.6.4', '<')) {
                return true;
            }
        }
        return false;
    }

    /**
     * Creates the custom table via dbDelta and sets the fallback mode flag
     * depending on whether creation actually succeeded.
     *
     * @return bool true if the table exists after the attempt.
     */
    public static function create_table(): bool
    {
        global $wpdb;
        $table_name      = self::get_table_name();
        $charset_collate = $wpdb->get_charset_collate();

        $sql = "CREATE TABLE $table_name (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            event_id varchar(100) NOT NULL,
            start_date datetime DEFAULT NULL,
            end_date datetime DEFAULT NULL,
            event_data longtext NOT NULL,
            PRIMARY KEY  (id),
            UNIQUE KEY event_id (event_id),
            KEY start_date (start_date),
            KEY end_date (end_date)
        ) $charset_collate;";

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta($sql);

        $success = self::table_exists();
        // Persist the result so table_not_available() can use it cheaply.
        self::set_fallback_flag(!$success);
        return $success;
    }

    /* ── Read ─────────────────────────────────────────────────────────────── */

    public static function get_events($include_archive = 0)
    {
        if (self::table_not_available()) {
            return json_decode(get_option('termin_kalender_events', '[]'), true) ?: [];
        }

        global $wpdb;
        $table_name = esc_sql(self::get_table_name());

        $wpdb->suppress_errors(true);
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

        $query = "SELECT event_data FROM $table_name WHERE start_date IS NOT NULL";

        if ($include_archive) {
            $archive_table = esc_sql($table_name . '_archive');
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$archive_table'");
            if ($table_exists === $archive_table) {
                $query = "(SELECT event_data FROM $table_name WHERE start_date IS NOT NULL) UNION ALL (SELECT event_data FROM $archive_table WHERE start_date IS NOT NULL)";
            }
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $results = $wpdb->get_results($query, ARRAY_A);
        $wpdb->suppress_errors(false);

        $events = [];
        if (!empty($results)) {
            foreach ($results as $row) {
                $decoded = json_decode($row['event_data'], true);
                if ($decoded) {
                    $events[] = $decoded;
                }
            }
        }
        return $events;
    }

    public static function get_event($event_id)
    {
        if (self::table_not_available()) {
            $events = json_decode(get_option('termin_kalender_events', '[]'), true) ?: [];
            foreach ($events as $e) {
                if (isset($e['id']) && $e['id'] === $event_id) {
                    return $e;
                }
            }
            return null;
        }

        global $wpdb;
        $table_name = esc_sql(self::get_table_name());

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $row = $wpdb->get_row(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $wpdb->prepare("SELECT event_data FROM $table_name WHERE event_id = %s", $event_id),
            ARRAY_A
        );

        if ($row && !empty($row['event_data'])) {
            return json_decode($row['event_data'], true);
        }
        return null;
    }

    /**
     * Returns all events whose event_id starts with the given prefix.
     * Used by todo and teammessage modules instead of direct $wpdb queries.
     * Falls back to filtering the options JSON when the table is unavailable.
     *
     * @param string $prefix e.g. 'todo-' or 'teammessage-'
     * @return array
     */
    public static function get_events_by_prefix(string $prefix): array
    {
        if (self::table_not_available()) {
            $all = json_decode(get_option('termin_kalender_events', '[]'), true) ?: [];
            return array_values(array_filter($all, function ($e) use ($prefix) {
                return isset($e['id']) && str_starts_with((string) $e['id'], $prefix);
            }));
        }

        global $wpdb;
        $table_name = esc_sql(self::get_table_name());
        $like       = $wpdb->esc_like($prefix) . '%';

        $wpdb->suppress_errors(true);
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $results = $wpdb->get_results(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $wpdb->prepare("SELECT event_data FROM $table_name WHERE event_id LIKE %s", $like),
            ARRAY_A
        );
        $wpdb->suppress_errors(false);

        $events = [];
        if (!empty($results)) {
            foreach ($results as $row) {
                $decoded = json_decode($row['event_data'], true);
                if ($decoded) {
                    $events[] = $decoded;
                }
            }
        }
        return $events;
    }

    /* ── Write ────────────────────────────────────────────────────────────── */

    public static function save_event($event)
    {
        $event_id = isset($event['id']) ? $event['id'] : '';
        if (empty($event_id)) {
            return false;
        }

        if (self::table_not_available()) {
            $events = json_decode(get_option('termin_kalender_events', '[]'), true) ?: [];
            $found = false;
            foreach ($events as &$e) {
                if (isset($e['id']) && $e['id'] === $event_id) {
                    $e = array_merge($e, $event);
                    $found = true;
                    break;
                }
            }
            unset($e);
            if (!$found) {
                $events[] = $event;
            }
            update_option('termin_kalender_events', wp_json_encode(array_values($events)), false);
            return true;
        }

        global $wpdb;
        $table_name = esc_sql(self::get_table_name());

        $start_date = null;
        if (!empty($event['start'])) {
            $start_date = gmdate('Y-m-d H:i:s', strtotime($event['start']));
        } elseif (!empty($event['extendedProps']['dtstart'])) {
            $start_date = gmdate('Y-m-d H:i:s', strtotime($event['extendedProps']['dtstart']));
        }

        $end_date = null;
        if (!empty($event['end'])) {
            $end_date = gmdate('Y-m-d H:i:s', strtotime($event['end']));
        } elseif (!empty($event['extendedProps']['until'])) {
            $end_date = gmdate('Y-m-d H:i:s', strtotime($event['extendedProps']['until']));
        } else {
            $end_date = $start_date;
        }

        $event_data = wp_json_encode($event);

        // Upsert operation
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $wpdb->query(
            $wpdb->prepare(
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                "INSERT INTO $table_name (event_id, start_date, end_date, event_data) 
                 VALUES (%s, %s, %s, %s) 
                 ON DUPLICATE KEY UPDATE start_date = VALUES(start_date), end_date = VALUES(end_date), event_data = VALUES(event_data)",
                $event_id,
                $start_date,
                $end_date,
                $event_data
            )
        );

        return true;
    }

    public static function delete_event($event_id)
    {
        if (self::table_not_available()) {
            $events = json_decode(get_option('termin_kalender_events', '[]'), true) ?: [];
            $events = array_filter($events, function ($e) use ($event_id) {
                return isset($e['id']) && $e['id'] !== $event_id;
            });
            update_option('termin_kalender_events', wp_json_encode(array_values($events)), false);
            return;
        }

        global $wpdb;
        $table_name = esc_sql(self::get_table_name());

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $wpdb->query(
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $wpdb->prepare("DELETE FROM $table_name WHERE event_id = %s", $event_id)
        );
    }

    /**
     * Deletes all events whose event_id starts with the given prefix.
     * Used by the backup restore feature. Falls back to filtering the options JSON.
     *
     * @param string $prefix e.g. 'todo-' or 'teammessage-'
     */
    public static function delete_by_prefix(string $prefix): void
    {
        if (self::table_not_available()) {
            $all      = json_decode(get_option('termin_kalender_events', '[]'), true) ?: [];
            $filtered = array_values(array_filter($all, function ($e) use ($prefix) {
                return !(isset($e['id']) && str_starts_with((string) $e['id'], $prefix));
            }));
            update_option('termin_kalender_events', wp_json_encode($filtered), false);
            return;
        }

        global $wpdb;
        $table_name = esc_sql(self::get_table_name());
        $like       = $wpdb->esc_like($prefix) . '%';

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $wpdb->query($wpdb->prepare("DELETE FROM $table_name WHERE event_id LIKE %s", $like));
    }

    /**
     * Deletes all events EXCEPT those whose event_id starts with one of the
     * given prefixes. Used by backup restore to clear regular events while
     * preserving todos and teammessages.
     *
     * @param string[] $excluded_prefixes e.g. ['todo-', 'teammessage-']
     */
    public static function delete_except_prefixes(array $excluded_prefixes): void
    {
        if (self::table_not_available()) {
            $all      = json_decode(get_option('termin_kalender_events', '[]'), true) ?: [];
            $filtered = array_values(array_filter($all, function ($e) use ($excluded_prefixes) {
                $id = (string) ($e['id'] ?? '');
                foreach ($excluded_prefixes as $prefix) {
                    if (str_starts_with($id, $prefix)) {
                        return true; // keep
                    }
                }
                return false; // remove
            }));
            update_option('termin_kalender_events', wp_json_encode($filtered), false);
            return;
        }

        global $wpdb;
        $table_name = esc_sql(self::get_table_name());

        $conditions = [];
        $values     = [];
        foreach ($excluded_prefixes as $prefix) {
            $conditions[] = 'event_id NOT LIKE %s';
            $values[]     = $wpdb->esc_like($prefix) . '%';
        }
        $where = implode(' AND ', $conditions);

        // $where is built exclusively from the hard-coded literal 'event_id NOT LIKE %s' — safe.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $wpdb->query($wpdb->prepare("DELETE FROM $table_name WHERE $where", ...$values));
    }

    /**
     * Returns the total number of rows in the events table (all types combined).
     * Returns 0 when the table is unavailable (archive badge is irrelevant in fallback mode).
     */
    public static function count_events(): int
    {
        if (self::table_not_available()) {
            return 0;
        }

        global $wpdb;
        $table_name = esc_sql(self::get_table_name());

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        return (int) $wpdb->get_var("SELECT COUNT(id) FROM $table_name");
    }

    /**
     * Returns the oldest calendar year present in the events table (past years only).
     * Returns null when the table is unavailable — archiving is not supported in fallback mode.
     */
    public static function get_oldest_year(): ?int
    {
        if (self::table_not_available()) {
            return null;
        }

        global $wpdb;
        $table        = esc_sql(self::get_table_name());
        $current_year = (int) current_time('Y');

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
        $year = (int) $wpdb->get_var(
            $wpdb->prepare(
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                "SELECT MIN(YEAR(start_date)) FROM $table WHERE YEAR(start_date) > 2000 AND YEAR(start_date) < %d AND start_date IS NOT NULL",
                $current_year
            )
        );
        return $year > 0 ? $year : null;
    }

    /* ── Migration ────────────────────────────────────────────────────────── */

    public static function migrate_from_option()
    {
        if (self::table_not_available()) {
            // Table does not exist (server restriction or legacy PRO).
            // Do not attempt migration — data stays in the option.
            return;
        }

        $old_events = get_option('termin_kalender_events', null);

        // Wenn es keinen alten Blob gibt (oder er bereits gemoved wurde), nichts tun.
        if ($old_events === null) {
            return;
        }

        self::create_table();

        $events_array = [];
        if (is_string($old_events)) {
            $events_array = json_decode($old_events, true);
        } elseif (is_array($old_events)) {
            $events_array = $old_events;
        }

        if (!empty($events_array) && is_array($events_array)) {
            foreach ($events_array as $event) {
                // Nur speichern wenn event gültig ist
                if (isset($event['id'])) {
                    self::save_event($event);
                }
            }
        }

        // Rename original option for safety backup (prevent re-running)
        update_option('_termin_kalender_events_backup', $old_events, false);
        delete_option('termin_kalender_events');
    }
}

// WPCS: Dies ist eine zwingende Abwärtskompatibilitäts-Brücke für das PRO Plugin.
// WPCS: Wird nach vollständiger Umstellung aller PRO-Kunden irgendwann deprecated.
class_alias('Termin_Kalender_Event_Repository', 'TK_Event_Repository');
