<?php
/**
 * Dispatcher Class
 *
 * Single unified dispatcher that handles all request types (REST, AJAX, Web)
 * with automatic type detection and proper response formatting.
 *
 * @package Licenzo\Framework\Foundation
 */

namespace Licenzo\Framework\Foundation;

use Licenzo\Framework\Http\Request;
use Licenzo\Framework\Http\RequestType;
use WP_Error;
use WP_REST_Response;

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Unified request dispatcher
 */
class Dispatcher
{
    /**
     * Application instance
     */
    protected Application $app;

    /**
     * Create a new dispatcher instance
     */
    public function __construct(Application $app)
    {
        $this->app = $app;
    }

    /**
     * Dispatch request - unified method for all request types
     *
     * @param mixed $request Request object (WP_REST_Request, Request, or null)
     * @param mixed $action Controller action (optional, can be in context)
     * @param array $context Additional context (route, params, type, etc.)
     * @return mixed Response based on a request type
     * @throws \Throwable
     */
    public function dispatch($request = null, $action = null, array $context = [])
    {
        $type = null;

        if(!RequestType::isApi()) {
            return;
        }

        try {
            // 1. Normalize request
            $request = $this->normalizeRequest($request);

            // 2. Detect request type (using WordPress constants)
            $type = $this->detectType($context);

            // 3. Resolve action (if not provided)
            if ($action === null) {
                $action = $this->resolveAction($request, $context);
            }

            // 4. Prepare parameters for controller invocation
            $parameters = $this->prepareParameters($request, $context);

            // 5. Invoke controller with dependency injection
            $response = $this->invokeController($action, $parameters);

            // 6. Format response based on type
            return $this->formatResponse($response, $type);

        } catch (\Throwable $e) {
            // Handle exception based on type
            $type = $type ?? $this->detectType($context);
            return $this->formatException($e, $type);
        }
    }

    /**
     * Normalize request to framework Request
     */
    protected function normalizeRequest($request): Request
    {
        if ($request instanceof Request) {
            return $request;
        }

        if ($request instanceof \WP_REST_Request) {
            return Request::createFromWordPressRequest($request);
        }

        // Capture current request
        return Request::capture();
    }

    /**
     * Detect request type using WordPress constants
     */
    protected function detectType(array $context): string
    {
        // 1. Explicit type in context (highest priority)
        if (isset($context['type'])) {
            return $context['type'];
        }

        // 2. Check if REST API (using WordPress constant)
        if (defined('REST_REQUEST') && REST_REQUEST) {
            return 'rest';
        }

        // 3. Check if has route in context (REST API)
        if (isset($context['route'])) {
            return 'rest';
        }

        // 4. Check if AJAX (using WordPress constant)
        if (defined('DOING_AJAX') && DOING_AJAX) {
            return 'ajax';
        }

        // 5. Check if AJAX (using WordPress function)
        if (function_exists('wp_doing_ajax') && wp_doing_ajax()) {
            return 'ajax';
        }

        // 6. Check if CRON
        if (defined('DOING_CRON') && DOING_CRON) {
            return 'cron';
        }

        // 7. Check if admin page
        if (is_admin()) {
            return 'web';
        }

        // 8. Default to web (frontend)
        return 'web';
    }

    /**
     * Resolve action from context or route
     */
    protected function resolveAction(Request $request, array $context)
    {
        $action = null;

        // From context
        if (isset($context['action'])) {
            $action = $context['action'];
        }
        // From route
        elseif (isset($context['route']) && method_exists($context['route'], 'getAction')) {
            $action = $context['route']->getAction();
        }
        // From request attributes
        elseif ($request->attributes->has('action')) {
            $action = $request->attributes->get('action');
        }

        if ($action === null) {
            throw new \InvalidArgumentException('No action provided');
        }

        // Normalize action format for Application->call()
        // Convert [ControllerClass::class, 'method'] to 'ControllerClass@method'
        if (is_array($action) && count($action) === 2 && is_string($action[0])) {
            return $action[0] . '@' . $action[1];
        }

        return $action;
    }

    /**
     * Prepare parameters for Application->call()
     *
     * Application->call() will handle dependency injection,
     * we just need to provide route parameters and request
     */
    protected function prepareParameters(Request $request, array $context): array
    {
        $parameters = [];

        // Add route parameters (for REST API)
        if (isset($context['route_params'])) {
            $parameters = array_merge($parameters, $context['route_params']);
        }

        if (isset($context['route']) && method_exists($context['route'], 'getParameters')) {
            $parameters = array_merge($parameters, $context['route']->getParameters());
        }

        // Add custom parameters
        if (isset($context['params'])) {
            $parameters = array_merge($parameters, $context['params']);
        }

        // Add request object
        $parameters['request'] = $request;

        return $parameters;
    }

