<?php

namespace BexioConnectorByHelvy;

/**
 * Robust Sync Manager to prevent duplicate syncing
 *
 * @since 0.1.0
 */

defined('ABSPATH') || exit;

class SyncManager
{
    use Singleton;

    const LOCK_TIMEOUT = 300; // 5 minutes
    const OPERATION_TIMEOUT = 120; // 2 minutes per operation
    
    private $locks_cache = [];

    public function __construct() {
        // Clean up expired locks on init
        $this->cleanup_expired_locks();
        
        // Schedule periodic cleanup
        if (!wp_next_scheduled('bexio_cleanup_sync_locks')) {
            wp_schedule_event(time(), 'hourly', 'bexio_cleanup_sync_locks');
        }
        add_action('bexio_cleanup_sync_locks', [$this, 'cleanup_expired_locks']);
    }

    /**
     * Execute a sync operation with atomic locking
     * 
     * @param string $operation_type Type of operation (order, invoice, payment, etc.)
     * @param int $order_id WooCommerce order ID
     * @param callable $callback Function to execute
     * @param array $args Arguments to pass to callback
     * @return mixed Result of callback or error
     */
    public function execute_sync_operation($operation_type, $order_id, $callback, $args = []) {
        $lock_key = $this->get_lock_key($operation_type, $order_id);
        $operation_id = $this->generate_operation_id($operation_type, $order_id);
        
        // Check if this exact operation is already completed
        if ($this->is_operation_completed($operation_type, $order_id)) {
            return ['status' => 'SKIPPED', 'reason' => "Operation {$operation_type} already completed for order {$order_id}"];
        }
        
        // Try to acquire atomic lock
        if (!$this->acquire_lock($lock_key, $operation_id)) {
            return ['status' => 'LOCKED', 'reason' => "Another sync operation is in progress for order {$order_id}"];
        }

        try {
            // Double-check completion status after acquiring lock
            if ($this->is_operation_completed($operation_type, $order_id)) {
                return ['status' => 'SKIPPED', 'reason' => "Operation {$operation_type} completed by another process"];
            }

            // Mark operation as started
            $this->mark_operation_started($operation_type, $order_id, $operation_id);
            
            // Execute the sync operation
            $result = call_user_func_array($callback, $args);
            
            // Mark operation as completed if successful
            if (!($result instanceof \WP_Error) && !$this->is_error_result($result)) {
                $this->mark_operation_completed($operation_type, $order_id, $operation_id);
            }
            
            return $result;
            
        } catch (\Throwable $e) {
            _bcbh_log("Sync operation failed: " . $e->getMessage());
            return new \WP_Error('SYNC_OPERATION_FAILED', $e->getMessage());
        } finally {
            // Always release the lock
            $this->release_lock($lock_key, $operation_id);
        }
    }

    /**
     * Acquire an atomic lock using WordPress transients
     */
    private function acquire_lock($lock_key, $operation_id) {
        $cache_key = 'sync_lock_' . $lock_key;
        
        // Try to set transient atomically
        $lock_data = [
            'operation_id' => $operation_id,
            'timestamp' => time(),
            'pid' => _bcbh_getpid(),
        ];
        
        // Use set_transient with expiration - it returns false if already exists
        $acquired = set_transient($cache_key, $lock_data, self::LOCK_TIMEOUT);
        
        if ($acquired) {
            $this->locks_cache[$lock_key] = $operation_id;
            _bcbh_log("🔒 Lock acquired for {$lock_key} with operation {$operation_id}");
            return true;
        }
        
        // Check if existing lock is stale
        $existing_lock = get_transient($cache_key);
        if ($existing_lock && isset($existing_lock['timestamp'])) {
            $age = time() - $existing_lock['timestamp'];
            if ($age > self::LOCK_TIMEOUT) {
                // Force release stale lock and try again
                delete_transient($cache_key);
                $acquired = set_transient($cache_key, $lock_data, self::LOCK_TIMEOUT);
                if ($acquired) {
                    $this->locks_cache[$lock_key] = $operation_id;
                    _bcbh_log("🔒 Lock acquired for {$lock_key} after clearing stale lock");
                    return true;
                }
            }
        }
        
        _bcbh_log("❌ Failed to acquire lock for {$lock_key}");
        return false;
    }

    /**
     * Release a lock
     */
    private function release_lock($lock_key, $operation_id) {
        $cache_key = 'sync_lock_' . $lock_key;
        $existing_lock = get_transient($cache_key);
        
        // Only release if we own the lock
        if ($existing_lock && isset($existing_lock['operation_id']) && $existing_lock['operation_id'] === $operation_id) {
            delete_transient($cache_key);
            unset($this->locks_cache[$lock_key]);
            _bcbh_log("🔓 Lock released for {$lock_key}");
        }
    }

    /**
     * Generate unique operation ID
     */
    private function generate_operation_id($operation_type, $order_id) {
        return md5($operation_type . '_' . $order_id . '_' . time() . '_' . uniqid());
    }

    /**
     * Get lock key for operation
     */
    private function get_lock_key($operation_type, $order_id) {
        return md5("bexio_sync_{$operation_type}_{$order_id}");
    }

    /**
     * Check if operation is already completed
     */
    private function is_operation_completed($operation_type, $order_id) {
        $completion_key = AdminAPISettings::$prefix . "sync_completed_{$operation_type}";
        $completed_operations = get_post_meta($order_id, $completion_key, true);
        
        if (!$completed_operations) {
            return false;
        }
        
        // Check if operation was completed in the last hour (prevents re-sync of legitimate updates)
        if (is_array($completed_operations)) {
            $latest_completion = max($completed_operations);
            return (time() - $latest_completion) < 3600; // 1 hour
        }
        
        return (time() - $completed_operations) < 3600;
    }

