<?php

declare(strict_types=1);

namespace SecurWP\Modules\Scanner\Service;

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


/**
 * Class ScannerSettings
 * 
 * Encapsulates all scanner settings logic: retrieval, validation, and defaults.
 * 
 */
class ScannerSettings {

    const TABLE_NAME = 'securwp_settings';
    private const REMOVED_SETTINGS_KEYS = [
        'scope_core',
        'scope_vuln',
        'scope_plugins',
        'scope_themes',
        'scope_uploads',
        'scope_root',
        'lowResourceMode',
    ];
    private const ALLOWED_SCHEDULES = [ 'none', 'daily', 'weekly', 'monthly' ];
    private const SCHEDULE_ALIASES = [
        'manual'    => 'none',
        'off'       => 'none',
        'disabled'  => 'none',
        'daily_2am' => 'daily',
    ];
    private const ALLOWED_INTENSITIES = [ 'standard', 'deep' ];
    private const INTENSITY_ALIASES = [
        'high_sensitive' => 'deep',
        'full'           => 'deep',
        'file_scan'      => 'standard',
    ];
    private static $table_ready = false;

    /**
     * Get all settings with defaults applied.
     * 
     * @return array
     */
    public static function get_all() {
        $stored = self::strip_removed_settings( self::get_stored_settings() );
        $defaults = self::get_defaults();
        $settings = wp_parse_args( $stored, $defaults );
        $settings['schedule'] = self::normalize_schedule( $settings['schedule'] ?? $defaults['schedule'], (string) $defaults['schedule'] );
        $settings['scanIntensity'] = self::normalize_scan_intensity( $settings['scanIntensity'] ?? $defaults['scanIntensity'], (string) $defaults['scanIntensity'] );
        if ( $settings['schedule'] !== 'monthly' ) {
            $settings['scheduleDay'] = 0;
        }
        $settings['quarantine_retention_days'] = self::normalize_quarantine_retention_days(
            $settings['quarantine_retention_days'] ?? $defaults['quarantine_retention_days'],
            (int) $defaults['quarantine_retention_days']
        );
        $raw_scan_exclusions        = array_key_exists( 'scanExclusions', $stored )
            ? $stored['scanExclusions']
            : ( $defaults['scanExclusions'] ?? '' );
        $settings['scanExclusions'] = self::normalize_scan_exclusions( $raw_scan_exclusions );

        return $settings;
    }

    /**
     * Save settings.
     * 
     * @param array $input Raw input array
     * @return bool Success
     */
    public static function save( $input ) {
        // Preserve internal scanner metadata that should not be user-editable.
        $existing = self::strip_removed_settings( self::get_stored_settings() );
        $defaults = self::get_defaults();
        $current  = wp_parse_args( $existing, $defaults );
        $schedule = self::normalize_schedule( $input['schedule'] ?? $current['schedule'], $current['schedule'] );

        $settings = [
            'schedule'           => $schedule,
            'scheduleDay'        => ( 'monthly' === $schedule ) ? max( 0, min( 28, absint( $input['scheduleDay'] ?? $current['scheduleDay'] ) ) ) : 0,
            'scanIntensity'      => self::normalize_scan_intensity( $input['scanIntensity'] ?? $current['scanIntensity'], $current['scanIntensity'] ),
            'emailNotifications' => filter_var( $input['emailNotifications'] ?? $current['emailNotifications'], FILTER_VALIDATE_BOOLEAN ),
            'notificationEmail'  => sanitize_email( $input['notificationEmail'] ?? $current['notificationEmail'] ),
            'scanMaxDuration'    => absint( $input['scanMaxDuration'] ?? $current['scanMaxDuration'] ),
            'stageMaxExecutionTime' => absint( $input['stageMaxExecutionTime'] ?? $current['stageMaxExecutionTime'] ),
            'scanExclusions'     => self::normalize_scan_exclusions(
                array_key_exists( 'scanExclusions', $input ) ? $input['scanExclusions'] : $current['scanExclusions']
            ),
            'quarantine_retention_days' => self::normalize_quarantine_retention_days(
                $input['quarantine_retention_days'] ?? $current['quarantine_retention_days'],
                (int) $current['quarantine_retention_days']
            ),
        ];

        $settings['malware_sig_version'] = sanitize_text_field(
            $existing['malware_sig_version'] ?? ( $defaults['malware_sig_version'] ?? '0.0.0' )
        );
        $settings['hash_cache_epoch'] = sanitize_text_field(
            $existing['hash_cache_epoch'] ?? ( $defaults['hash_cache_epoch'] ?? '0' )
        );
        $settings['hash_cache_keys'] = self::sanitize_hash_cache_keys(
            $existing['hash_cache_keys'] ?? ( $defaults['hash_cache_keys'] ?? [] )
        );

        return self::save_settings( $settings );
    }