    /**
     * Format response based on request type
     */
    protected function formatResponse($response, string $type)
    {
        // Handle WP_Error first (common to all types)
        if (is_wp_error($response)) {
            return $this->formatError($response, $type);
        }

        // Format based on type
        return match($type) {
            'rest' => $this->formatRest($response),
            'ajax' => $this->formatAjax($response),
            'web' => $this->formatWeb($response),
            'cron' => $response, // Return as-is for cron
            default => throw new \InvalidArgumentException("Unknown request type: {$type}"),
        };
    }

    /**
     * Format exception based on request type
     */
    protected function formatException(\Throwable $e, string $type)
    {
        return match($type) {
            'rest' => $this->formatRestException($e),
            'ajax' => $this->formatAjaxException($e),
            'web' => $this->formatWebException($e),
            'cron' => throw $e, // Re-throw for cron
            default => throw $e,
        };
    }

    // ==================== REST FORMATTING ====================

    /**
     * Format REST API response
     */
    protected function formatRest($response): WP_REST_Response
    {
        if ($response instanceof WP_REST_Response) {
            return $this->addProcessingTime($response);
        }

        $data = is_array($response) ? $response : ['data' => $response];
        $data['processing_time_ms'] = $this->getProcessingTime();
        $status = 200;
        if (is_array($response) && isset($response['status']) && is_numeric($response['status'])) {
            $status = (int) $response['status'];
            if ($status < 100 || $status > 599) {
                $status = 200;
            }
        }

        return rest_ensure_response(new WP_REST_Response($data, $status));
    }

    /**
     * Format REST API exception
     */
    protected function formatRestException(\Throwable $e): WP_REST_Response
    {
        $data = [
            'success' => false,
            'message' => $e->getMessage(),
            'processing_time_ms' => $this->getProcessingTime(),
        ];

        // Add trace in debug mode
        if (defined('WP_DEBUG') && WP_DEBUG) {
            $data['trace'] = $e->getTraceAsString();
        }

        // Determine status code based on exception type
        $status = match(true) {
            $e instanceof \Licenzo\Framework\Exceptions\ValidationException => 400,
            $e instanceof \Licenzo\Framework\Exceptions\AuthorizationException => 403,
            $e instanceof \Licenzo\Framework\Exceptions\ModelNotFoundException => 404,
            default => 500,
        };

        return rest_ensure_response(new WP_REST_Response($data, $status));
    }

    // ==================== AJAX FORMATTING ====================

    /**
     * Format AJAX response
     */
    protected function formatAjax($response): void
    {
        $data = is_array($response) ? $response : ['data' => $response];
        $data['processing_time_ms'] = $this->getProcessingTime();

        wp_send_json_success($data);
        // Note: wp_send_json_success() exits
    }

    /**
     * Format AJAX exception
     */
    protected function formatAjaxException(\Throwable $e): void
    {
        $data = [
            'message' => $e->getMessage(),
            'processing_time_ms' => $this->getProcessingTime(),
        ];

        // Add trace in debug mode
        if (defined('WP_DEBUG') && WP_DEBUG) {
            $data['trace'] = $e->getTraceAsString();
        }

        wp_send_json_error($data);
        // Note: wp_send_json_error() exits
    }

    // ==================== WEB FORMATTING ====================

    /**
     * Format web response
     */
    protected function formatWeb($response)
    {
        // 1. View file (most common for web)
        if (is_array($response) && isset($response['view'])) {
            return $this->renderView($response['view'], $response['data'] ?? []);
        }

        // 2. Redirect
        if (is_array($response) && isset($response['redirect'])) {
            wp_redirect($response['redirect']);
            exit;
        }

        // 3. HTML string
        if (is_string($response)) {
            echo $response;
            return;
        }

        // 4. Raw data
        return $response;
    }

    /**
     * Render view file using output buffering
     *
     * This handles view files that contain echo statements.
     * extract() with EXTR_SKIP is standard practice for view/template
     * engines (used by Laravel Blade, WordPress core get_template_part, etc.)
     * to make passed data available as local variables in the view scope.
     */
    protected function renderView(string $viewPath, array $data = []): void
    {
        // phpcs:ignore WordPress.PHP.DontExtract.extract_extract -- View renderer, standard template engine pattern
        extract($data, EXTR_SKIP);

        // Start output buffering
        ob_start();

        try {
            // Include view file
            include $viewPath;

            // Get buffered content and clean buffer
            $content = ob_get_clean();

            // Echo the content
            echo $content;

        } catch (\Throwable $e) {
            // Clean buffer on error
            ob_end_clean();
            throw $e;
        }
    }

