<?php
/**
 * Audit logger for sensitive plugin actions.
 *
 * Writes to both the PHP error log (for server-side monitoring)
 * and a custom database table (for admin-visible history).
 *
 * @package AfriRegions
 */

defined( 'ABSPATH' ) or die( 'Forbidden' );

class SCPWOO_Audit {

    const PREFIX     = '[AfriRegions]';
    const TABLE_NAME = 'scpwoo_audit_log';
    const DB_VERSION = '1.0';

    // =========================================================================
    // Table management
    // =========================================================================

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

    /**
     * Create the audit log table if it doesn't exist.
     */
    public static function create_table(): void {
        global $wpdb;

        $table           = self::get_table_name();
        $charset_collate = $wpdb->get_charset_collate();

        $sql = "CREATE TABLE {$table} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            user_login VARCHAR(60) NOT NULL DEFAULT '',
            level VARCHAR(20) NOT NULL DEFAULT 'info',
            action VARCHAR(100) NOT NULL DEFAULT '',
            message TEXT NOT NULL,
            context TEXT,
            ip_address VARCHAR(45) NOT NULL DEFAULT '',
            PRIMARY KEY  (id),
            KEY idx_created (created_at),
            KEY idx_action (action),
            KEY idx_level (level)
        ) {$charset_collate};";

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

    /**
     * Check if the audit table needs to be created.
     *
     * @return bool
     */
    public static function needs_table(): bool {
        return get_option( 'scpwoo_audit_db_version', '' ) !== self::DB_VERSION;
    }

    /**
     * Ensure the audit table exists.
     */
    public static function maybe_create_table(): void {
        if ( self::needs_table() ) {
            self::create_table();
            update_option( 'scpwoo_audit_db_version', self::DB_VERSION );
        }
    }

    // =========================================================================
    // Logging
    // =========================================================================

    /**
     * Log a message.
     *
     * By default, writes to PHP error_log only. Admin actions (geo changes,
     * restrictions) explicitly pass $persist = true to also write to the
     * database table visible in the Activity Log admin page.
     *
     * @param string $level   Log level: 'info', 'warning', 'error', 'security'.
     * @param string $message Human-readable description.
     * @param array  $context Optional key-value pairs for structured data.
     * @param bool   $persist If true, also write to the database audit table.
     * @return void
     */
    public static function log( string $level, string $message, array $context = array(), bool $persist = false ): void {
        $user_id = get_current_user_id();
        $ip      = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'cli';

        // 1. Always write to PHP error_log
        $entry = sprintf(
            '%s [%s] %s | user=%d ip=%s',
            self::PREFIX,
            strtoupper( $level ),
            $message,
            $user_id,
            $ip
        );
        if ( ! empty( $context ) ) {
            $entry .= ' | ' . wp_json_encode( $context, JSON_UNESCAPED_UNICODE );
        }
        error_log( $entry );

        // 2. Only persist admin actions to the database table
        if ( $persist ) {
            self::write_to_db( $level, '', $message, $context, $user_id, $ip );
        }
    }

    /**
     * Write a log entry to the database.
     *
     * @param string $level
     * @param string $action
     * @param string $message
     * @param array  $context
     * @param int    $user_id
     * @param string $ip
     */
    private static function write_to_db( string $level, string $action, string $message, array $context, int $user_id, string $ip ): void {
        global $wpdb;

        // Skip if $wpdb is not available (CLI, test environment)
        if ( ! $wpdb || ! is_object( $wpdb ) ) {
            return;
        }

        $table = self::get_table_name();

        // Skip if table doesn't exist (before migration or in test environment)
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
            return;
        }

        // Get username for display
        $user_login = '';
        if ( $user_id > 0 ) {
            $user = get_userdata( $user_id );
            if ( $user ) {
                $user_login = $user->user_login;
            }
        }

        $wpdb->insert( $table, array(
            'user_id'    => $user_id,
            'user_login' => $user_login,
            'level'      => $level,
            'action'     => $action,
            'message'    => $message,
            'context'    => ! empty( $context ) ? wp_json_encode( $context, JSON_UNESCAPED_UNICODE ) : null,
            'ip_address' => $ip,
        ), array( '%d', '%s', '%s', '%s', '%s', '%s', '%s' ) );

