<?php

/**
 * 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\Cookie\Cookie;
use CodeIgniter\Cookie\CookieStore;
use CodeIgniter\Cookie\Exceptions\CookieException;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use Config\App;
use Config\Cookie as CookieConfig;
use Config\Services;

/**
 * Representation of an outgoing, server-side response.
 *
 * Per the HTTP specification, this interface includes properties for
 * each of the following:
 *
 * - Protocol version
 * - Status code and reason phrase
 * - Headers
 * - Message body
 */
class Response extends Message implements ResponseInterface
{
    use ResponseTrait;

    /**
     * HTTP status codes
     *
     * @var array
     */
    protected static $statusCodes = [
        // 1xx: Informational
        100 => 'Continue',
        101 => 'Switching Protocols',
        102 => 'Processing', // http://www.iana.org/go/rfc2518
        103 => 'Early Hints', // http://www.ietf.org/rfc/rfc8297.txt
        // 2xx: Success
        200 => 'OK',
        201 => 'Created',
        202 => 'Accepted',
        203 => 'Non-Authoritative Information', // 1.1
        204 => 'No Content',
        205 => 'Reset Content',
        206 => 'Partial Content',
        207 => 'Multi-Status', // http://www.iana.org/go/rfc4918
        208 => 'Already Reported', // http://www.iana.org/go/rfc5842
        226 => 'IM Used', // 1.1; http://www.ietf.org/rfc/rfc3229.txt
        // 3xx: Redirection
        300 => 'Multiple Choices',
        301 => 'Moved Permanently',
        302 => 'Found', // Formerly 'Moved Temporarily'
        303 => 'See Other', // 1.1
        304 => 'Not Modified',
        305 => 'Use Proxy', // 1.1
        306 => 'Switch Proxy', // No longer used
        307 => 'Temporary Redirect', // 1.1
        308 => 'Permanent Redirect', // 1.1; Experimental; http://www.ietf.org/rfc/rfc7238.txt
        // 4xx: Client error
        400 => 'Bad Request',
        401 => 'Unauthorized',
        402 => 'Payment Required',
        403 => 'Forbidden',
        404 => 'Not Found',
        405 => 'Method Not Allowed',
        406 => 'Not Acceptable',
        407 => 'Proxy Authentication Required',
        408 => 'Request Timeout',
        409 => 'Conflict',
        410 => 'Gone',
        411 => 'Length Required',
        412 => 'Precondition Failed',
        413 => 'Content Too Large', // https://www.iana.org/assignments/http-status-codes/http-status-codes.xml
        414 => 'URI Too Long', // https://www.iana.org/assignments/http-status-codes/http-status-codes.xml
        415 => 'Unsupported Media Type',
        416 => 'Requested Range Not Satisfiable',
        417 => 'Expectation Failed',
        418 => "I'm a teapot", // April's Fools joke; http://www.ietf.org/rfc/rfc2324.txt
        // 419 (Authentication Timeout) is a non-standard status code with unknown origin
        421 => 'Misdirected Request', // http://www.iana.org/go/rfc7540 Section 9.1.2
        422 => 'Unprocessable Content', // https://www.iana.org/assignments/http-status-codes/http-status-codes.xml
        423 => 'Locked', // http://www.iana.org/go/rfc4918
        424 => 'Failed Dependency', // http://www.iana.org/go/rfc4918
        425 => 'Too Early', // https://datatracker.ietf.org/doc/draft-ietf-httpbis-replay/
        426 => 'Upgrade Required',
        428 => 'Precondition Required', // 1.1; http://www.ietf.org/rfc/rfc6585.txt
        429 => 'Too Many Requests', // 1.1; http://www.ietf.org/rfc/rfc6585.txt
        431 => 'Request Header Fields Too Large', // 1.1; http://www.ietf.org/rfc/rfc6585.txt
        451 => 'Unavailable For Legal Reasons', // http://tools.ietf.org/html/rfc7725
        499 => 'Client Closed Request', // http://lxr.nginx.org/source/src/http/ngx_http_request.h#0133
        // 5xx: Server error
        500 => 'Internal Server Error',
        501 => 'Not Implemented',
        502 => 'Bad Gateway',
        503 => 'Service Unavailable',
        504 => 'Gateway Timeout',
        505 => 'HTTP Version Not Supported',
        506 => 'Variant Also Negotiates', // 1.1; http://www.ietf.org/rfc/rfc2295.txt
        507 => 'Insufficient Storage', // http://www.iana.org/go/rfc4918
        508 => 'Loop Detected', // http://www.iana.org/go/rfc5842
        510 => 'Not Extended', // http://www.ietf.org/rfc/rfc2774.txt
        511 => 'Network Authentication Required', // http://www.ietf.org/rfc/rfc6585.txt
        599 => 'Network Connect Timeout Error', // https://httpstatuses.com/599
    ];

