<?php

declare(strict_types=1);

namespace CalendarPlugin\Infrastructure\WordPress;

use CalendarPlugin\Contracts\DatabaseAdapterInterface;
use DateTimeImmutable;
use DateTimeZone;

final class MigrationManager
{
    private const SCHEMA_VERSION = '5';
    private const STEM_OPTION = 'calendar_plugin_table_stem';

    public function __construct(private readonly DatabaseAdapterInterface $db, private readonly string $tableStem = 'cs_calendar')
    {
    }

    public function migrate(): void
    {
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';

        global $wpdb;
        $charsetCollate = $wpdb->get_charset_collate();
        $prefix = $this->db->getPrefix();
        $stem = trim($this->tableStem, '_');

        $events = $prefix . $stem . '_events';
        $exceptions = $prefix . $stem . '_recurrence_exceptions';
        $users = $prefix . $stem . '_users';
        $tokens = $prefix . $stem . '_user_tokens';
        $audit = $prefix . $stem . '_audit_log';
        $tombstones = $prefix . $stem . '_caldav_tombstones';

        $sqlEvents = "CREATE TABLE {$events} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            uid VARCHAR(191) NOT NULL,
            visibility VARCHAR(16) NOT NULL DEFAULT 'public',
            title TEXT NOT NULL,
            description LONGTEXT NOT NULL,
            location TEXT NOT NULL,
            category TEXT NOT NULL,
            all_day_event TINYINT(1) NOT NULL DEFAULT 0,
            start_datetime VARCHAR(64) NOT NULL,
            end_datetime VARCHAR(64) NOT NULL,
            repeat_type VARCHAR(24) NOT NULL DEFAULT 'none',
            repeat_interval INT NOT NULL DEFAULT 1,
            repeat_nth_mode VARCHAR(32) NOT NULL DEFAULT '',
            repeat_nth_day INT NULL,
            repeat_nth_pos INT NULL,
            repeat_nth_weekday INT NULL,
            repeat_range_mode VARCHAR(24) NOT NULL DEFAULT 'none',
            repeat_count INT NULL,
            repeat_until VARCHAR(16) NULL,
            timezone VARCHAR(64) NOT NULL DEFAULT 'Europe/London',
            caldav_resource VARCHAR(191) NULL,
            etag VARCHAR(64) NULL,
            sync_version INT NOT NULL DEFAULT 1,
            last_modified_by_user_id BIGINT UNSIGNED NULL,
            created_at VARCHAR(32) NOT NULL,
            updated_at VARCHAR(32) NOT NULL,
            PRIMARY KEY (id),
            UNIQUE KEY uid (uid(191)),
            UNIQUE KEY caldav_resource (caldav_resource),
            KEY start_datetime (start_datetime(32)),
            KEY end_datetime (end_datetime(32))
        ) {$charsetCollate};";

