<?php

namespace classes;

use Exception;
use interfaces\response_i;
use JetBrains\PhpStorm\NoReturn;
use objects\users_o;

class response implements response_i
{
    private bool $matching_route_found = false;
    private array $data = [];
    private array $meta = [];
    private array $includes = [];
    private users_o $users_o;
    private ?array $jsonRequestBody = null;

    #[NoReturn] public function success(mixed $data, int $status = null): void
    {
        $this->response(true, $data, $status);
    }

    #[NoReturn] public function response(bool $success, mixed $data, int $status = null): void
    {
        global $DEBUG;
        header('Content-Type: application/json; charset=utf-8');
        if ($status) {
            http_response_code($status);
        } else {
            http_response_code($success ? 200 : 400);
        }
        // If the data isn't an array, convert it to an array
        if (!is_array($data) && !is_object($data)) {
            // Check if the variable is a valid JSON string
            if (is_string($data) && json_decode($data) !== null) {
                $data = json_decode($data, true);
            } else {
                // If the variable is not a valid JSON string, convert it to an array
                $data = ['message' => $data];
            }
        }
        // If the debug mode is enabled, add the debug data to the response
        if ($DEBUG && false) {
            $this->add_include('debug', [
                'memory' => memory_get_usage(),
                'time' => microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'],
                'data' => $this->get_data()
            ]);
        }
        try {
            release_manager::recordBackendFailure($success, $data, $status ?? ($success ? 200 : 400));
        } catch (\Throwable) {
            // Release failure telemetry is best-effort and must not block responses.
        }
        echo json_encode([
            'success' => $success,
            'data' => $data,
            'meta' => $this->meta,
            'includes' => $this->includes
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    #[NoReturn] public function rawJson(mixed $data, int $status = 200): void
    {
        header('Content-Type: application/json; charset=utf-8');
        http_response_code($status);

        if (!is_array($data) && !is_object($data)) {
            if (is_string($data) && json_decode($data) !== null) {
                echo $data;
                exit;
            }

            $data = ['message' => $data];
        }

        echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }

    public function add_include(string $string, array $dataArray): void
    {
        $this->add_included($string, $dataArray);
    }

    public function add_included(string $key, mixed $value): void
    {
        $this->includes[$key] = $value;
    }

    public function get_data(): array
    {
        return $this->data;
    }

    public function get_meta(): array
    {
        return $this->meta;
    }

    public function get_includes(): array
    {
        return $this->includes;
    }

    #[NoReturn] public function not_found(): void
    {
        $this->error('Not found', 404);
    }

    #[NoReturn] public function error(mixed $data, int $status = null): void
    {
        $this->response(false, $data, $status);
    }

    #[NoReturn] public function rate_limit_exceeded(): void
    {
        $this->error('Rate limit exceeded', 429);
    }

    public function matching_route_found(): void
    {
        $this->matching_route_found = true;
    }

    #[NoReturn] public function method_not_allowed(): void
    {
        $this->error('Method not allowed', 405);
    }

    #[NoReturn] public function internal_server_error($error): void
    {
        $this->error('Internal server error' . ($error ? ': ' . $error : ''), 500);
    }

    #[NoReturn] public function forbidden(array $permissions): void
    {
        $this->error([
            'message' => 'Missing permission(s)',
            'permissions' => $permissions
        ], 403);
    }

    public function paginate(int $page, int $per_page, int $total, string $search = null, array $filters = null, array $order = null): void
    {
        // If the total is 0, return 1 page, 0 total
        if ($total === 0) {
            $total = 1;
        }
        $this->add_meta('pagination', [
            'page' => $page,
            'per_page' => $per_page,
            'total' => $total,
            'search' => $search,
            'filters' => $filters,
            'order' => $order
        ]);
    }

    public function add_meta(string $key, mixed $value): void
    {
        $this->meta[$key] = $value;
    }

    public function is_matching_route_found(): bool
    {
        return $this->matching_route_found;
    }

    public function add_debug(mixed $data): void
    {
        $this->add_data('debug', $data);
    }

    public function add_data(string $key, mixed $value): void
    {
        $this->data[$key] = $value;
    }

    public function getRequestParameter(string $key): mixed
    {
        return $this->requestParametersForMethod()[$key] ?? null;
    }

    /**
     * Get all request parameters
     * @return array
     */
    public function getAllRequestParameters(): array
    {
        return $this->requestParametersForMethod();
    }

    /**
     * This function checks if a request parameter is set.
     * This is different from getRequestParameter because this function allows for empty & null values.
     * @param string $key
     * @return bool
     */
    public function isRequestParameterSet(string $key): bool
    {
        return array_key_exists($key, $this->requestParametersForMethod());
    }

    private function requestParametersForMethod(): array
    {
        $method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));

        if (in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
            return array_replace($_GET, $this->jsonRequestBody());
        }

        if ($method === 'DELETE') {
            return array_replace($_GET, $this->jsonRequestBody());
        }

        if ($method === 'GET' || $method === 'OPTIONS') {
            return $_GET;
        }

        return $this->jsonRequestBody();
    }

    private function jsonRequestBody(): array
    {
        if ($this->jsonRequestBody !== null) {
            return $this->jsonRequestBody;
        }

        $decoded = json_decode($this->rawRequestBody(), true);
        $this->jsonRequestBody = is_array($decoded) ? $decoded : [];

        return $this->jsonRequestBody;
    }

    protected function rawRequestBody(): string
    {
        $body = file_get_contents('php://input');
        return is_string($body) ? $body : '';
    }

    public function parseFilters(?string $filters): array|null
    {
        if ($filters) {
            $filters = explode(',', $filters);
            $temp = [];
            foreach ( $filters as $filter ) {
                $filter = explode(':', $filter);
                $temp[$filter[0]] = $filter[1];
            }
            $filters = $temp;
        }
        if ($filters) {
            return $filters;
        }
        return null;
    }

    public function add_debug_list(string $list_name, int $key, $value): void
    {
        $this->add_data($list_name, [$key => $value]);
    }

    #[NoReturn] public function displayPDF($raw_pdf): void
    {
        self::disableAll();
        $virtual_file = fopen('php://temp', 'r+');
        fwrite($virtual_file, $raw_pdf);
        fseek($virtual_file, 0);
        file_put_contents('/tmp/debug_output.pdf', stream_get_contents($virtual_file));
        // Return the PDF /tmp/debug_output.pdf
        header('Content-Type: application/pdf');
        header('Content-Disposition: inline; filename="debug_output.pdf"');
        header('Content-Length: ' . filesize('/tmp/debug_output.pdf'));
        header('Content-Transfer-Encoding: binary');
        header('Accept-Ranges: bytes');
        readfile('/tmp/debug_output.pdf');
        exit;

    }

    private static function disableAll(): void
    {
        error_reporting(0);
        ini_set('display_errors', '0');
    }

    /**
     * This function gets the user object from the token
     * @throws Exception If the token is invalid, or the user is not found
     */
    public function get_user(): users_o|false
    {
        // Check if the user is already set
        if (!isset($this->users_o)) {
            // Get the user object
            $this->users_o = (new authentication())->get_user();
        }
        // Return the user object
        return $this->users_o;
    }
}
