<?php
/**
 * DataLoader - Dynamic Data Loading for UltraGuard Pro
 *
 * Central data loading service that resolves data sources for widgets,
 * AJAX handlers, and the render engine. Supports multiple source types:
 * callback, option, transient, table, static, and filter.
 *
 * @package    UltraguardPro\view
 * @author     Vikas Bhardwaj
 * @version    1.0.0
 * @since      1.0.0
 */

namespace UltraguardPro\view;

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

/**
 * DataLoader class
 *
 * Resolves 'source' descriptors into actual data values. A source descriptor
 * is an array with a 'type' key that tells the loader how to fetch the data:
 *
 *   ['type'=>'callback',  'callback'=>callable, 'args'=>[]]
 *   ['type'=>'option',    'key'=>'ultraguard_some_option', 'default'=>null]
 *   ['type'=>'transient', 'key'=>'some_transient', 'default'=>null]
 *   ['type'=>'table',     'table'=>'scan_results', 'method'=>'get_all', 'args'=>[]]
 *   ['type'=>'filter',    'tag'=>'ultraguard_load_some_data', 'default'=>null]
 *   ['type'=>'static',    'value'=><anything>]
 *
 * A plain string source is treated as an option key.
 */
class DataLoader {

    /** @var DataLoader|null */
    private static $instance = null;

    /** @var array<string,mixed> request-level cache */
    private $cache = [];

    private $db_tables = null;

    // -------------------------------------------------------------------------

    private function __construct() {
        // Lazy-resolve db_tables when first needed
    }

    public static function get_instance(): self {
        if (null === self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    // -------------------------------------------------------------------------
    // PUBLIC API
    // -------------------------------------------------------------------------

    /**
     * Load data from a source descriptor.
     *
     * @param  array|string $source  Source descriptor or option key string.
     * @param  array        $context Optional context passed to callbacks / filters.
     * @return mixed                 Resolved value or null on failure.
     */
    public function load($source, array $context = []) {
        // Normalise plain-string source to an option lookup
        if (is_string($source)) {
            $source = ['type' => 'option', 'key' => $source];
        }

        if (!is_array($source) || empty($source['type'])) {
            return null;
        }

        $cache_key = $this->make_cache_key($source, $context);
        if (array_key_exists($cache_key, $this->cache)) {
            return $this->cache[$cache_key];
        }

        $value = $this->resolve($source, $context);

        // Allow global filtering of loaded data
        $value = apply_filters('ultraguard_data_loader_result', $value, $source, $context);

        $this->cache[$cache_key] = $value;
        return $value;
    }

    /**
     * Invalidate a specific cache entry.
     *
     * @param  array|string $source
     * @param  array        $context
     */
    public function invalidate($source, array $context = []): void {
        if (is_string($source)) {
            $source = ['type' => 'option', 'key' => $source];
        }
        $cache_key = $this->make_cache_key($source, $context);
        unset($this->cache[$cache_key]);
    }

    /** Flush the entire request-level cache. */
    public function flush_cache(): void {
        $this->cache = [];
    }

    // -------------------------------------------------------------------------
    // PRIVATE RESOLUTION
    // -------------------------------------------------------------------------

    /**
     * Dispatch to the correct resolver based on source type.
     */
    private function resolve(array $source, array $context) {
        $type = sanitize_key($source['type']);

        switch ($type) {
            case 'callback':
                return $this->resolve_callback($source, $context);

            case 'option':
                return $this->resolve_option($source);

            case 'transient':
                return $this->resolve_transient($source);

            case 'table':
                return $this->resolve_table($source, $context);

            case 'filter':
                return $this->resolve_filter($source, $context);

            case 'static':
                return $source['value'] ?? null;

            default:
                // Allow custom resolvers via filter
                return apply_filters("ultraguard_data_loader_resolve_{$type}", null, $source, $context);
        }
    }

    /**
     * Resolve a callable source.
     * source: ['type'=>'callback', 'callback'=>callable, 'args'=>[]]
     */
    private function resolve_callback(array $source, array $context) {
        $callback = $source['callback'] ?? null;
        if (!is_callable($callback)) {
            return null;
        }
        $args = $source['args'] ?? [];
        if (!is_array($args)) {
            $args = [$args];
        }
        // Always append context as the last argument
        $args[] = $context;
        try {
            return call_user_func_array($callback, $args);
        } catch (\Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                \UltraguardPro\core\Logger::error( 'UltraGuard DataLoader callback error: ' . $e->getMessage() );
            }
            return null;
        }
    }

    /**
     * Resolve a WordPress option.
     * source: ['type'=>'option', 'key'=>'option_name', 'default'=>null]
     */
    private function resolve_option(array $source) {
        $key     = $source['key']     ?? '';
        $default = $source['default'] ?? null;
        if (empty($key)) {
            return $default;
        }
        return get_option($key, $default);
    }

    /**
     * Resolve a WordPress transient.
     * source: ['type'=>'transient', 'key'=>'transient_name', 'default'=>null]
     */
    private function resolve_transient(array $source) {
        $key     = $source['key']     ?? '';
        $default = $source['default'] ?? null;
        if (empty($key)) {
            return $default;
        }
        $value = CacheManager::get_instance()->get_db($key);
        return ($value === false) ? $default : $value;
    }

    /**
     * Resolve data from a plugin database table.
     * source: ['type'=>'table', 'table'=>'table_key', 'method'=>'method_name', 'args'=>[]]
     */
    private function resolve_table(array $source, array $context) {
        $table_key = $source['table']  ?? '';
        $method    = $source['method'] ?? 'get_all';
        $args      = $source['args']   ?? [];

        if (empty($table_key)) {
            return null;
        }

        $db_tables = $this->get_db_tables();
        if (!$db_tables) {
            return null;
        }

        $table = $db_tables->get_table($table_key);
        if (!$table || !method_exists($table, $method)) {
            return null;
        }

        if (!is_array($args)) {
            $args = [$args];
        }

        try {
            return call_user_func_array([$table, $method], $args);
        } catch (\Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                \UltraguardPro\core\Logger::error( "UltraGuard DataLoader table error [{$table_key}::{$method}]: " . $e->getMessage() );
            }
            return null;
        }
    }

    /**
     * Resolve data via a WordPress filter.
     * source: ['type'=>'filter', 'tag'=>'hook_name', 'default'=>null]
     */
    private function resolve_filter(array $source, array $context) {
        $tag     = $source['tag']     ?? '';
        $default = $source['default'] ?? null;
        if (empty($tag)) {
            return $default;
        }
        return apply_filters($tag, $default, $context);
    }

    // -------------------------------------------------------------------------
    // HELPERS
    // -------------------------------------------------------------------------

    private function make_cache_key($source, array $context): string {
        return md5(serialize($source) . serialize($context));
    }

    private function get_db_tables() {
        if ($this->db_tables !== null) {
            return $this->db_tables;
        }
        if (function_exists('\UltraguardPro\ultraguard_pro')) {
            $plugin = \UltraguardPro\ultraguard_pro();
            if ($plugin && isset($plugin->db_tables)) {
                $this->db_tables = $plugin->db_tables;
            }
        }
        return $this->db_tables;
    }
}