    /**
     * Get default settings.
     * 
     * @return array
     */
    public static function get_defaults() {
        return [
            'schedule'           => 'daily',
            'scheduleDay'        => 0,
            'scanIntensity'      => 'standard',
            'emailNotifications' => false,
            'notificationEmail'  => get_option( 'admin_email' ),
            'scanMaxDuration'    => 0,
            'stageMaxExecutionTime' => 0,
            'scanExclusions'     => '',
            'quarantine_retention_days' => 30,
            'malware_sig_version'=> '0.0.0', // Updated by Preflight
            'hash_cache_epoch'   => '0', // Updated by Preflight for global cache invalidation
            'hash_cache_keys'    => [], // Internal: proof verification keys
        ];
    }

    /**
     * Get a specific setting value.
     * 
     * @param string $key Setting key
     * @return mixed Setting value or null if invalid key
     */
    public static function get( $key ) {
        $all = self::get_all();
        return $all[$key] ?? null;
    }

    /**
     * Normalize schedule input and preserve backward compatibility for legacy values.
     *
     * @param mixed  $value
     * @param string $fallback
     * @return string
     */
    private static function normalize_schedule( $value, string $fallback ): string {
        $candidate = sanitize_key( (string) $value );
        if ( isset( self::SCHEDULE_ALIASES[ $candidate ] ) ) {
            $candidate = self::SCHEDULE_ALIASES[ $candidate ];
        }

        if ( ! in_array( $candidate, self::ALLOWED_SCHEDULES, true ) ) {
            $candidate = sanitize_key( $fallback );
        }
        if ( ! in_array( $candidate, self::ALLOWED_SCHEDULES, true ) ) {
            $candidate = self::get_defaults()['schedule'];
        }

        return $candidate;
    }

    /**
     * Normalize scan intensity input and preserve legacy aliases.
     *
     * @param mixed  $value
     * @param string $fallback
     * @return string
     */
    private static function normalize_scan_intensity( $value, string $fallback ): string {
        $candidate = sanitize_key( (string) $value );
        if ( isset( self::INTENSITY_ALIASES[ $candidate ] ) ) {
            $candidate = self::INTENSITY_ALIASES[ $candidate ];
        }

        if ( ! in_array( $candidate, self::ALLOWED_INTENSITIES, true ) ) {
            $candidate = sanitize_key( $fallback );
        }
        if ( isset( self::INTENSITY_ALIASES[ $candidate ] ) ) {
            $candidate = self::INTENSITY_ALIASES[ $candidate ];
        }
        if ( ! in_array( $candidate, self::ALLOWED_INTENSITIES, true ) ) {
            $candidate = self::get_defaults()['scanIntensity'];
        }

        return $candidate;
    }

    /**
     * Normalize quarantine retention days.
     *
     * @param mixed $value
     * @param int   $fallback
     * @return int
     */
    private static function normalize_quarantine_retention_days( $value, int $fallback ): int {
        $candidate = absint( $value );
        if ( $candidate <= 0 ) {
            $candidate = absint( $fallback );
        }
        if ( $candidate <= 0 ) {
            $candidate = (int) self::get_defaults()['quarantine_retention_days'];
        }

        return max( 1, min( 30, $candidate ) );
    }

    /**
     * Normalize scan exclusions while preserving explicit operator intent.
     *
     * @param mixed $value
     * @return string
     */
    public static function normalize_scan_exclusions( $value ): string {
        $raw = is_string( $value ) ? $value : '';

        $lines = preg_split( "/\r\n|\n|\r/", sanitize_textarea_field( $raw ) );
        if ( ! is_array( $lines ) ) {
            return '';
        }

        $normalized = [];
        $seen       = [];

        foreach ( $lines as $line ) {
            $candidate = trim( (string) $line );
            if ( $candidate === '' ) {
                continue;
            }

            $fingerprint = strtolower( $candidate );
            if ( isset( $seen[ $fingerprint ] ) ) {
                continue;
            }

            $seen[ $fingerprint ] = true;
            $normalized[]         = $candidate;
        }

        return implode( "\n", $normalized );
    }

    /**
     * Update internal scanner metadata without exposing it to user input.
     *
     * @param string $key Internal setting key.
     * @param mixed $value Internal setting value.
     * @return bool Success.
     */
    public static function set_internal_setting( string $key, $value ): bool {
        $allowed = [ 'malware_sig_version', 'hash_cache_epoch', 'hash_cache_keys' ];
        if ( ! in_array( $key, $allowed, true ) ) {
            return false;
        }

        $settings = self::strip_removed_settings( self::get_stored_settings() );
        if ( $key === 'malware_sig_version' ) {
            $settings[$key] = sanitize_text_field( (string) $value );
        } elseif ( $key === 'hash_cache_epoch' ) {
            $settings[$key] = sanitize_text_field( (string) $value );
        } elseif ( $key === 'hash_cache_keys' ) {
            $settings[$key] = self::sanitize_hash_cache_keys( is_array( $value ) ? $value : [] );
        }

        return self::save_settings( $settings );
    }

    public static function has_removed_settings(): bool {
        $stored = self::get_stored_settings();

        foreach ( self::REMOVED_SETTINGS_KEYS as $key ) {
            if ( array_key_exists( $key, $stored ) ) {
                return true;
            }
        }

        return false;
    }

