<?php

declare(strict_types=1);

namespace SecurWP\Modules\Scanner\Repository;

use SecurWP\Modules\Scanner\Entity\ScanStatus;

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


class ScanStatusRepository {
    private static $table_name = 'securwp_scan_status';

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

    /**
     * Add log entry with error handling.
     *
     * @param string $scan_id
     * @param int    $level
     * @param string $type
     * @param string $msg
     * @return int|false Insert ID on success, false on failure
     */
    public static function add( $scan_id, $level, $type, $msg ) {
        try {
            global $wpdb;
            $table = self::get_table();

            // Validate inputs
            if ( empty( $scan_id ) || empty( $msg ) ) {
                return false;
            }

            $ctime = sprintf( '%.6f', microtime( true ) );

            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Inserts a plugin-owned scan status log row.
            $result = $wpdb->insert(
                $table,
                [
                    'scan_id' => sanitize_text_field( $scan_id ),
                    'ctime'   => $ctime,
                    'level'   => absint( $level ),
                    'type'    => sanitize_text_field( $type ),
                    'message' => wp_strip_all_tags( $msg ), // Strip all HTML/PHP tags for security
                ],
                [ '%s', '%s', '%d', '%s', '%s' ]
            );

            // Check for errors
            if ( $result === false ) {
                securwp_log( 'SecurWP: Failed to insert scan status log: ' . $wpdb->last_error );
                return false;
            }

            return $wpdb->insert_id;

        } catch ( \Throwable $e ) {
            securwp_log( 'SecurWP: Error in ScanStatusRepository::add - ' . $e->getMessage() );
            return false;
        }
    }

    /**
     * Get status events after a certain timestamp with error handling.
     *
     * @param string $scan_id
     * @param float $last_ctime
     * @param int $limit
     * @return array
     */
    public static function get_events( $scan_id, $last_ctime = 0, $limit = 100 ) {
        try {
            global $wpdb;
            $table = self::get_table();

            // Validate inputs
            if ( empty( $scan_id ) ) {
                return [];
            }

            $limit = absint( $limit );
            if ( $limit <= 0 || $limit > 1000 ) {
                $limit = 100; // Sane default
            }

            // Use a closed cursor on ctime. Multiple rows can be inserted within
            // the same microtime bucket during terminal completion/failure, and
            // the client already de-duplicates by row ID.
            $results = self::get_results_prepared(
                "SELECT * FROM " . self::table_identifier() . " WHERE scan_id = %s AND ctime >= %s ORDER BY ctime ASC LIMIT %d",
                sanitize_text_field( $scan_id ),
                sprintf( '%.6f', (float) $last_ctime ),
                $limit
            );

            if ( $results === null || $wpdb->last_error ) {
                securwp_log( 'SecurWP: Failed to get scan events: ' . $wpdb->last_error );
                return [];
            }

            return array_map( function( $row ) {
                return new ScanStatus( $row );
            }, $results );

        } catch ( \Throwable $e ) {
            securwp_log( 'SecurWP: Error in ScanStatusRepository::get_events - ' . $e->getMessage() );
            return [];
        }
    }

    /**
     * Get most recent log entries regardless of scan ID.
     *
     * @param int $limit
     * @return array
     */
    public static function get_recent( $limit = 100 ) {
        try {
            global $wpdb;
            $table = self::get_table();

            $limit = absint( $limit );
            if ( $limit <= 0 || $limit > 1000 ) {
                $limit = 100;
            }

            $results = self::get_results_prepared(
                "SELECT * FROM " . self::table_identifier() . " ORDER BY ctime DESC LIMIT %d",
                $limit
            );

            if ( $results === null ) return [];

            return array_map( function( $row ) {
                return new ScanStatus( $row );
            }, array_reverse( $results ) ); // Reverse to show oldest first in UI flow

        } catch ( \Throwable $e ) {
            return [];
        }
    }

    /**
     * Clear all log entries.
     * Called when a new scan starts to remove old logs.
     *
     * @return bool Success
     */
    public static function clear_all() {
        try {
            global $wpdb;
            $table = self::get_table();

            $result = self::query_prepared( 'TRUNCATE TABLE ' . self::table_identifier() );

            if ( $result === false ) {
                securwp_log( 'SecurWP: Failed to clear scan status logs: ' . $wpdb->last_error );
                return false;
            }

            return true;

        } catch ( \Throwable $e ) {
            securwp_log( 'SecurWP: Error in ScanStatusRepository::clear_all - ' . $e->getMessage() );
            return false;
        }
    }

