<?php
/**
 * VmpFence Base Sync Module
 *
 * Abstract base class for all SIEM sync modules.
 * Each module syncs unsynced records from a local WordPress table
 * to the Newton Security backend in batches.
 *
 * Handles: batch fetching, transformation, API call, marking synced.
 *
 * @package    VMPFence
 * @subpackage Includes\Sync
 * @since      2.2.4
 */

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

abstract class VmpFence_Base_Sync {

    /**
     * WordPress table name (with prefix, e.g., wp_vmpfence_audit_log)
     * Set by child class constructor
     * @var string
     */
    protected $table_name;

    /**
     * Records per API call
     * @var int
     */
    protected $batch_size = 50;

    /**
     * API client instance
     * @var VMPFence_API_Client
     */
    protected $api_client;

    /**
     * Site UUID (server-assigned)
     * @var string
     */
    protected $site_uuid;

    /**
     * Get the API client method name for this sync module.
     * E.g., 'sync_audit_logs_batch'
     *
     * @since 2.2.4
     * @return string
     */
    abstract protected function get_endpoint_method(): string;

    /**
     * Transform a database row into the API payload format.
     * Each sync module implements its own field mapping.
     *
     * @since 2.2.4
     * @param array $row Database row (associative array)
     * @return array Transformed record for API
     */
    abstract protected function transform_record(array $row): array;

    /**
     * Constructor
     *
     * @since 2.2.4
     */
    public function __construct() {
        if (class_exists('VMPFence_API_Client')) {
            $this->api_client = new VMPFence_API_Client();
        }
        $this->refresh_site_uuid();
    }

    /**
     * Refresh site UUID from wp_options.
     * Called at construction and again at sync() start to pick up
     * any UUID that was obtained between construction and sync time.
     *
     * @since 2.2.4
     * @return void
     */
    protected function refresh_site_uuid() {
        $this->site_uuid = get_option(VMPFence_Constants::OPTION_SITE_UUID, '');
    }

