<?php
/**
 * ScanResultsTable — standalone class file.
 *
 * @package UltraguardPro\modules\antivirus\db
 */

namespace UltraguardPro\modules\antivirus\db;

use UltraguardPro\db\BaseTable;

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

/**
 * Antivirus module DB table classes.
 *
 * Contains all four table classes owned by the Antivirus module:
 *   • ScanResultsTable
 *   • QuarantineTable
 *   • MalwareSignaturesTable
 *   • VulnerabilityScansTable
 *
 * @package UltraguardPro\modules\antivirus\db
 */

namespace UltraguardPro\modules\antivirus\db;

use UltraguardPro\db\BaseTable;

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

// ScanResultsTable
class ScanResultsTable extends BaseTable {

    protected $table_name  = 'ultraguard_scan_results';
    protected $cache_group = 'ultraguard_scan_results';

    /**
     * @return array<string,mixed>
     */
    public function get_defaults(): array {
        return [
            'id'                => 0,
            'scan_id'           => '',
            'scan_type'         => 'full',
            'file_path'         => '',
            'file_hash'         => null,
            'file_size'         => 0,
            'threat_type'       => null,
            'threat_level'      => 'none',
            'status'            => 'pending',
            'details'           => null,
            'is_false_positive' => 0,
            'created_by'        => null,
            'created_at'        => '',
            'updated_at'        => '',
        ];
    }

    /**
     * @param  array<string,mixed>|false $data
     * @return array<string,mixed>|false
     */
    protected function validate_data( $data ) {
        if ( empty( $data['scan_id'] ) || empty( $data['file_path'] ) ) {
            return false;
        }

        $valid_types    = [ 'full', 'quick', 'custom', 'scheduled', 'single' ];
        $valid_levels   = [ 'none', 'low', 'medium', 'high', 'critical' ];
        $valid_statuses = [ 'pending', 'clean', 'infected', 'quarantined', 'false_positive', 'skipped', 'error' ];

        if ( isset( $data['scan_type'] ) && ! in_array( $data['scan_type'], $valid_types, true ) ) {
            $data['scan_type'] = 'full';
        }
        if ( isset( $data['threat_level'] ) && ! in_array( $data['threat_level'], $valid_levels, true ) ) {
            $data['threat_level'] = 'none';
        }
        if ( isset( $data['status'] ) && ! in_array( $data['status'], $valid_statuses, true ) ) {
            $data['status'] = 'pending';
        }
        return $data;
    }

    /**
     * @param  array<string,mixed> $data
     * @return array<string,mixed>
     */
    protected function sanitize_data( $data ): array {
        $out = [];
        foreach ( $data as $k => $v ) {
            $out[ $k ] = match ( $k ) {
                'file_path', 'details' => sanitize_textarea_field( (string) $v ),
                'file_size'            => absint( $v ),
                'is_false_positive'    => (int) (bool) $v,
                'created_by'           => $v === null ? null : absint( $v ),
                default                => is_string( $v ) ? sanitize_text_field( $v ) : $v,
            };
        }
        return $out;
    }

    // ── Domain methods ─────────────────────────────────────────────────────────

    /**
     * Retrieve all results for a given scan session.
     *
     * @param  string $scan_id  Scan session ID.
     * @return array<int,array<string,mixed>>
     */
    public function get_by_scan( string $scan_id ): array {
        return $this->get_by( [ 'scan_id' => $scan_id ], 10000 );
    }

    /**
     * Retrieve infected files up to a given limit.
     *
     * @param  int $limit  Max number of rows to return.
     * @return array<int,array<string,mixed>>
     */
    public function get_infected_files( int $limit = 200 ): array {
        return $this->get_by( [ 'status' => 'infected' ], $limit, 'created_at', 'DESC' );
    }

    /**
     * Summary counts for a specific scan session.
     *
     * @param  string $scan_id
     * @return array{total: int, clean: int, infected: int, quarantined: int, skipped: int}
     */
    public function get_scan_summary( string $scan_id ): array {
        return [
            'total'       => $this->count( [ 'scan_id' => $scan_id ] ),
            'clean'       => $this->count( [ 'scan_id' => $scan_id, 'status' => 'clean' ] ),
            'infected'    => $this->count( [ 'scan_id' => $scan_id, 'status' => 'infected' ] ),
            'quarantined' => $this->count( [ 'scan_id' => $scan_id, 'status' => 'quarantined' ] ),
            'skipped'     => $this->count( [ 'scan_id' => $scan_id, 'status' => 'skipped' ] ),
        ];
    }

    /**
     * Get overall stats across all scan sessions.
     *
     * @return array{total: int, infected: int, clean: int, quarantined: int, critical: int}
     */
    public function get_overall_stats(): array {
        return [
            'total'       => $this->count(),
            'infected'    => $this->count( [ 'status' => 'infected' ] ),
            'clean'       => $this->count( [ 'status' => 'clean' ] ),
            'quarantined' => $this->count( [ 'status' => 'quarantined' ] ),
            'critical'    => $this->count( [ 'threat_level' => 'critical', 'status' => 'infected' ] ),
        ];
    }

    /**
     * Mark a scan result as a false positive.
     *
     * @param  int $id  Record ID.
     * @return bool
     */
    public function mark_as_false_positive( int $id ): bool {
        return $this->update( $id, [ 'is_false_positive' => 1, 'status' => 'false_positive' ] );
    }

    /**
     * Check whether a file was previously marked as a false positive.
     *
     * The match is intentionally scoped to the same path and content hash so a
     * changed file is scanned again even if an older version was allowlisted.
     *
     * @param string      $file_path Absolute file path.
     * @param string|null $file_hash Current file hash.
     * @return bool
     */
    public function is_known_false_positive( string $file_path, ?string $file_hash = null ): bool {
        $matches = $this->get_by(
            [
                'file_path'         => $file_path,
                'is_false_positive' => 1,
            ],
            25,
            'updated_at',
            'DESC'
        );

        if ( empty( $matches ) ) {
            return false;
        }

        if ( null === $file_hash || '' === $file_hash ) {
            return true;
        }

        foreach ( $matches as $row ) {
            if ( (string) ( $row['file_hash'] ?? '' ) === $file_hash ) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return string[]
     */
    protected function get_searchable_fields(): array {
        return [ 'scan_id', 'file_path', 'threat_type', 'status' ];
    }

    // ── Cache overrides ─────────────────────────────────────────────────────

    /**
     * Scan result inserts happen for every scanned file.
     *
     * Clearing all table caches per insert is too expensive for large scans
     * because it repeatedly sweeps transient rows in wp_options. The module
     * already flushes antivirus caches once at scan completion, which is the
     * right invalidation point for this workload.
     */
    protected function invalidate_caches_after_insert() {
        // Intentionally no-op. AntivirusModule::flush_all_antivirus_caches()
        // runs after scan completion and after bulk maintenance actions.
    }

    /**
     * Override: clear ALL caches after any update (e.g. quarantine status).
     *
     * @param int $id  Updated record ID.
     */
    protected function invalidate_caches_after_update( $id ) {
        $this->clear_all_caches();
    }

    /**
     * Override: clear ALL caches after bulk operations (e.g. clear clean results).
     */
    protected function invalidate_caches_after_bulk() {
        $this->clear_all_caches();
    }
}

// QuarantineTable
// =============================================================================
