<?php
/**
 * DatabaseSchema — Dynamic schema registry backed by module schemas.
 *
 * ARCHITECTURE CHANGE (v6.5.0)
 * ─────────────────────────────────────────────────────────────────────────────
 * The old implementation had every table definition hardcoded here, mirroring
 * (and diverging from) the actual CREATE TABLE statements in each module's
 * schema class. That created two maintenance burdens:
 *   1. Every schema change required editing both the module schema AND this file.
 *   2. The static definitions silently diverged from the real schema over time.
 *
 * The new implementation has NO hardcoded table definitions. Instead it:
 *   a) Delegates table-name resolution to DatabaseTablesModule's live registry.
 *   b) Falls back to a static name map when the registry is not yet loaded.
 *   c) Derives column info on demand from SHOW COLUMNS so there is always one
 *      source of truth: the actual database.
 *
 * COMPATIBILITY NOTE FOR DatabaseCrud + BaseTable
 * ─────────────────────────────────────────────────────────────────────────────
 * BaseTable stores $table_name = 'ultraguard_firewall_rules' (un-prefixed, no
 * wp_ prefix). get_table_name() accepts both short keys ('firewall_rules') and
 * full un-prefixed names ('ultraguard_firewall_rules') and always returns the
 * fully qualified name including $wpdb->prefix.
 *
 * @package UltraguardPro\core
 */

namespace UltraguardPro\core;

if ( ! defined( 'ABSPATH' ) ) { exit; }

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange -- Schema registry intentionally inspects and compares real DB structure.

class DatabaseSchema {

    /** @var array<string, string> Runtime cache: short_key|unprefixed_name → full_prefixed_name */
    private static array $name_cache = [];

    /** @var array<string, array<string, string>> Runtime cache: full_table_name → [col => type] */
    private static array $column_cache = [];

    /** @var bool Whether name cache has been populated from the module registry */
    private static bool $registry_loaded = false;

    // ─── Public API ───────────────────────────────────────────────────────────

    /**
     * Full prefixed table name for a given key.
     *
     * Accepts both:
     *   • Short module-registry keys  e.g. 'firewall_rules'
     *   • Un-prefixed full names      e.g. 'ultraguard_firewall_rules'
     *
     * @param  string      $table_key
     * @return string|null  Returns best-guess prefixed name; never null in practice.
     */
    public static function get_table_name( string $table_key ): ?string {
        if ( isset( self::$name_cache[ $table_key ] ) ) {
            return self::$name_cache[ $table_key ];
        }

        self::maybe_load_registry();

        if ( isset( self::$name_cache[ $table_key ] ) ) {
            return self::$name_cache[ $table_key ];
        }

        // Key is an un-prefixed full name like 'ultraguard_firewall_rules'.
        // Prepend $wpdb->prefix and cache.
        global $wpdb;
        $full = $wpdb->prefix . $table_key;
        self::$name_cache[ $table_key ] = $full;
        return $full;
    }

    /**
     * All known prefixed table names (short keys + un-prefixed full names).
     *
     * @return array<string, string>
     */
    public static function get_table_names(): array {
        self::maybe_load_registry();
        return self::$name_cache;
    }

    /**
     * Check whether a column exists in a table.
     *
     * @param  string $table_key  Short key or un-prefixed full name.
     * @param  string $column
     * @return bool
     */
    public static function column_exists( string $table_key, string $column ): bool {
        if ( empty( $column ) ) { return false; }

        $full_table = self::get_table_name( $table_key );
        if ( ! $full_table ) { return false; }

        if ( ! isset( self::$column_cache[ $full_table ] ) ) {
            self::load_columns( $full_table );
        }

        return isset( self::$column_cache[ $full_table ][ $column ] );
    }

    /**
     * Return column-definition map compatible with what DatabaseCrud expects.
     *
     * Format: [ 'columns' => [ 'col_name' => 'mysql_type_string', ... ] ]
     *
     * @param  string $table_key
     * @return array{columns: array<string, string>}
     */
    public static function get_table_definition( string $table_key ): array {
        $full_table = self::get_table_name( $table_key );
        if ( ! $full_table ) { return [ 'columns' => [] ]; }

        if ( ! isset( self::$column_cache[ $full_table ] ) ) {
            self::load_columns( $full_table );
        }

        return [ 'columns' => self::$column_cache[ $full_table ] ?? [] ];
    }

    /**
     * Invalidate all caches — called after dbDelta runs.
     */
    public static function flush_cache(): void {
        self::$name_cache      = [];
        self::$column_cache    = [];
        self::$registry_loaded = false;
    }

    // ─── Private helpers ──────────────────────────────────────────────────────

