<?php
defined( 'ABSPATH' ) || exit;

/**
 * Persistent error log — tracks every PHP error seen during scans and records
 * whether it was solved (auto-fix, server patch, or guardian).
 *
 * Table: sitecl_error_log
 */
class SITECL_Error_Log {

    private const TABLE = 'sitecl_error_log';

    // -------------------------------------------------------------------------
    // Write
    // -------------------------------------------------------------------------

    /**
     * Upsert a single error from a scan.
     * On first sight inserts; on repeat increments occurrence_count + updates last_seen.
     *
     * @param array $error  Keys: type (string), message, file, line, plugin_name (optional)
     */
    public static function upsert( array $error ): void {
        global $wpdb;
        $table = $wpdb->prefix . self::TABLE;
        $hash  = self::hash( $error );
        $now   = current_time( 'mysql' );

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $existing = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT id, occurrence_count FROM `{$table}` WHERE error_hash = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $hash
            )
        );

        if ( $existing ) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->update(
                $table,
                [
                    'last_seen'        => $now,
                    'occurrence_count' => (int) $existing->occurrence_count + 1,
                ],
                [ 'id' => (int) $existing->id ],
                [ '%s', '%d' ],
                [ '%d' ]
            );
        } else {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
            $wpdb->insert(
                $table,
                [
                    'error_hash'       => $hash,
                    'type'             => sanitize_text_field( $error['type']        ?? '' ),
                    'message'          => sanitize_textarea_field( $error['message'] ?? '' ),
                    'file'             => sanitize_text_field( $error['file']        ?? '' ),
                    'line'             => (int) ( $error['line']                     ?? 0 ),
                    'plugin_name'      => sanitize_text_field( $error['plugin_name'] ?? '' ),
                    'first_seen'       => $now,
                    'last_seen'        => $now,
                    'occurrence_count' => 1,
                    'status'           => 'open',
                ],
                [ '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%d', '%s' ]
            );
        }
    }

    /**
     * Sync a batch of issues from a full scan.
     * Only processes issues that carry a `raw_error` (PHP error reader results).
     *
     * @param array $issues  Issues array from SITECL_Scanner::run_full_scan()
     */
    public static function sync_from_scan( array $issues ): void {
        foreach ( $issues as $issue ) {
            if ( empty( $issue['raw_error'] ) || ! is_array( $issue['raw_error'] ) ) {
                continue;
            }
            self::upsert( $issue['raw_error'] );
        }
    }

    /**
     * Mark a specific error as solved.
     *
     * @param string $error_hash    md5 hash (from self::hash())
     * @param string $fix_type      'auto_fix' | 'server_patch' | 'guardian'
     * @param string $fix_key       Fix identifier, e.g. 'clear_transients'
     * @param string $server_job_id Server job ID (server_patch only)
     */
    public static function mark_solved(
        string $error_hash,
        string $fix_type,
        string $fix_key = '',
        string $server_job_id = ''
    ): void {
        global $wpdb;
        $table = $wpdb->prefix . self::TABLE;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->update(
            $table,
            [
                'status'        => 'solved',
                'fix_type'      => sanitize_text_field( $fix_type ),
                'fix_key'       => sanitize_text_field( $fix_key ),
                'fixed_at'      => current_time( 'mysql' ),
                'server_job_id' => sanitize_text_field( $server_job_id ),
            ],
            [ 'error_hash' => $error_hash, 'status' => 'open' ],
            [ '%s', '%s', '%s', '%s', '%s' ],
            [ '%s', '%s' ]
        );
    }

    /**
     * Mark all currently-open errors as solved by a broad fix (e.g. guardian clearing a php_fatal).
     *
     * @param string $fix_type  'auto_fix' | 'guardian'
     * @param string $fix_key   Fix key, e.g. 'clear_object_cache'
     */
    public static function mark_open_solved( string $fix_type, string $fix_key ): void {
        global $wpdb;
        $table = $wpdb->prefix . self::TABLE;
        $now   = current_time( 'mysql' );

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->query(
            $wpdb->prepare(
                "UPDATE `{$table}` SET status='solved', fix_type=%s, fix_key=%s, fixed_at=%s WHERE status='open'", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $fix_type,
                $fix_key,
                $now
            )
        );
    }

    // -------------------------------------------------------------------------
    // Read
    // -------------------------------------------------------------------------

    /**
     * Get all errors for the dashboard — open first, then solved, newest last_seen.
     *
     * @param int $limit  Maximum rows to return (default 200).
     */
    public static function get_all( int $limit = 200 ): array {
        global $wpdb;
        $table = $wpdb->prefix . self::TABLE;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM `{$table}` ORDER BY FIELD(status,'open','solved'), last_seen DESC LIMIT %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $limit
            ),
            ARRAY_A
        );

        return $rows ?: [];
    }

    /**
     * Aggregate summary counts for the dashboard header.
     *
     * @return array { total: int, open: int, solved: int }
     */
    public static function get_summary(): array {
        global $wpdb;
        $table = $wpdb->prefix . self::TABLE;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $rows = $wpdb->get_results(
            "SELECT status, COUNT(*) AS count FROM `{$table}` GROUP BY status", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            ARRAY_A
        );

        $summary = [ 'total' => 0, 'open' => 0, 'solved' => 0 ];
        foreach ( (array) $rows as $row ) {
            $key              = $row['status'];
            $summary[ $key ]  = (int) $row['count'];
            $summary['total'] += (int) $row['count'];
        }
        return $summary;
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    /**
     * Compute a stable hash for an error.
     * Normalises PHP integer error constants to string names so hashes match
     * between error_get_last() (returns int type) and the error reader (returns string).
     *
     * @param array $error  Keys: type, message, file, line
     */
    public static function hash( array $error ): string {
        return md5(
            self::normalize_type( $error['type'] ?? '' ) .
            ( $error['message'] ?? '' ) .
            ( $error['file']    ?? '' ) .
            ( $error['line']    ?? '' )
        );
    }

    /**
     * Normalise PHP error type — accepts both the integer constant (from error_get_last)
     * and the string label (from SITECL_Error_Reader).
     */
    private static function normalize_type( $type ): string {
        if ( is_int( $type ) ) {
            $map = [
                E_ERROR         => 'Fatal error',
                E_PARSE         => 'Parse error',
                E_CORE_ERROR    => 'Core error',
                E_COMPILE_ERROR => 'Compile error',
                E_WARNING       => 'PHP Warning',
                E_NOTICE        => 'PHP Notice',
                E_DEPRECATED    => 'PHP Deprecated',
            ];
            return $map[ $type ] ?? (string) $type;
        }
        return (string) $type;
    }
}