    /**
     * Mark operation as started
     */
    private function mark_operation_started($operation_type, $order_id, $operation_id) {
        $start_key = AdminAPISettings::$prefix . "sync_started_{$operation_type}";
        update_post_meta($order_id, $start_key, [
            'operation_id' => $operation_id,
            'timestamp' => time(),
            'pid' => _bcbh_getpid()
        ]);
    }

    /**
     * Mark operation as completed
     */
    private function mark_operation_completed($operation_type, $order_id, $operation_id) {
        $completion_key = AdminAPISettings::$prefix . "sync_completed_{$operation_type}";
        $existing_completions = get_post_meta($order_id, $completion_key, true);
        
        if (!is_array($existing_completions)) {
            $existing_completions = $existing_completions ? [$existing_completions] : [];
        }
        
        $existing_completions[] = time();
        
        // Keep only last 5 completion timestamps
        $existing_completions = array_slice($existing_completions, -5);
        
        update_post_meta($order_id, $completion_key, $existing_completions);
        
        // Clean up start marker
        $start_key = AdminAPISettings::$prefix . "sync_started_{$operation_type}";
        delete_post_meta($order_id, $start_key);
        
        _bcbh_log("✅ Operation {$operation_type} completed for order {$order_id}");
    }

    /**
     * Check if result indicates an error or should not be marked as completed
     */
    private function is_error_result($result) {
        if ($result instanceof \WP_Error) {
            return true;
        }
        
        if (is_array($result) && isset($result['status'])) {
            // Don't mark as completed if the operation was skipped or failed
            if (in_array($result['status'], ['ERROR', 'SKIPPED', 'LOCKED'])) {
                return true;
            }
        }
        
        return false;
    }

    /**
     * Cleanup expired locks and operations
     */
    public function cleanup_expired_locks() {
        global $wpdb;
        
        // Clean up expired transients (WordPress doesn't auto-clean them perfectly)
        $wpdb->query($wpdb->prepare(
            "DELETE FROM {$wpdb->options} 
             WHERE option_name LIKE %s 
             AND option_value < %d",
            '_transient_timeout_sync_lock_%',
            time()
        ));
        
        // Clean up old operation start markers
        $start_meta_keys = $wpdb->get_results($wpdb->prepare(
            "SELECT post_id, meta_key, meta_value 
             FROM {$wpdb->postmeta} 
             WHERE meta_key LIKE %s",
            AdminAPISettings::$prefix . 'sync_started_%'
        ));
        
        foreach ($start_meta_keys as $meta) {
            $meta_value = maybe_unserialize($meta->meta_value);
            if (is_array($meta_value) && isset($meta_value['timestamp'])) {
                $age = time() - $meta_value['timestamp'];
                if ($age > self::OPERATION_TIMEOUT) {
                    delete_post_meta($meta->post_id, $meta->meta_key);
                    _bcbh_log("🧹 Cleaned up stale operation marker: {$meta->meta_key} for post {$meta->post_id}");
                }
            }
        }
    }

    /**
     * Get all supported operation types
     */
    private function get_all_operation_types() {
        return ['order', 'delivery', 'invoice_create', 'invoice_issue', 'payment', 'fulfillment'];
    }

    /**
     * Get sync status for an order
     */
    public function get_sync_status($order_id) {
        $operations = $this->get_all_operation_types();
        $status = [];
        
        foreach ($operations as $operation) {
            $completion_key = AdminAPISettings::$prefix . "sync_completed_{$operation}";
            $start_key = AdminAPISettings::$prefix . "sync_started_{$operation}";
            
            $completed = get_post_meta($order_id, $completion_key, true);
            $started = get_post_meta($order_id, $start_key, true);
            
            $status[$operation] = [
                'completed' => !empty($completed),
                'last_completed' => is_array($completed) ? max($completed) : $completed,
                'in_progress' => !empty($started),
                'started_at' => is_array($started) && isset($started['timestamp']) ? $started['timestamp'] : null
            ];
        }
        
        return $status;
    }

    /**
     * Reset sync status for an order (for manual re-sync)
     */
    public function reset_sync_status($order_id, $operation_types = null) {
        if ($operation_types === null) {
            $operation_types = $this->get_all_operation_types();
        }
        
        if (!is_array($operation_types)) {
            $operation_types = [$operation_types];
        }
        
        foreach ($operation_types as $operation) {
            $completion_key = AdminAPISettings::$prefix . "sync_completed_{$operation}";
            $start_key = AdminAPISettings::$prefix . "sync_started_{$operation}";
            
            delete_post_meta($order_id, $completion_key);
            delete_post_meta($order_id, $start_key);
        }
        
        _bcbh_log("🔄 Reset sync status for order {$order_id}, operations: " . implode(', ', $operation_types));
    }

    /**
     * Force release all locks for an order (emergency use)
     */
    public function force_release_order_locks($order_id) {
        $operations = $this->get_all_operation_types();
        
        foreach ($operations as $operation) {
            $lock_key = $this->get_lock_key($operation, $order_id);
            $cache_key = 'sync_lock_' . $lock_key;
            delete_transient($cache_key);
            unset($this->locks_cache[$lock_key]);
        }
        
        _bcbh_log("🚨 Force released all locks for order {$order_id}");
    }
}