    /**
     * Populate $name_cache from the live module registry.
     *
     * Called at most once per request.  Strategy:
     *
     *   1. Ask DatabaseTablesModule for every schema it has registered.     *
     *   2. If the module registry is not available yet (very early boot),
     *      fall back to a minimal static list of ONLY core infrastructure
     *      tables owned by the db_tables module itself.
     *
     * This design means DatabaseSchema NEVER needs to know about individual
     * modules. Adding a new module with new tables requires zero changes to
     * any core file — the module registers its own schema via
     * DatabaseTablesModule::register_schema() and is picked up automatically.
     */
    private static function maybe_load_registry(): void {
        if ( self::$registry_loaded ) { return; }
        self::$registry_loaded = true; // set first to prevent recursion

        global $wpdb;

        // Path 1: live module registry (normal runtime path).
        $loaded = false;

        try {
            $plugin    = function_exists( '\\UltraguardPro\\ultraguard_pro' )
                ? \UltraguardPro\ultraguard_pro()
                : null;
            $db_tables = ( $plugin && isset( $plugin->modules ) ) ? $plugin->modules->get_db_tables() : null;

            if ( $db_tables ) {
                foreach ( $db_tables->get_registered_schemas() as $schema ) {
                    foreach ( array_keys( $schema->get_schemas() ) as $full_name ) {
                        $unprefixed = str_starts_with( $full_name, $wpdb->prefix )
                            ? substr( $full_name, strlen( $wpdb->prefix ) )
                            : '';
                        self::$name_cache[ $full_name ] = $full_name;
                        if ( $unprefixed ) {
                            self::$name_cache[ $unprefixed ] = $full_name;
                        }
                    }
                    foreach ( $schema->get_table_map() as $short_key => $fqcn ) {
                        if ( ! class_exists( $fqcn ) ) { continue; }
                        try {
                            $instance   = new $fqcn();
                            $unprefixed = $instance->get_table_name_raw();
                            if ( $unprefixed ) {
                                $full = $wpdb->prefix . $unprefixed;
                                self::$name_cache[ $short_key ]  = $full;
                                self::$name_cache[ $unprefixed ] = $full;
                            }
                        } catch ( \Throwable $inner ) {
                        }
                    }
                }
                $loaded = true;
            }
        } catch ( \Throwable $e ) {
            if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Database bootstrap failures must not recurse back through the DB-backed logger.
                error_log( '[UltraGuard Pro][ERROR] DatabaseSchema registry load failed: ' . $e->getMessage() );
            }
        }

        // Path 2: minimal static fallback when registry is not available yet.
        if ( ! $loaded ) {
            self::load_core_fallback();
        }
    }

    /**
     * Minimal static table map — core db_tables infrastructure tables ONLY.
     *
     * Module-specific tables must NOT be listed here. Each module registers
     * its own schema via DatabaseTablesModule::register_schema(), and
     * maybe_load_registry() discovers them automatically from that registry.
     * Adding a module table here would violate the modular architecture.
     */
    private static function load_core_fallback(): void {
        global $wpdb;

        $core = [
            'security_logs'   => 'ultraguard_security_logs',
            'audit_logs'      => 'ultraguard_audit_logs',
            'security_alerts' => 'ultraguard_security_alerts',
            'security_events' => 'ultraguard_security_events',
            'threat_ips'      => 'ultraguard_threat_ips',
        ];

        foreach ( $core as $short_key => $unprefixed ) {
            $full = $wpdb->prefix . $unprefixed;
            self::$name_cache[ $short_key ]  = $full;
            self::$name_cache[ $unprefixed ] = $full;
        }
    }

    /**
     * Introspect column names and types via SHOW COLUMNS.
     * Runs at most once per table per HTTP request.
     */
    private static function load_columns( string $full_table_name ): void {
        global $wpdb;

        self::$column_cache[ $full_table_name ] = [];

        try {
            // Validate the table name before querying (no user input here, but be safe)
            $safe_table = preg_replace( '/[^a-zA-Z0-9_]/', '', $full_table_name );
            if ( $safe_table !== $full_table_name ) { return; }
            $escaped_table = esc_sql( $safe_table );

            // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- internal identifier sanitized to safe SQL characters only.
            $cols = $wpdb->get_results( "SHOW COLUMNS FROM `{$escaped_table}`", ARRAY_A );

            if ( ! empty( $cols ) && is_array( $cols ) ) {
                foreach ( $cols as $col ) {
                    $name = $col['Field'] ?? '';
                    $type = strtolower( $col['Type'] ?? 'varchar(255)' );
                    if ( $name !== '' ) {
                        self::$column_cache[ $full_table_name ][ $name ] = $type;
                    }
                }
            }
        } catch ( \Throwable $e ) {
            // Table may not exist yet during fresh install — leave cache empty.
            if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                \UltraguardPro\core\Logger::error(
                    'DatabaseSchema SHOW COLUMNS failed for ' . $full_table_name . ': ' . $e->getMessage()
                );
            }
        }
    }
}