    /**
     * Delete log rows older than the provided cutoff time.
     *
     * @param float $cutoff_time Microtime cutoff - delete records older than this
     * @return int|false Number of rows deleted, or false on error
     */
    public static function trim( $cutoff_time ) {
        try {
            global $wpdb;
            $table = self::get_table();

            // Validate input
            if ( ! is_numeric( $cutoff_time ) || $cutoff_time <= 0 ) {
                return false;
            }

            // Delete old records
            $result = self::query_prepared(
                "DELETE FROM " . self::table_identifier() . " WHERE ctime < %s",
                sprintf( '%.6f', $cutoff_time )
            );

            if ( $result === false ) {
                securwp_log( 'SecurWP: Failed to trim scan status logs: ' . $wpdb->last_error );
                return false;
            }

            return $result; // Number of rows deleted

        } catch ( \Throwable $e ) {
            securwp_log( 'SecurWP: Error in ScanStatusRepository::trim - ' . $e->getMessage() );
            return false;
        }
    }

    /**
     * Trim by row count instead of timestamp.
     * Keeps only the most recent N records.
     *
     * @param int $keep_count Maximum number of records to keep
     * @return int|false Number of rows deleted, or false on error
     */
    public static function trim_by_count( $keep_count = 10000 ) {
        try {
            global $wpdb;
            $table = self::get_table();

            $keep_count = absint( $keep_count );
            if ( $keep_count <= 0 ) {
                $keep_count = 10000; // Sane default
            }

            // Count total records
            $count = self::get_var_query( 'SELECT COUNT(*) FROM ' . self::table_identifier() );

            if ( $count === null || $wpdb->last_error ) {
                securwp_log( 'SecurWP: Failed to count scan status logs: ' . $wpdb->last_error );
                return false;
            }

            if ( $count <= $keep_count ) {
                return 0; // Nothing to trim
            }

            $delete_count = $count - $keep_count;

            // Delete oldest records
            $result = self::query_prepared(
                "DELETE FROM " . self::table_identifier() . " ORDER BY ctime ASC LIMIT %d",
                $delete_count
            );

            if ( $result === false ) {
                securwp_log( 'SecurWP: Failed to trim scan status logs by count: ' . $wpdb->last_error );
                return false;
            }

            return $result;

        } catch ( \Throwable $e ) {
            securwp_log( 'SecurWP: Error in ScanStatusRepository::trim_by_count - ' . $e->getMessage() );
            return false;
        }
    }

	private static function table_identifier(): string {
		global $wpdb;
		$table_name = $wpdb->prefix . self::$table_name;
		if ( $table_name === '' || strlen( $table_name ) > 64 || ! preg_match( '/^[A-Za-z0-9_]+$/', $table_name ) ) {
			throw new \InvalidArgumentException( 'Invalid table identifier.' );
		}

        return '`' . $table_name . '`';
    }

    private static function get_results_prepared( string $sql, ...$params ): array {
        global $wpdb;
        $prepared = self::prepare_sql( $sql, ...$params );
        if ( '' === $prepared ) {
            return [];
        }

        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Prepared repository query.
        $results = $wpdb->get_results( $prepared, ARRAY_A );
        return is_array( $results ) ? $results : [];
    }

    private static function get_var_query( string $sql ) {
        global $wpdb;
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Reads one scalar from the plugin-owned scan status table.
        return $wpdb->get_var( $sql );
    }

    private static function query_prepared( string $sql, ...$params ) {
        global $wpdb;
        $prepared = empty( $params ) ? $sql : self::prepare_sql( $sql, ...$params );
        if ( '' === $prepared ) {
            return false;
        }

        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Executes a statement against the plugin-owned scan status table.
        return $wpdb->query( $prepared );
    }

    private static function prepare_sql( string $sql, ...$params ): string {
        global $wpdb;
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Parameter binding is delegated to wpdb::prepare() using the literal SQL passed by each repository method.
        $prepared = $wpdb->prepare( $sql, ...$params );
        return is_string( $prepared ) ? $prepared : '';
    }
}