        // Auto-cleanup: probabilistic — runs ~1 in 20 inserts to avoid extra queries on every write
        if ( wp_rand( 1, 20 ) === 1 ) {
            $keep_from = $wpdb->get_var( "SELECT id FROM {$table} ORDER BY id DESC LIMIT 1 OFFSET 499" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            if ( $keep_from ) {
                $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE id < %d", $keep_from ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            }
        }
    }

    // =========================================================================
    // Specialized log methods
    // =========================================================================

    /**
     * Log a geo data modification (city/neighborhood add/delete).
     */
    public static function log_geo_change( string $action, string $country_code, array $details = array() ): void {
        $merged = array_merge( array( 'country' => $country_code ), $details );
        $message = self::format_geo_message( $action, $merged );

        $user_id = get_current_user_id();
        $ip      = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'cli';

        // error_log
        $entry = sprintf( '%s [INFO] %s | user=%d ip=%s | %s', self::PREFIX, $message, $user_id, $ip, wp_json_encode( $merged, JSON_UNESCAPED_UNICODE ) );
        error_log( $entry );

        // database
        self::write_to_db( 'info', $action, $message, $merged, $user_id, $ip );
    }

    /**
     * Log a restriction change.
     */
    public static function log_restrictions_saved( int $region_count, int $city_count ): void {
        $context = array( 'blocked_regions' => $region_count, 'blocked_cities' => $city_count );
        $message = sprintf( 'Restrictions updated: %d regions, %d cities blocked', $region_count, $city_count );

        $user_id = get_current_user_id();
        $ip      = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'cli';

        error_log( sprintf( '%s [INFO] %s | user=%d ip=%s', self::PREFIX, $message, $user_id, $ip ) );
        self::write_to_db( 'info', 'save_restrictions', $message, $context, $user_id, $ip );
    }

    /**
     * Log a rate limit hit.
     */
    public static function log_rate_limit( string $endpoint ): void {
        self::log( 'security', 'Rate limit exceeded on ' . $endpoint );
    }

    /**
     * Log a checkout validation failure.
     */
    public static function log_validation_failure( string $type, string $reason, array $context = array() ): void {
        self::log( 'warning', 'Checkout validation failed (' . $type . '): ' . $reason, $context );
    }

    // =========================================================================
    // Read methods (for admin page)
    // =========================================================================

    /**
     * Get recent audit log entries.
     *
     * @param int    $limit  Max entries to return.
     * @param int    $offset Offset for pagination.
     * @param string $action Filter by action (optional).
     * @return array
     */
    public static function get_entries( int $limit = 50, int $offset = 0, string $action = '', string $date_from = '', string $date_to = '' ): array {
        global $wpdb;

        $table = self::get_table_name();
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
            return array();
        }

        $where_parts = array();
        $params = array();

        if ( ! empty( $action ) ) {
            $where_parts[] = 'action = %s';
            $params[] = $action;
        }
        if ( ! empty( $date_from ) ) {
            $where_parts[] = 'created_at >= %s';
            $params[] = $date_from . ' 00:00:00';
        }
        if ( ! empty( $date_to ) ) {
            $where_parts[] = 'created_at <= %s';
            $params[] = $date_to . ' 23:59:59';
        }

        $where = ! empty( $where_parts ) ? 'WHERE ' . implode( ' AND ', $where_parts ) : '';
        $params[] = $limit;
        $params[] = $offset;

        return $wpdb->get_results( $wpdb->prepare(
            "SELECT * FROM {$table} {$where} ORDER BY created_at DESC LIMIT %d OFFSET %d",
            ...$params
        ), ARRAY_A );
    }

    /**
     * Get total count of entries (for pagination).
     *
     * @param string $action Filter by action (optional).
     * @return int
     */
    public static function get_total_count( string $action = '', string $date_from = '', string $date_to = '' ): int {
        global $wpdb;

        $table = self::get_table_name();
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
            return 0;
        }

        $where_parts = array();
        $params = array();

        if ( ! empty( $action ) ) {
            $where_parts[] = 'action = %s';
            $params[] = $action;
        }
        if ( ! empty( $date_from ) ) {
            $where_parts[] = 'created_at >= %s';
            $params[] = $date_from . ' 00:00:00';
        }
        if ( ! empty( $date_to ) ) {
            $where_parts[] = 'created_at <= %s';
            $params[] = $date_to . ' 23:59:59';
        }

        $where = ! empty( $where_parts ) ? 'WHERE ' . implode( ' AND ', $where_parts ) : '';

        if ( ! empty( $params ) ) {
            return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} {$where}", ...$params ) );
        }

        return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
    }

    // =========================================================================
    // Purge
    // =========================================================================

    /**
     * Delete all entries from the audit log table.
     *
     * @return bool
     */
    public static function purge(): bool {
        global $wpdb;

        if ( ! $wpdb || ! is_object( $wpdb ) ) {
            return false;
        }

        $table = self::get_table_name();
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
            return false;
        }

        $wpdb->query( "TRUNCATE TABLE {$table}" );
        return true;
    }

    /**
     * Delete log entries older than $days days.
     * Called by WP-Cron daily.
     *
     * @param int $days Retention period in days (default 90).
     * @return int Number of rows deleted.
     */
    public static function purge_old_entries( int $days = 90 ): int {
        global $wpdb;

        if ( ! $wpdb || ! is_object( $wpdb ) ) {
            return 0;
        }

        $table = self::get_table_name();
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
            return 0;
        }

        $cutoff = gmdate( 'Y-m-d H:i:s', strtotime( "-{$days} days" ) );
        $deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE created_at < %s", $cutoff ) );
        return (int) $deleted;
    }

    // =========================================================================
    // Helpers
    // =========================================================================

    /**
     * Format a human-readable message for geo changes.
     */
    private static function format_geo_message( string $action, array $details ): string {
        $country = $details['country'] ?? '??';
        switch ( $action ) {
            case 'add_city':
                return sprintf( 'Added city "%s" to region %s (%s)', $details['city'] ?? '?', $details['region'] ?? '?', $country );
            case 'delete_city':
                return sprintf( 'Deleted city "%s" from region %s (%s)', $details['city'] ?? '?', $details['region'] ?? '?', $country );
            case 'add_neighborhood':
                return sprintf( 'Added neighborhood "%s" to city "%s" (%s)', $details['neighborhood'] ?? '?', $details['city'] ?? '?', $country );
            case 'delete_neighborhood':
                return sprintf( 'Deleted neighborhood "%s" from city "%s" (%s)', $details['neighborhood'] ?? '?', $details['city'] ?? '?', $country );
            default:
                return 'Geo data changed: ' . $action;
        }
    }
}