    /**
     * The current reason phrase for this response.
     * If empty string, will use the default provided for the status code.
     *
     * @var string
     */
    protected $reason = '';

    /**
     * The current status code for this response.
     * The status code is a 3-digit integer result code of the server's attempt
     * to understand and satisfy the request.
     *
     * @var int
     */
    protected $statusCode = 200;

    /**
     * If true, will not write output. Useful during testing.
     *
     * @var bool
     *
     * @internal Used for framework testing, should not be relied on otherwise
     */
    protected $pretend = false;

    /**
     * Constructor
     *
     * @param App $config
     *
     * @todo Recommend removing reliance on config injection
     */
    public function __construct($config)
    {
        // Default to a non-caching page.
        // Also ensures that a Cache-control header exists.
        $this->noCache();

        // We need CSP object even if not enabled to avoid calls to non existing methods
        $this->CSP = Services::csp();

        $this->CSPEnabled = $config->CSPEnabled;

        // DEPRECATED COOKIE MANAGEMENT

        $this->cookiePrefix   = $config->cookiePrefix;
        $this->cookieDomain   = $config->cookieDomain;
        $this->cookiePath     = $config->cookiePath;
        $this->cookieSecure   = $config->cookieSecure;
        $this->cookieHTTPOnly = $config->cookieHTTPOnly;
        $this->cookieSameSite = $config->cookieSameSite ?? Cookie::SAMESITE_LAX;

        $config->cookieSameSite ??= Cookie::SAMESITE_LAX;

        if (! in_array(strtolower($config->cookieSameSite ?: Cookie::SAMESITE_LAX), Cookie::ALLOWED_SAMESITE_VALUES, true)) {
            throw CookieException::forInvalidSameSite($config->cookieSameSite);
        }

        $this->cookieStore = new CookieStore([]);
        Cookie::setDefaults(config(CookieConfig::class) ?? [
            // @todo Remove this fallback when deprecated `App` members are removed
            'prefix'   => $config->cookiePrefix,
            'path'     => $config->cookiePath,
            'domain'   => $config->cookieDomain,
            'secure'   => $config->cookieSecure,
            'httponly' => $config->cookieHTTPOnly,
            'samesite' => $config->cookieSameSite ?? Cookie::SAMESITE_LAX,
        ]);

        // Default to an HTML Content-Type. Devs can override if needed.
        $this->setContentType('text/html');
    }

    /**
     * Turns "pretend" mode on or off to aid in testing.
     *
     * Note that this is not a part of the interface so
     * should not be relied on outside of internal testing.
     *
     * @return $this
     *
     * @testTag only available to test code
     */
    public function pretend(bool $pretend = true)
    {
        $this->pretend = $pretend;

        return $this;
    }

    /**
     * Gets the response status code.
     *
     * The status code is a 3-digit integer result code of the server's attempt
     * to understand and satisfy the request.
     *
     * @return int Status code.
     */
    public function getStatusCode(): int
    {
        if (empty($this->statusCode)) {
            throw HTTPException::forMissingResponseStatus();
        }

        return $this->statusCode;
    }

    /**
     * Gets the response response phrase associated with the status code.
     *
     * @see http://tools.ietf.org/html/rfc7231#section-6
     * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
     *
     * @deprecated Use getReasonPhrase()
     *
     * @codeCoverageIgnore
     */
    public function getReason(): string
    {
        return $this->getReasonPhrase();
    }

    /**
     * Gets the response reason phrase associated with the status code.
     *
     * Because a reason phrase is not a required element in a response
     * status line, the reason phrase value MAY be null. Implementations MAY
     * choose to return the default RFC 7231 recommended reason phrase (or those
     * listed in the IANA HTTP Status Code Registry) for the response's
     * status code.
     *
     * @see http://tools.ietf.org/html/rfc7231#section-6
     * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
     *
     * @return string Reason phrase; must return an empty string if none present.
     */
    public function getReasonPhrase()
    {
        if ($this->reason === '') {
            return ! empty($this->statusCode) ? static::$statusCodes[$this->statusCode] : '';
        }

        return $this->reason;
    }
}
