<?php

declare(strict_types=1);

/**
 * This file is part of CodeIgniter 4 framework.
 *
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace CodeIgniter\HTTP;

use CodeIgniter\Exceptions\BadMethodCallException;
use CodeIgniter\Exceptions\ConfigException;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use Config\App;

/**
 * URI for the application site
 *
 * @see \CodeIgniter\HTTP\SiteURITest
 */
class SiteURI extends URI
{
    /**
     * The current baseURL.
     */
    private readonly URI $baseURL;

    /**
     * The path part of baseURL.
     *
     * The baseURL "http://example.com/" → '/'
     * The baseURL "http://localhost:8888/ci431/public/" → '/ci431/public/'
     */
    private string $basePathWithoutIndexPage;

    /**
     * The Index File.
     */
    private readonly string $indexPage;

    /**
     * List of URI segments in baseURL and indexPage.
     *
     * If the URI is "http://localhost:8888/ci431/public/index.php/test?a=b",
     * and the baseURL is "http://localhost:8888/ci431/public/", then:
     *   $baseSegments = [
     *       0 => 'ci431',
     *       1 => 'public',
     *       2 => 'index.php',
     *   ];
     */
    private array $baseSegments;

    /**
     * List of URI segments after indexPage.
     *
     * The word "URI Segments" originally means only the URI path part relative
     * to the baseURL.
     *
     * If the URI is "http://localhost:8888/ci431/public/index.php/test?a=b",
     * and the baseURL is "http://localhost:8888/ci431/public/", then:
     *   $segments = [
     *       0 => 'test',
     *   ];
     *
     * @var array<int, string>
     *
     * @deprecated This property will be private.
     */
    protected $segments;

    /**
     * URI path relative to baseURL.
     *
     * If the baseURL contains sub folders, this value will be different from
     * the current URI path.
     *
     * This value never starts with '/'.
     */
    private string $routePath;

    /**
     * @param string              $relativePath URI path relative to baseURL. May include
     *                                          queries or fragments.
     * @param string|null         $host         Optional current hostname.
     * @param 'http'|'https'|null $scheme       Optional scheme. 'http' or 'https'.
     */
    public function __construct(
        App $configApp,
        string $relativePath = '',
        ?string $host = null,
        ?string $scheme = null,
    ) {
        $this->indexPage = $configApp->indexPage;

        $this->baseURL = $this->determineBaseURL($configApp, $host, $scheme);

        $this->setBasePath();

        // Fix routePath, query, fragment
        [$routePath, $query, $fragment] = $this->parseRelativePath($relativePath);

        // Fix indexPage and routePath
        $indexPageRoutePath = $this->getIndexPageRoutePath($routePath);

        // Fix the current URI
        $uri = $this->baseURL . $indexPageRoutePath;

        // applyParts
        $parts = parse_url($uri);
        if ($parts === false) {
            throw HTTPException::forUnableToParseURI($uri);
        }
        $parts['query']    = $query;
        $parts['fragment'] = $fragment;
        $this->applyParts($parts);

        $this->setRoutePath($routePath);
    }

    private function parseRelativePath(string $relativePath): array
    {
        $parts = parse_url('http://dummy/' . $relativePath);
        if ($parts === false) {
            throw HTTPException::forUnableToParseURI($relativePath);
        }

        $routePath = $relativePath === '/' ? '/' : ltrim($parts['path'], '/');

        $query    = $parts['query'] ?? '';
        $fragment = $parts['fragment'] ?? '';

        return [$routePath, $query, $fragment];
    }

    private function determineBaseURL(
        App $configApp,
        ?string $host,
        ?string $scheme,
    ): URI {
        $baseURL = $this->normalizeBaseURL($configApp);

        $uri = new URI($baseURL);

        // Update scheme
        if ($scheme !== null && $scheme !== '') {
            $uri->setScheme($scheme);
        } elseif ($configApp->forceGlobalSecureRequests) {
            $uri->setScheme('https');
        }

        // Update host
        if ($host !== null) {
            $uri->setHost($host);
        }

        return $uri;
    }

    private function getIndexPageRoutePath(string $routePath): string
    {
        // Remove starting slash unless it is `/`.
        if ($routePath !== '' && $routePath[0] === '/' && $routePath !== '/') {
            $routePath = ltrim($routePath, '/');
        }

        // Check for an index page
        $indexPage = '';
        if ($this->indexPage !== '') {
            $indexPage = $this->indexPage;

            // Check if we need a separator
            if ($routePath !== '' && $routePath[0] !== '/' && $routePath[0] !== '?') {
                $indexPage .= '/';
            }
        }

        $indexPageRoutePath = $indexPage . $routePath;

        if ($indexPageRoutePath === '/') {
            $indexPageRoutePath = '';
        }

        return $indexPageRoutePath;
    }

    private function normalizeBaseURL(App $configApp): string
    {
        // It's possible the user forgot a trailing slash on their
        // baseURL, so let's help them out.
        $baseURL = rtrim($configApp->baseURL, '/ ') . '/';

        // Validate baseURL
        if (filter_var($baseURL, FILTER_VALIDATE_URL) === false) {
            throw new ConfigException(
                'Config\App::$baseURL "' . $baseURL . '" is not a valid URL.',
            );
        }

        return $baseURL;
    }

    /**
     * Sets basePathWithoutIndexPage and baseSegments.
     */
    private function setBasePath(): void
    {
        $this->basePathWithoutIndexPage = $this->baseURL->getPath();

        $this->baseSegments = $this->convertToSegments($this->basePathWithoutIndexPage);

        if ($this->indexPage !== '') {
            $this->baseSegments[] = $this->indexPage;
        }
    }

