<?php

namespace TableBuilderPro\Routes;

defined('ABSPATH') || exit;

class DataTableRoute
{
    use \TableBuilderPro\Traits\Singleton;

    public function __construct()
    {
        add_action('rest_api_init', [$this, 'register_routes']);
    }

    public function register_routes()
    {
        register_rest_route('table-builder/v1', '/data-table', [
            [
                'methods'             => 'GET',
                'callback'            => [$this, 'handle_get_request'],
                'permission_callback' => '__return_true',
                'args'                => $this->get_args_schema(),
            ],
        ]);

        register_rest_route('table-builder/v1', '/data-table/refresh', [
            [
                'methods'             => 'POST',
                'callback'            => [$this, 'handle_refresh_request'],
                'permission_callback' => '__return_true',
                'args'                => $this->get_args_schema(),
            ],
        ]);
    }

    private function get_args_schema()
    {
        return [
            'type' => [
                'required' => true,
                'type'     => 'string',
                'enum'     => ['csv', 'googleSheet', 'jsonUrl', 'nestedJsonUrl'],
            ],
            'url' => [
                'required' => true,
                'type'     => 'string',
            ],
            'nestedPath' => [
                'type' => 'string',
            ],
            'liveUpdate' => [
                'type'    => 'boolean',
                'default' => false,
            ],
            'cacheDuration' => [
                'type' => 'integer',
            ],
        ];
    }

    public function handle_get_request(\WP_REST_Request $request)
    {
        $params = $this->parse_request_params($request);
        $cached = $this->get_cached_data($params['cacheKey']);

        if ($params['liveUpdate'] && $params['cacheDuration'] > 0) {
            if ($cached !== false) {
                return $cached;
            }
            // No cache exists, fetch fresh and cache it
            $duration = $params['cacheDuration'] * MINUTE_IN_SECONDS;
            return $this->fetch_and_cache_data($params, $duration);
        }
        // Not liveUpdate and cached data exists
        if (!$params['liveUpdate'] && $cached !== false) {
            return $cached; // Serve valid cached data
        }

        // All other cases: fetch fresh and optionally cache it
        $duration = ($params['cacheDuration'] > 0) ? $params['cacheDuration'] * MINUTE_IN_SECONDS : 0;
        return $this->fetch_and_cache_data($params, $duration);
    }

    public function handle_refresh_request(\WP_REST_Request $request)
    {
        $params = $this->parse_request_params($request);
        $this->delete_cached_data($params['cacheKey']);

        $data = $this->fetch_and_cache_data($params, MINUTE_IN_SECONDS);
        return $data;
    }

    private function parse_request_params(\WP_REST_Request $request): array
    {
        $type          = $request->get_param('type');
        $url           = esc_url_raw($request->get_param('url'));
        $nestedPath    = sanitize_text_field($request->get_param('nestedPath'));
        $liveUpdate    = filter_var($request->get_param('liveUpdate'), FILTER_VALIDATE_BOOLEAN);
        $cacheDuration = intval($request->get_param('cacheDuration'));
        $cacheKey      = $this->generate_cache_key($type, $url, $nestedPath);

        return compact('type', 'url', 'nestedPath', 'liveUpdate', 'cacheDuration', 'cacheKey');
    }

    private function fetch_and_cache_data(array $params, int $cacheDuration = 0)
    {
        $data = $this->get_data_by_type($params['type'], $params['url'], $params['nestedPath']);
        if (is_wp_error($data)) {
            return $data;
        }

        // Set cache duration based on liveUpdate or provided duration
        $duration = $cacheDuration
            ?: ($params['liveUpdate'] ? max($params['cacheDuration'] * MINUTE_IN_SECONDS, 12 * MINUTE_IN_SECONDS) : MINUTE_IN_SECONDS);

        if ($duration > 0) {
            set_transient($params['cacheKey'], $data, $duration);
        }

        return $data;
    }

    private function get_cached_data(string $cacheKey)
    {
        $timeout = get_option('_transient_timeout_' . $cacheKey);

        if ($timeout !== false && time() > (int) $timeout) {
            delete_transient($cacheKey);
            return false;
        }
        return get_transient($cacheKey);
    }

    private function delete_cached_data(string $cacheKey)
    {
        delete_transient($cacheKey);
    }