    /**
     * Format web exception
     */
    protected function formatWebException(\Throwable $e): void
    {
        // In debug mode, show detailed error
        if (defined('WP_DEBUG') && WP_DEBUG) {
            $message = sprintf(
                '<h1>Error</h1><p>%s</p><pre>%s</pre>',
                esc_html($e->getMessage()),
                esc_html($e->getTraceAsString())
            );
            wp_die($message, 'Error', ['response' => 500]);
        }

        // In production, show generic error
        wp_die('An error occurred. Please try again later.', 'Error', ['response' => 500]);
    }

    // ==================== ERROR FORMATTING ====================

    /**
     * Format WP_Error based on request type
     */
    protected function formatError(WP_Error $error, string $type)
    {
        return match($type) {
            'rest' => $this->formatRestError($error),
            'ajax' => $this->formatAjaxError($error),
            'web' => $this->formatWebError($error),
            default => $error,
        };
    }

    /**
     * Format REST API WP_Error
     */
    protected function formatRestError(WP_Error $error): WP_REST_Response
    {
        $data = [
            'success' => false,
            'message' => $error->get_error_message(),
            'code' => $error->get_error_code(),
            'processing_time_ms' => $this->getProcessingTime(),
        ];

        return rest_ensure_response(new WP_REST_Response($data, 400));
    }

    /**
     * Format AJAX WP_Error
     */
    protected function formatAjaxError(WP_Error $error): void
    {
        wp_send_json_error([
            'message' => $error->get_error_message(),
            'code' => $error->get_error_code(),
            'processing_time_ms' => $this->getProcessingTime(),
        ]);
    }

    /**
     * Format web WP_Error
     */
    protected function formatWebError(WP_Error $error): void
    {
        wp_die(esc_html($error->get_error_message()), 'Error', ['response' => 500]);
    }

    // ==================== HELPERS ====================

    /**
     * Invoke controller with dependency injection
     *
     * Handles 'ControllerClass@method' format and resolves dependencies
     */
    protected function invokeController($action, array $parameters = [])
    {
        // Parse 'ControllerClass@method' format
        if (is_string($action) && strpos($action, '@') !== false) {
            list($class, $method) = explode('@', $action, 2);

            // Resolve controller from container
            $controller = $this->app->make($class);

            // Call method with parameters
            return $this->callMethod($controller, $method, $parameters);
        }

        // Handle callable (closure, array, etc.)
        if (is_callable($action)) {
            return call_user_func_array($action, $parameters);
        }

        throw new \InvalidArgumentException('Invalid action format: ' . print_r($action, true));
    }

    /**
     * Call controller method with dependency injection
     */
    protected function callMethod($controller, string $method, array $parameters = [])
    {
        if (!method_exists($controller, $method)) {
            throw new \BadMethodCallException(
                'Method ' . $method . ' does not exist on ' . get_class($controller)
            );
        }

        $reflection = new \ReflectionMethod($controller, $method);
        $dependencies = [];

        foreach ($reflection->getParameters() as $parameter) {
            $name = $parameter->getName();
            $type = $parameter->getType();

            // 1. Check if parameter is provided
            if (array_key_exists($name, $parameters)) {
                $dependencies[] = $parameters[$name];
                continue;
            }

            // 2. Try to resolve by type hint
            if ($type && !$type->isBuiltin()) {
                $className = $type->getName();

                // Special handling for Request
                if ($className === 'Licenzo\Framework\Http\Request' || is_subclass_of($className, 'Licenzo\Framework\Http\Request')) {
                    $dependencies[] = $parameters['request'] ?? $this->app->make('request');
                    continue;
                }

                // Resolve from container
                try {
                    $dependencies[] = $this->app->make($className);
                    continue;
                } catch (\Exception $e) {
                    // Continue to next check
                }
            }

            // 3. Check if parameter has default value
            if ($parameter->isDefaultValueAvailable()) {
                $dependencies[] = $parameter->getDefaultValue();
                continue;
            }

            // 4. Parameter is required but not provided
            throw new \InvalidArgumentException(
                'Unable to resolve parameter $' . $name . ' for ' .
                get_class($controller) . '::' . $method
            );
        }

        return $reflection->invokeArgs($controller, $dependencies);
    }

    /**
     * Get processing time from application start
     */
    protected function getProcessingTime(): float
    {
        return round((microtime(true) - $this->app->getStartTime()) * 1000, 2);
    }

    /**
     * Add processing time to WP_REST_Response
     */
    protected function addProcessingTime(WP_REST_Response $response): WP_REST_Response
    {
        $data = $response->get_data();
        $data['processing_time_ms'] = $this->getProcessingTime();
        $response->set_data($data);

        return rest_ensure_response($response);
    }
}