    public static function cleanup_removed_settings(): bool {
        $stored = self::get_stored_settings();
        $clean = self::strip_removed_settings( $stored );

        if ( $clean === $stored ) {
            return true;
        }

        return self::save_settings( $clean );
    }

    /**
     * Persist settings into the scanner settings table.
     *
     * @param array $settings Settings payload.
     * @return bool Success.
     */
    private static function save_settings( array $settings ): bool {
        self::ensure_table();

        global $wpdb;
        $table = self::get_table_name();

        $encoded = wp_json_encode( $settings );
        if ( false === $encoded ) {
            return false;
        }

        $now = time();
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Updates the singleton row in the plugin-owned scanner settings table.
        $updated = $wpdb->update(
            $table,
            [
                'settings'   => $encoded,
                'updated_at' => $now,
            ],
            [ 'id' => 1 ],
            [ '%s', '%d' ],
            [ '%d' ]
        );

        if ( false === $updated ) {
            return false;
        }

        if ( $updated > 0 || self::settings_row_exists() ) {
            return true;
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Inserts the singleton row in the plugin-owned scanner settings table when it does not exist yet.
        $inserted = $wpdb->insert(
            $table,
            [
                'id'             => 1,
                'settings'       => $encoded,
                'cloud_settings' => null,
                'created_at'     => $now,
                'updated_at'     => $now,
            ],
            [ '%d', '%s', '%s', '%d', '%d' ]
        );

        return false !== $inserted;
    }

    /**
     * Load stored settings without defaults.
     *
     * @return array
     */
    private static function get_stored_settings(): array {
        self::ensure_table();

        global $wpdb;
        $table = self::table_identifier( self::get_table_name() );

        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Prepared settings query.
        $row = $wpdb->get_row(
            "SELECT settings FROM {$table} ORDER BY id DESC LIMIT 1",
            ARRAY_A
        );
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter

        if ( ! $row || empty( $row['settings'] ) ) {
            return [];
        }

        $decoded = json_decode( $row['settings'], true );
        return is_array( $decoded ) ? $decoded : [];
    }

    /**
     * Ensure the settings table exists for this install.
     *
     * @return void
     */
    private static function ensure_table(): void {
        if ( self::$table_ready ) {
            return;
        }

        global $wpdb;
        $table = self::get_table_name();

        $charset_collate = $wpdb->get_charset_collate();
        $sql = "CREATE TABLE {$table} (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            settings longtext NOT NULL,
            cloud_settings longtext NULL,
            firewall longtext NULL,
            hardening longtext NULL,
            global_settings longtext NULL,
            twofa longtext NULL,
            headers longtext NULL,
            notifications longtext NULL,
            cloudflare longtext NULL,
            branding longtext NULL,
            state longtext NULL,
            created_at int(11) NOT NULL,
            updated_at int(11) NOT NULL,
            PRIMARY KEY (id)
        ) {$charset_collate};";

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

        self::$table_ready = true;
    }

    /**
     * Get the scanner settings table name.
     *
     * @return string
     */
    private static function get_table_name(): string {
        global $wpdb;
        return $wpdb->prefix . self::TABLE_NAME;
    }

    /**
     * Whether the singleton settings row already exists.
     */
	private static function settings_row_exists(): bool {
		global $wpdb;
		$table = self::table_identifier( self::get_table_name() );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$prepared = $wpdb->prepare(
			"SELECT COUNT(*) FROM {$table} WHERE id = %d",
			1
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Executes a prepared query against the plugin-owned singleton settings table.
		$count = (int) $wpdb->get_var( $prepared );

		return $count > 0;
    }

	/**
	 * Quote a table identifier after validating its shape.
	 */
	private static function table_identifier( string $table ): string {
		if ( $table === '' || strlen( $table ) > 64 || ! preg_match( '/^[A-Za-z0-9_]+$/', $table ) ) {
			throw new \InvalidArgumentException( 'Invalid table identifier.' );
		}

		return '`' . $table . '`';
	}

    /**
     * Sanitize hash cache public keys before persistence.
     *
     * @param array $keys Raw key map.
     * @return array<int, string>
     */
    private static function sanitize_hash_cache_keys( array $keys ): array {
        $clean = [];
        foreach ( $keys as $key_id => $public_key_b64 ) {
            $kid = (int) $key_id;
            if ( $kid < 0 || $kid > 255 ) {
                continue;
            }
            if ( ! is_string( $public_key_b64 ) ) {
                continue;
            }
            $public_key_b64 = trim( $public_key_b64 );
            if ( $public_key_b64 === '' ) {
                continue;
            }
            $raw = base64_decode( $public_key_b64, true );
            if ( false === $raw || strlen( $raw ) !== 32 ) {
                continue;
            }
            $clean[ $kid ] = $public_key_b64;
        }
        return $clean;
    }

    private static function strip_removed_settings( array $settings ): array {
        foreach ( self::REMOVED_SETTINGS_KEYS as $key ) {
            unset( $settings[ $key ] );
        }

        return $settings;
    }
}
