<?php
/**
 * Upgrade snapshots, migration status, rollback, and privacy-safe diagnostics.
 */
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- Upgrade verification and rollback operate on fixed plugin-owned tables and must inspect live schema/data rather than cached values.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class YATOON_Release_Safety {
    const BACKUPS_OPTION = 'yatoon_upgrade_backups_v1';
    const STATUS_OPTION  = 'yatoon_migration_status_v1';

    public static function bootstrap() {
        add_action( 'plugins_loaded', array( __CLASS__, 'prepare_upgrade' ), 4 );
        add_action( 'plugins_loaded', array( __CLASS__, 'mark_upgrade_complete' ), 99 );
        add_action( 'admin_post_yatoon_create_backup', array( __CLASS__, 'handle_create_backup' ) );
        add_action( 'admin_post_yatoon_restore_backup', array( __CLASS__, 'handle_restore_backup' ) );
        add_action( 'admin_post_yatoon_export_diagnostics', array( __CLASS__, 'export_diagnostics' ) );
    }

    public static function prepare_upgrade() {
        $installed = (string) get_option( 'yatoon_schema_version', '' );
        if ( '' === $installed || ! version_compare( $installed, YATOON_DB_VERSION, '<' ) ) {
            return;
        }
        $status = (array) get_option( self::STATUS_OPTION, array() );
        if ( ( $status['target'] ?? '' ) === YATOON_DB_VERSION && in_array( $status['status'] ?? '', array( 'backup_ready', 'complete' ), true ) ) {
            return;
        }
        update_option( self::STATUS_OPTION, array(
            'status'  => 'preparing_backup',
            'from'    => $installed,
            'target'  => YATOON_DB_VERSION,
            'started' => current_time( 'mysql' ),
        ), false );
        $result = self::create_backup( 'automatic-upgrade' );
        update_option( self::STATUS_OPTION, array(
            'status'  => is_wp_error( $result ) ? 'backup_failed' : 'backup_ready',
            'from'    => $installed,
            'target'  => YATOON_DB_VERSION,
            'backup'  => is_wp_error( $result ) ? '' : $result,
            'error'   => is_wp_error( $result ) ? $result->get_error_message() : '',
            'started' => current_time( 'mysql' ),
        ), false );
    }

    public static function mark_upgrade_complete() {
        $status = (array) get_option( self::STATUS_OPTION, array() );
        if ( ( $status['target'] ?? '' ) !== YATOON_DB_VERSION || 'complete' === ( $status['status'] ?? '' ) ) {
            return;
        }
        if ( (string) get_option( 'yatoon_schema_version', '' ) === YATOON_DB_VERSION ) {
            $status['status'] = 'complete';
            $status['completed'] = current_time( 'mysql' );
            update_option( self::STATUS_OPTION, $status, false );
        }
    }

    private static function core_tables() {
        global $wpdb;
        $like   = $wpdb->esc_like( $wpdb->prefix . 'yatoon_' ) . '%';
        $tables = (array) $wpdb->get_col( $wpdb->prepare( 'SHOW TABLES LIKE %s', $like ) );
        $short  = array();
        foreach ( $tables as $table ) {
            if ( 0 !== strpos( $table, $wpdb->prefix ) ) {
                continue;
            }
            $name = substr( $table, strlen( $wpdb->prefix ) );
            if ( preg_match( '/^yatoon_[a-z0-9_]+$/', $name ) ) {
                $short[] = $name;
            }
        }
        sort( $short, SORT_STRING );
        return array_values( array_unique( $short ) );
    }

    public static function create_backup( $reason = 'manual' ) {
        global $wpdb;
        $stamp = gmdate( 'Ymd_His' );
        $manifest = array(
            'id'      => $stamp,
            'version' => (string) get_option( 'yatoon_schema_version', YATOON_VERSION ),
            'reason'  => sanitize_key( $reason ),
            'created' => current_time( 'mysql' ),
            'tables'  => array(),
        );
        foreach ( self::core_tables() as $short ) {
            $source = $wpdb->prefix . $short;
            if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $source ) ) !== $source ) {
                continue;
            }
            $suffix = substr( preg_replace( '/^yatoon_/', '', $short ), 0, 14 ) . '_' . substr( md5( $short ), 0, 7 );
            $backup = substr( $wpdb->prefix . 'ytnbak_' . $stamp . '_' . $suffix, 0, 64 );
            if ( false === $wpdb->query( "CREATE TABLE `{$backup}` LIKE `{$source}`" ) ) {
                self::drop_manifest_tables( $manifest );
            return new WP_Error( 'backup_schema_failed', sprintf( __( 'Could not create backup table for %s.', 'yatoon-booking-system' ), $short ) );
            }
            $columns = self::writable_columns( $source );
            if ( empty( $columns ) ) {
                $wpdb->query( "DROP TABLE IF EXISTS `{$backup}`" );
                self::drop_manifest_tables( $manifest );
            return new WP_Error( 'backup_columns_failed', sprintf( __( 'Could not inspect backup columns for %s.', 'yatoon-booking-system' ), $short ) );
            }
            $column_sql = '`' . implode( '`,`', $columns ) . '`';
            if ( false === $wpdb->query( "INSERT INTO `{$backup}` ({$column_sql}) SELECT {$column_sql} FROM `{$source}`" ) ) {
                $wpdb->query( "DROP TABLE IF EXISTS `{$backup}`" );
                self::drop_manifest_tables( $manifest );
            return new WP_Error( 'backup_copy_failed', sprintf( __( 'Could not copy backup data for %s.', 'yatoon-booking-system' ), $short ) );
            }
            $manifest['tables'][ $short ] = $backup;
        }
        $backups = (array) get_option( self::BACKUPS_OPTION, array() );
        $backups[ $stamp ] = $manifest;
        while ( count( $backups ) > 3 ) {
            $old = array_shift( $backups );
            self::drop_manifest_tables( (array) $old );
        }
        update_option( self::BACKUPS_OPTION, $backups, false );
        return $stamp;
    }

    /**
     * Return columns that may be supplied in INSERT statements.
     *
     * MySQL rejects explicit values for generated columns, including values
     * copied from an identical table created with CREATE TABLE ... LIKE.
     */
    private static function writable_columns( $table ) {
        global $wpdb;
        if ( ! preg_match( '/^[A-Za-z0-9_]+$/', $table ) ) {
            return array();
        }
        $columns = array();
        foreach ( (array) $wpdb->get_results( "SHOW FULL COLUMNS FROM `{$table}`" ) as $column ) {
            $name  = sanitize_key( $column->Field ?? '' );
            $extra = strtoupper( (string) ( $column->Extra ?? '' ) );
            if ( $name && false === strpos( $extra, 'GENERATED' ) ) {
                $columns[] = $name;
            }
        }
        return $columns;
    }

    private static function drop_manifest_tables( array $manifest ) {
        global $wpdb;
        foreach ( (array) ( $manifest['tables'] ?? array() ) as $table ) {
            if ( preg_match( '/^[A-Za-z0-9_]+$/', $table ) && strpos( $table, $wpdb->prefix . 'ytnbak_' ) === 0 ) {
                $wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
            }
        }
    }

    private static function require_admin( $nonce_action ) {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( esc_html__( 'You do not have permission to manage backups.', 'yatoon-booking-system' ) );
        }
        check_admin_referer( $nonce_action );
    }

    public static function handle_create_backup() {
        self::require_admin( 'yatoon_create_backup' );
        $result = self::create_backup( 'manual' );
        self::redirect( is_wp_error( $result ) ? 'backup_failed' : 'backup_created' );
    }

    public static function handle_restore_backup() {
        self::require_admin( 'yatoon_restore_backup' );
        $id = sanitize_key( wp_unslash( $_POST['backup_id'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- require_admin() verifies the restore action immediately above.
        $backups = (array) get_option( self::BACKUPS_OPTION, array() );
        if ( empty( $backups[ $id ]['tables'] ) ) {
            self::redirect( 'backup_missing' );
        }
        global $wpdb;
        foreach ( $backups[ $id ]['tables'] as $short => $backup ) {
            $live = $wpdb->prefix . sanitize_key( $short );
            if ( ! preg_match( '/^[A-Za-z0-9_]+$/', $backup ) || strpos( $backup, $wpdb->prefix . 'ytnbak_' ) !== 0 ) {
                continue;
            }
            // Restore only columns shared by both schemas. This keeps a backup made
            // before an upgrade usable after that upgrade adds new columns.
            $live_columns   = self::writable_columns( $live );
            $backup_columns = self::writable_columns( $backup );
            $columns        = array_values( array_intersect( $live_columns, $backup_columns ) );
            if ( empty( $columns ) ) {
                self::redirect( 'restore_failed' );
            }
            $column_sql = '`' . implode( '`,`', $columns ) . '`';
            $wpdb->query( 'START TRANSACTION' );
            $ok = false !== $wpdb->query( "DELETE FROM `{$live}`" )
                && false !== $wpdb->query( "INSERT INTO `{$live}` ({$column_sql}) SELECT {$column_sql} FROM `{$backup}`" );
            $wpdb->query( $ok ? 'COMMIT' : 'ROLLBACK' );
            if ( ! $ok ) {
                self::redirect( 'restore_failed' );
            }
        }
        update_option( self::STATUS_OPTION, array(
            'status'    => 'restored',
            'backup'    => $id,
            'completed' => current_time( 'mysql' ),
        ), false );
        self::redirect( 'restored' );
    }

    private static function redirect( $notice ) {
        wp_safe_redirect( add_query_arg( array(
            'page' => 'yatoon-operations', 'tab' => 'reliability', 'yatoon_notice' => sanitize_key( $notice ),
        ), admin_url( 'admin.php' ) ) );
        exit;
    }

    public static function diagnostic_payload() {
        global $wpdb, $wp_version;
        $counts = array();
        foreach ( self::core_tables() as $short ) {
            $table = $wpdb->prefix . $short;
            if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table ) {
                $counts[ $short ] = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" );
            }
        }
        $theme = wp_get_theme();
        return array(
            'generated_at'     => gmdate( 'c' ),
            'site'             => wp_parse_url( home_url(), PHP_URL_HOST ),
            'wordpress'        => $wp_version,
            'php'              => PHP_VERSION,
            'plugin'           => YATOON_VERSION,
            'package'          => defined( 'YATOON_PACKAGE_TIER' ) ? YATOON_PACKAGE_TIER : 'unknown',
            'database'         => (string) get_option( 'yatoon_schema_version', '' ),
            'platform'         => sanitize_key( get_option( 'yatoon_active_platform', 'local' ) ),
            'timezone'         => yatoon_timezone_string(),
            'theme'            => array( 'name' => $theme->get( 'Name' ), 'version' => $theme->get( 'Version' ) ),
            'health'           => class_exists( 'YATOON_Operations' ) ? YATOON_Operations::health_checks() : array(),
            'migration'        => get_option( self::STATUS_OPTION, array() ),
            'backups'          => array_values( array_map( static function ( $backup ) {
                return array_intersect_key( (array) $backup, array_flip( array( 'id', 'version', 'reason', 'created' ) ) );
            }, (array) get_option( self::BACKUPS_OPTION, array() ) ) ),
            'square_sync'      => get_option( 'yatoon_square_sync_health', array() ),
            'square_webhooks'  => array_slice( (array) get_option( 'yatoon_square_webhook_events', array() ), -25, null, true ),
            'sync_logs'        => array_slice( (array) get_option( 'yatoon_sync_logs', array() ), -25 ),
            'scheduled'        => array(
                'booking_sync' => wp_next_scheduled( 'yatoon_sync_square_bookings_only' ),
                'reminders'    => wp_next_scheduled( 'yatoon_send_reminders' ),
                'waitlist'     => wp_next_scheduled( 'yatoon_check_waitlist_availability' ),
            ),
            'table_counts'     => $counts,
        );
    }

    public static function export_diagnostics() {
        self::require_admin( 'yatoon_export_diagnostics' );
        nocache_headers();
        header( 'Content-Type: application/json; charset=utf-8' );
        header( 'Content-Disposition: attachment; filename="yatoon-diagnostics-' . gmdate( 'Ymd-His' ) . '.json"' );
        echo wp_json_encode( self::diagnostic_payload(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
        exit;
    }
}

YATOON_Release_Safety::bootstrap();