    private function get_data_by_type(string $type, string $url, string $nestedPath = '')
    {
        switch ($type) {
            case 'csv':
                return $this->parse_csv($url);
            case 'googleSheet':
                return $this->fetch_google_sheet_csv($url);
            case 'jsonUrl':
                return $this->parse_flat_json($url);
            case 'nestedJsonUrl':
                return $this->parse_nested_json($url, $nestedPath);
            default:
                return new \WP_Error('invalid_type', __('Unsupported data type.', 'table-builder-block'), ['status' => 400]);
        }
    }

    private function fetch_google_sheet_csv(string $url)
    {
        if (!preg_match('/\/d\/([a-zA-Z0-9-_]+)/', $url, $matches)) {
            return new \WP_Error('invalid_url', __('Invalid Google Sheet URL.', 'table-builder-block'), ['status' => 400]);
        }

        $sheetId = $matches[1];
        $csvUrl  = "https://docs.google.com/spreadsheets/d/{$sheetId}/export?format=csv";

        $response = wp_remote_get($csvUrl);
        if (is_wp_error($response)) {
            return $response;
        }

        if (wp_remote_retrieve_response_code($response) !== 200) {
            return new \WP_Error('private_sheet', __('This Google Sheet is not publicly accessible.', 'table-builder-block'), [
                'status' => 403,
                'details' => [
                    'url'       => $csvUrl,
                    'http_code' => wp_remote_retrieve_response_code($response),
                ],
            ]);
        }

        return $this->parse_csv($csvUrl);
    }

    private function parse_csv(string $url)
    {
        $response = wp_remote_get($url);
        if (is_wp_error($response)) {
            return $response;
        }

        $body  = wp_remote_retrieve_body($response);
        $lines = array_filter(array_map('trim', explode("\n", $body)));

        if (count($lines) < 2) {
            return new \WP_Error('empty_csv', __('CSV has no data.', 'table-builder-block'), ['status' => 400]);
        }

        $header = str_getcsv(array_shift($lines));
        $data = array_filter(array_map(function ($line) use ($header) {
            $row = str_getcsv($line);
            return count($row) === count($header) ? (object) array_combine($header, $row) : null;
        }, $lines));

        return $data;
    }

    private function parse_flat_json(string $url)
    {
        $data = $this->fetch_json_data($url);
        if (is_wp_error($data)) {
            return $data;
        }

        // Handle wrapped API responses (like DummyJSON, WordPress REST API, etc.)
        $processedData = $this->extract_data_array($data);

        // Check if it's nested after extraction
        if ($this->is_nested_json($processedData)) {
            return new \WP_Error('nested_json', __('Expected flat JSON, but nested structure found.', 'table-builder-block'), ['status' => 400]);
        }

        return array_map(fn($item) => (object) $item, $processedData);
    }

    private function parse_nested_json(string $url, string $path = '')
    {
        $data = $this->fetch_json_data($url);
        if (is_wp_error($data)) {
            return $data;
        }

        // Handle wrapped API responses first
        $processedData = $this->extract_data_array($data);

        if (!$this->is_nested_json($processedData)) {
            return array_map(fn($item) => (object) $item, $processedData);
        }

        $paths  = $this->extract_paths($processedData);
        $values = $path ? $this->extract_values_at_path($processedData, $path) : [];

        return [
            'paths'  => $paths,
            'values' => array_map(fn($item) => (object) $item, $values),
        ];
    }

    //Extract the actual data array from wrapped API responses
    private function extract_data_array($data)
    {
        if (!is_array($data)) {
            return [];
        }

        // If it's already a direct array of objects, return as is
        if ($this->is_indexed_array($data)) {
            return $data;
        }

        // For nested JSON, we should show all possible paths
        $allArrays = [];

        foreach ($data as $key => $value) {
            if (is_array($value)) {
                if ($this->is_indexed_array($value) && !empty($value)) {
                    $firstItem = reset($value);
                    if (is_array($firstItem)) {
                        $allArrays[] = [
                            'key' => $key,
                            'value' => $value,
                            'size' => count($value)
                        ];
                    }
                }
            }
        }

        // If we find arrays, return the first one by default
        if (!empty($allArrays)) {
            usort($allArrays, function ($a, $b) {
                return $b['size'] <=> $a['size'];
            });
            return $allArrays[0]['value'];
        }

        // If no arrays found, return the original data (might be a single object)
        if ($this->is_assoc_array($data)) {
            return [$data];
        }

        return $data;
    }