    /**
     * @deprecated
     */
    public function setBaseURL(string $baseURL): void
    {
        throw new BadMethodCallException('Cannot use this method.');
    }

    /**
     * @deprecated
     */
    public function setURI(?string $uri = null)
    {
        throw new BadMethodCallException('Cannot use this method.');
    }

    /**
     * Returns the baseURL.
     *
     * @interal
     */
    public function getBaseURL(): string
    {
        return (string) $this->baseURL;
    }

    /**
     * Returns the URI path relative to baseURL.
     *
     * @return string The Route path.
     */
    public function getRoutePath(): string
    {
        return $this->routePath;
    }

    /**
     * Formats the URI as a string.
     */
    public function __toString(): string
    {
        return static::createURIString(
            $this->getScheme(),
            $this->getAuthority(),
            $this->getPath(),
            $this->getQuery(),
            $this->getFragment(),
        );
    }

    /**
     * Sets the route path (and segments).
     *
     * @return $this
     */
    public function setPath(string $path)
    {
        $this->setRoutePath($path);

        return $this;
    }

    /**
     * Sets the route path (and segments).
     */
    private function setRoutePath(string $routePath): void
    {
        $routePath = $this->filterPath($routePath);

        $indexPageRoutePath = $this->getIndexPageRoutePath($routePath);

        $this->path = $this->basePathWithoutIndexPage . $indexPageRoutePath;

        $this->routePath = ltrim($routePath, '/');

        $this->segments = $this->convertToSegments($this->routePath);
    }

    /**
     * Converts path to segments
     */
    private function convertToSegments(string $path): array
    {
        $tempPath = trim($path, '/');

        return ($tempPath === '') ? [] : explode('/', $tempPath);
    }

    /**
     * Sets the path portion of the URI based on segments.
     *
     * @return $this
     *
     * @deprecated This method will be private.
     */
    public function refreshPath()
    {
        $allSegments = array_merge($this->baseSegments, $this->segments);
        $this->path  = '/' . $this->filterPath(implode('/', $allSegments));

        if ($this->routePath === '/' && $this->path !== '/') {
            $this->path .= '/';
        }

        $this->routePath = $this->filterPath(implode('/', $this->segments));

        return $this;
    }

    /**
     * Saves our parts from a parse_url() call.
     *
     * @param array{
     *  host?: string,
     *  user?: string,
     *  path?: string,
     *  query?: string,
     *  fragment?: string,
     *  scheme?: string,
     *  port?: int,
     *  pass?: string,
     * } $parts
     */
    protected function applyParts(array $parts): void
    {
        if (isset($parts['host']) && $parts['host'] !== '') {
            $this->host = $parts['host'];
        }

        if (isset($parts['user']) && $parts['user'] !== '') {
            $this->user = $parts['user'];
        }

        if (isset($parts['path']) && $parts['path'] !== '') {
            $this->path = $this->filterPath($parts['path']);
        }

        if (isset($parts['query']) && $parts['query'] !== '') {
            $this->setQuery($parts['query']);
        }

        if (isset($parts['fragment']) && $parts['fragment'] !== '') {
            $this->fragment = $parts['fragment'];
        }

        if (isset($parts['scheme'])) {
            $this->setScheme(rtrim($parts['scheme'], ':/'));
        } else {
            $this->setScheme('http');
        }

        if (isset($parts['port'])) {
            // Valid port numbers are enforced by earlier parse_url or setPort()
            $this->port = $parts['port'];
        }

        if (isset($parts['pass'])) {
            $this->password = $parts['pass'];
        }
    }

    /**
     * For base_url() helper.
     *
     * @param array|string $relativePath URI string or array of URI segments.
     * @param string|null  $scheme       URI scheme. E.g., http, ftp. If empty
     *                                   string '' is set, a protocol-relative
     *                                   link is returned.
     */
    public function baseUrl($relativePath = '', ?string $scheme = null): string
    {
        $relativePath = $this->stringifyRelativePath($relativePath);

        $config            = clone config(App::class);
        $config->indexPage = '';

        $host = $this->getHost();

        $uri = new self($config, $relativePath, $host, $scheme);

        // Support protocol-relative links
        if ($scheme === '') {
            return substr((string) $uri, strlen($uri->getScheme()) + 1);
        }

        return (string) $uri;
    }

    /**
     * @param array|string $relativePath URI string or array of URI segments
     */
    private function stringifyRelativePath($relativePath): string
    {
        if (is_array($relativePath)) {
            $relativePath = implode('/', $relativePath);
        }

        return $relativePath;
    }

    /**
     * For site_url() helper.
     *
     * @param array|string $relativePath URI string or array of URI segments.
     * @param string|null  $scheme       URI scheme. E.g., http, ftp. If empty
     *                                   string '' is set, a protocol-relative
     *                                   link is returned.
     * @param App|null     $config       Alternate configuration to use.
     */
    public function siteUrl($relativePath = '', ?string $scheme = null, ?App $config = null): string
    {
        $relativePath = $this->stringifyRelativePath($relativePath);

        // Check current host.
        $host = $config instanceof App ? null : $this->getHost();

        $config ??= config(App::class);

        $uri = new self($config, $relativePath, $host, $scheme);

        // Support protocol-relative links
        if ($scheme === '') {
            return substr((string) $uri, strlen($uri->getScheme()) + 1);
        }

        return (string) $uri;
    }
}