        $sqlExceptions = "CREATE TABLE {$exceptions} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            event_id BIGINT UNSIGNED NOT NULL,
            occurrence_key VARCHAR(64) NOT NULL,
            exception_type VARCHAR(32) NOT NULL,
            created_at VARCHAR(32) NOT NULL,
            updated_at VARCHAR(32) NOT NULL,
            PRIMARY KEY (id),
            UNIQUE KEY event_occurrence (event_id, occurrence_key),
            KEY event_id (event_id)
        ) {$charsetCollate};";

        $sqlUsers = "CREATE TABLE {$users} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            email VARCHAR(191) NOT NULL,
            password_hash VARCHAR(255) NOT NULL,
            email_verified_at VARCHAR(32) NULL,
            account_status VARCHAR(32) NOT NULL DEFAULT 'pending_approval',
            created_at VARCHAR(32) NOT NULL,
            updated_at VARCHAR(32) NOT NULL,
            PRIMARY KEY (id),
            UNIQUE KEY email (email)
        ) {$charsetCollate};";

        $sqlTokens = "CREATE TABLE {$tokens} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            user_id BIGINT UNSIGNED NOT NULL,
            token_type VARCHAR(32) NOT NULL,
            token_hash VARCHAR(255) NOT NULL,
            expires_at VARCHAR(32) NOT NULL,
            used_at VARCHAR(32) NULL,
            created_at VARCHAR(32) NOT NULL,
            PRIMARY KEY (id),
            KEY user_id (user_id),
            KEY token_type (token_type)
        ) {$charsetCollate};";

        $sqlAudit = "CREATE TABLE {$audit} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            actor VARCHAR(191) NOT NULL,
            action VARCHAR(191) NOT NULL,
            target VARCHAR(191) NOT NULL,
            result VARCHAR(32) NOT NULL,
            created_at VARCHAR(32) NOT NULL,
            context_json LONGTEXT NULL,
            PRIMARY KEY (id),
            KEY action (action),
            KEY created_at (created_at)
        ) {$charsetCollate};";

        $sqlTombstones = "CREATE TABLE {$tombstones} (
            resource VARCHAR(191) NOT NULL,
            deleted_at VARCHAR(32) NOT NULL,
            PRIMARY KEY (resource)
        ) {$charsetCollate};";

        dbDelta($sqlEvents);
        dbDelta($sqlExceptions);
        dbDelta($sqlUsers);
        dbDelta($sqlTokens);
        dbDelta($sqlAudit);
        dbDelta($sqlTombstones);

        // Ensure every event has a stable CalDAV object resource name.
        $this->db->query(
            "UPDATE {$events}
             SET caldav_resource = CONCAT(uid, '.ics')
             WHERE (caldav_resource IS NULL OR caldav_resource = '')
               AND uid IS NOT NULL
               AND uid <> ''"
        );
        $this->db->query(
            "UPDATE {$events}
             SET visibility = 'public'
             WHERE visibility IS NULL
                OR visibility = ''
                OR visibility NOT IN ('public', 'private')"
        );
        $this->normalizeEventDateTimesToLondon($events);

        update_option(self::STEM_OPTION, $stem);
        update_option('calendar_plugin_schema_version', self::SCHEMA_VERSION);
    }

    public function assertActivationSafe(): void
    {
        $stem = trim($this->tableStem, '_');
        $ownedStem = trim((string) get_option(self::STEM_OPTION, ''), '_');
        if ($ownedStem !== '' && $ownedStem !== $stem) {
            throw new \RuntimeException(
                sprintf(
                    'Calendar Plugin is already initialized with table stem "%s". Requested stem "%s" is different.',
                    $ownedStem,
                    $stem
                )
            );
        }
        if ($ownedStem !== $stem && $this->anyTargetTablesExist($stem)) {
            $legacySchemaVersion = trim((string) get_option('calendar_plugin_schema_version', ''));
            if ($legacySchemaVersion === '') {
                throw new \RuntimeException(
                    sprintf(
                        'Calendar Plugin activation blocked: target tables for stem "%s" already exist. Choose another table stem via CALENDAR_PLUGIN_TABLE_STEM.',
                        $stem
                    )
                );
            }
        }
    }

    private function anyTargetTablesExist(string $stem): bool
    {
        $prefix = $this->db->getPrefix();
        $tables = [
            $prefix . $stem . '_events',
            $prefix . $stem . '_recurrence_exceptions',
            $prefix . $stem . '_users',
            $prefix . $stem . '_user_tokens',
            $prefix . $stem . '_audit_log',
            $prefix . $stem . '_caldav_tombstones',
        ];
        foreach ($tables as $table) {
            $sql = $this->db->prepare('SHOW TABLES LIKE %s', $table);
            if (count($this->db->getResults($sql)) > 0) {
                return true;
            }
        }
        return false;
    }

    private function normalizeEventDateTimesToLondon(string $eventsTable): void
    {
        $rows = $this->db->getResults(
            "SELECT id, start_datetime, end_datetime FROM {$eventsTable}"
        );
        $tz = new DateTimeZone('Europe/London');
        foreach ($rows as $row) {
            $id = (int) ($row->id ?? 0);
            if ($id <= 0) {
                continue;
            }
            $start = $this->normalizeDateTimeString((string) ($row->start_datetime ?? ''), $tz);
            $end = $this->normalizeDateTimeString((string) ($row->end_datetime ?? ''), $tz);
            if ($start === null || $end === null) {
                continue;
            }
            $sql = $this->db->prepare(
                "UPDATE {$eventsTable} SET start_datetime = %s, end_datetime = %s WHERE id = %d",
                $start,
                $end,
                $id
            );
            $this->db->query($sql);
        }
    }

    private function normalizeDateTimeString(string $value, DateTimeZone $tz): ?string
    {
        $value = trim($value);
        if ($value === '') {
            return null;
        }
        try {
            $dt = new DateTimeImmutable($value, $tz);
            return $dt->setTimezone($tz)->format('c');
        } catch (\Throwable) {
            return null;
        }
    }
}