    //Check if array is indexed (not associative)
    private function is_indexed_array($array): bool
    {
        if (!is_array($array) || empty($array)) {
            return false;
        }
        return array_keys($array) === range(0, count($array) - 1);
    }

    private function fetch_json_data(string $url)
    {
        $response = wp_remote_get($url, [
            'timeout' => 30,
            'headers' => [
                'Accept' => 'application/json',
                'User-Agent' => 'TableBuilderPro/1.0'
            ]
        ]);

        if (is_wp_error($response)) {
            return $response;
        }

        $response_code = wp_remote_retrieve_response_code($response);
        if ($response_code !== 200) {
            return new \WP_Error('http_error', __('HTTP request failed with code: ' . $response_code, 'table-builder-block'), ['status' => $response_code]);
        }

        $body = wp_remote_retrieve_body($response);
        $data = json_decode($body, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            return new \WP_Error('invalid_json', __('Could not parse JSON: ' . json_last_error_msg(), 'table-builder-block'), ['status' => 400]);
        }

        return $data ?: new \WP_Error('empty_json', __('Empty JSON response.', 'table-builder-block'), ['status' => 400]);
    }

    private function is_nested_json(array $data): bool
    {
        foreach ($data as $row) {
            if (is_array($row)) {
                foreach ($row as $value) {
                    if (is_array($value)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    private function extract_paths(array $data, string $base = 'root', bool $isRoot = true): array
    {
        $paths = $isRoot ? [$base] : [];

        // For root level, examine the structure more carefully
        if ($isRoot) {
            if ($this->is_indexed_array($data)) {
                if (!empty($data)) {
                    $firstItem = $data[0];
                    if (is_array($firstItem)) {
                        $paths = array_merge($paths, $this->find_nested_arrays($firstItem, $base));
                    }
                }
            } else {
                // Object-wrapped data - examine all properties
                foreach ($data as $key => $value) {
                    if (is_array($value)) {
                        if ($this->is_indexed_array($value) && !empty($value)) {
                            // Found an array - add it as a path
                            $arrayPath = "$base -> $key";
                            $paths[] = $arrayPath;

                            // Check first item of this array for nested structures
                            $firstItem = reset($value);
                            if (is_array($firstItem)) {
                                $paths = array_merge($paths, $this->find_nested_arrays($firstItem, $arrayPath));
                            }
                        } else if ($this->is_assoc_array($value)) {
                            $objectPath = "$base -> $key";
                            $paths[] = $objectPath;
                            $paths = array_merge($paths, $this->find_nested_arrays($value, $objectPath));
                        }
                    }
                }
            }
        } else {
            $sample = is_array($data) && isset($data[0]) && is_array($data[0]) ? $data[0] : $data;
            if (is_array($sample)) {
                $paths = array_merge($paths, $this->find_nested_arrays($sample, $base));
            }
        }

        return array_unique($paths);
    }

    //Recursively find all arrays within an object/array structure
    private function find_nested_arrays(array $item, string $basePath): array
    {
        $paths = [];

        foreach ($item as $key => $value) {
            if (is_array($value)) {
                $currentPath = "$basePath -> $key";

                if ($this->is_indexed_array($value)) {
                    // This is an array - add it as a path
                    $paths[] = $currentPath;

                    // If it contains objects, explore deeper
                    if (!empty($value)) {
                        $firstItem = reset($value);
                        if (is_array($firstItem) && $this->is_assoc_array($firstItem)) {
                            $paths = array_merge($paths, $this->find_nested_arrays($firstItem, $currentPath));
                        }
                    }
                } else if ($this->is_assoc_array($value)) {
                    $paths[] = $currentPath;
                    $paths = array_merge($paths, $this->find_nested_arrays($value, $currentPath));
                }
            }
        }

        return $paths;
    }

    private function extract_values_at_path(array $data, string $path): array
    {
        $keys = explode(' -> ', $path);
        array_shift($keys);

        $values = [];
        $isRootPath = empty($keys); // True when user selects just "root"

        // If path is empty (just 'root'), return the main data array
        if ($isRootPath) {
            if ($this->is_indexed_array($data)) {
                foreach ($data as $item) {
                    if (is_array($item) && $this->is_assoc_array($item)) {
                        $flat = $this->flatten_object($item, true);
                        if (!empty($flat)) {
                            $values[] = $flat;
                        }
                    }
                }
                return $values;
            } else {
                $extractedData = $this->extract_data_array($data);
                foreach ($extractedData as $item) {
                    if (is_array($item) && $this->is_assoc_array($item)) {
                        $flat = $this->flatten_object($item, true);
                        if (!empty($flat)) {
                            $values[] = $flat;
                        }
                    }
                }
                return $values;
            }
        }

        // Get the main data array first
        $mainData = $this->is_indexed_array($data) ? $data : $this->extract_data_array($data);

        // Navigate to the target array/object in each item
        foreach ($mainData as $item) {
            if (!is_array($item)) continue;

            $current = $item;
            $pathValid = true;

            // Navigate through each key in the path
            foreach ($keys as $key) {
                if (is_array($current) && isset($current[$key])) {
                    $current = $current[$key];
                } else {
                    $pathValid = false;
                    break;
                }
            }

            // If we successfully navigated to the target
            if ($pathValid && $current !== null) {
                if (is_array($current)) {
                    if ($this->is_indexed_array($current)) {
                        foreach ($current as $arrayItem) {
                            if (is_array($arrayItem) && $this->is_assoc_array($arrayItem)) {
                                $flat = $this->flatten_object($arrayItem, false, false);

                                if (!empty($flat)) {
                                    $values[] = $flat;
                                }
                            } else if (!is_array($arrayItem)) {
                                $values[] = ['value' => $arrayItem];
                            }
                        }
                    } else if ($this->is_assoc_array($current)) {
                        $flat = $this->flatten_object($current, false, false);

                        if (!empty($flat)) {
                            $values[] = $flat;
                        }
                    }
                } else {
                    $values[] = ['value' => $current];
                }
            }
        }

        return $values;
    }

    /**
     * Flatten an object to show only primitive values
     * When showing root data, exclude nested arrays/objects
     * When showing specific nested path, include everything
     */
    private function flatten_object(array $object, bool $excludeNested = false, bool $prefixKeys = true): array
    {
        $flat = [];

        foreach ($object as $key => $value) {
            if (is_array($value)) {
                if ($excludeNested) {
                    continue;
                }

                if ($this->is_indexed_array($value)) {
                    $allPrimitives = true;
                    foreach ($value as $item) {
                        if (is_array($item) || is_object($item)) {
                            $allPrimitives = false;
                            break;
                        }
                    }
                    if ($allPrimitives) {
                        $flat[$key] = implode(', ', $value);
                    } else {
                        foreach ($value as $index => $item) {
                            if (is_array($item)) {
                                $nestedFlat = $this->flatten_object($item, false, $prefixKeys);
                                foreach ($nestedFlat as $nestedKey => $nestedValue) {
                                    if ($prefixKeys) {
                                        $flat[$key . '.' . $index . '.' . $nestedKey] = $nestedValue;
                                    } else {
                                        $flat[$nestedKey] = $nestedValue;
                                    }
                                }
                            } else {
                                if ($prefixKeys) {
                                    $flat[$key . '.' . $index] = $item;
                                } else {
                                    $flat[] = $item; // Just append primitives without keys
                                }
                            }
                        }
                    }
                } else {
                    $nestedFlat = $this->flatten_object($value, false, $prefixKeys); // Associative array - flatten recursively
                    foreach ($nestedFlat as $nestedKey => $nestedValue) {
                        if ($prefixKeys) {
                            $flat[$key . '.' . $nestedKey] = $nestedValue;
                        } else {
                            $flat[$nestedKey] = $nestedValue;
                        }
                    }
                }
            } else {
                $flat[$key] = $value;
            }
        }
        return $flat;
    }

    private function generate_cache_key(string $type, string $url, string $nestedPath = ''): string
    {
        $keyParts = [$type, $url];
        if ($type === 'nestedJsonUrl' && !empty($nestedPath)) {
            $keyParts[] = $nestedPath;
        }
        return 'tbp_data_cache_' . md5(implode('|', $keyParts));
    }

    private function is_assoc_array(array $array): bool
    {
        return array_keys($array) !== range(0, count($array) - 1);
    }
}