    /**
     * Run the sync cycle: fetch unsynced → transform → send → mark synced.
     *
     * Processes batches until no unsynced records remain, or 3 consecutive
     * errors occur. Returns a summary array.
     *
     * @since 2.2.4
     * @return array {
     *     @type bool   $success Whether sync completed without fatal errors
     *     @type int    $synced  Total records successfully synced
     *     @type int    $batches Number of batches processed
     *     @type int    $errors  Number of batch errors encountered
     *     @type string $error   Error message (if $success is false)
     * }
     */
    public function sync(): array {
        // Refresh UUID — may have been obtained since this object was constructed
        $this->refresh_site_uuid();

        // Validate prerequisites
        if (empty($this->site_uuid) || strpos($this->site_uuid, 'temp-') === 0) {
            return array('success' => false, 'error' => 'No valid site UUID');
        }

        if (!$this->api_client) {
            return array('success' => false, 'error' => 'API client not available');
        }

        $api_key = '';
        if (class_exists('VMPFence_Config')) {
            $api_key = VMPFence_Config::get('central_api_key', '');
        }
        if (empty($api_key)) {
            return array('success' => false, 'error' => 'No API key configured');
        }

        global $wpdb;
        $created = 0;
        $consecutive_errors = 0;
        $batches = 0;

        while (true) {
            // Fetch unsynced records
            $rows = $wpdb->get_results($wpdb->prepare(
                "SELECT * FROM {$this->table_name} WHERE synced = 0 ORDER BY id ASC LIMIT %d",
                $this->batch_size
            ), ARRAY_A);

            if (empty($rows)) {
                break; // No more unsynced records
            }

            // Transform records to API format
            $records = array();
            foreach ($rows as $row) {
                try {
                    $records[] = $this->transform_record($row);
                } catch (\Exception $e) {
                    // Skip malformed records but log them
                    if (class_exists('VMPFence_Secure_Logger')) {
                        VMPFence_Secure_Logger::log('warning', 'SIEM sync transform error: ' . $e->getMessage(), array(
                            'table' => $this->table_name,
                            'row_id' => isset($row['id']) ? $row['id'] : 'unknown',
                        ));
                    }
                }
            }

            if (empty($records)) {
                // All rows in this batch failed to transform — mark them synced to avoid infinite loop
                $ids = wp_list_pluck($rows, 'id');
                $placeholders = implode(',', array_fill(0, count($ids), '%d'));
                $wpdb->query($wpdb->prepare(
                    "UPDATE {$this->table_name} SET synced = 1, synced_at = %s WHERE id IN ($placeholders)",
                    array_merge(array(current_time('mysql')), $ids)
                ));
                continue;
            }

            // Call the API
            $method = $this->get_endpoint_method();
            $response = $this->api_client->$method($api_key, $this->site_uuid, $records);

            if (is_wp_error($response)) {
                $consecutive_errors++;
                if (class_exists('VMPFence_Secure_Logger')) {
                    VMPFence_Secure_Logger::log('warning', 'SIEM batch sync error', array(
                        'method' => $method,
                        'error'  => $response->get_error_message(),
                        'batch'  => $batches + 1,
                    ));
                }
                if ($consecutive_errors >= 3) {
                    // Force-mark this batch as synced to unblock the queue.
                    // Without this, the same failing batch would be re-fetched
                    // every cron run, permanently blocking all later records.
                    $ids = wp_list_pluck($rows, 'id');
                    $placeholders = implode(',', array_fill(0, count($ids), '%d'));
                    $wpdb->query($wpdb->prepare(
                        "UPDATE {$this->table_name} SET synced = 1, synced_at = %s WHERE id IN ($placeholders)",
                        array_merge(array(current_time('mysql')), $ids)
                    ));
                    if (class_exists('VMPFence_Secure_Logger')) {
                        VMPFence_Secure_Logger::log('error', 'SIEM batch force-skipped after 3 consecutive failures', array(
                            'table'      => $this->table_name,
                            'skipped_ids' => count($ids),
                        ));
                    }
                    break;
                }
                continue;
            }

            // Reset consecutive error counter on success
            $consecutive_errors = 0;

            // Mark records as synced
            $ids = wp_list_pluck($rows, 'id');
            $placeholders = implode(',', array_fill(0, count($ids), '%d'));
            $wpdb->query($wpdb->prepare(
                "UPDATE {$this->table_name} SET synced = 1, synced_at = %s WHERE id IN ($placeholders)",
                array_merge(array(current_time('mysql')), $ids)
            ));

            $created += isset($response['created']) ? (int) $response['created'] : count($ids);
            $batches++;

            // If we got fewer records than batch_size, we're done
            if (count($rows) < $this->batch_size) {
                break;
            }

            // Throttle between batches (200ms)
            usleep(200000);
        }

        return array(
            'success' => $consecutive_errors === 0,
            'synced'  => $created,
            'batches' => $batches,
            'errors'  => $consecutive_errors,
        );
    }

    /**
     * Get the count of unsynced records for this module.
     *
     * @since 2.2.4
     * @return int
     */
    public function get_unsynced_count(): int {
        global $wpdb;

        $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $this->table_name));
        if (!$table_exists) {
            return 0;
        }

        return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$this->table_name} WHERE synced = 0");
    }

    /**
     * Safely convert a binary IP to string representation.
     * The plugin stores IPs as varbinary(16). The backend expects string IPs.
     *
     * @since 2.2.4
     * @param mixed $binary_ip Binary IP from database
     * @return string|null Human-readable IP address, or null
     */
    protected function format_ip($binary_ip) {
        if (empty($binary_ip)) {
            return null;
        }

        // If it's already a string IP (not binary), return as-is
        if (is_string($binary_ip) && (filter_var($binary_ip, FILTER_VALIDATE_IP) !== false)) {
            return $binary_ip;
        }

        $ip = @inet_ntop($binary_ip);
        return ($ip !== false) ? $ip : null;
    }
}
