<?php

namespace Velis\Http;

use DateTime;
use Phalcon\Http\Response as PhalconResponse;
use Phalcon\Http\ResponseInterface;
use Velis\App;
use Velis\Arrays;
use Velis\Exception as VelisException;
use Velis\Http\Response\FileResponseParams;
use Velis\Output;

/**
 * HTTP Response
 * @author Olek Procki <olo@velis.pl>
 *
 * @method self redirect($location = null, $externalRedirect = false, $statusCode = 302)
 */
class Response extends PhalconResponse
{
    /**
     * Postpone sending response
     */
    private bool $postpone = false;

    /**
     * Creates new response
     *
     * @param mixed $content
     * @param mixed $code
     * @param mixed $status
     */
    public function __construct($content = null, $code = null, $status = null)
    {
        parent::__construct($content, $code, $status);

        $request = App::getService('request');

        if (App::$config->xFrameOptionsDeny && !$request->isFrameAllowedAction()) {
            $this->setHeader('X-Frame-Options', 'DENY');
        }

        if (App::$config->strictTransportSecurity && App::$config->settings->httpMethod == 'https') {
            $this->setHeader('strict-transport-security', 'max-age=15768000');
        }

        if (App::$config->settings->xSSProtection) {
            $this->setHeader('X-XSS-Protection', '1; mode=block');
        }

        if (App::$config->settings->xContentTypeOptions) {
            $this->setHeader('X-Content-Type-Options', 'nosniff');
        }

        if (App::$config->settings->contentSecurityPolicy) {
            $this->setCSPHeaders();
        }

        if (!App::isConsole() && App::$user->isLogged() && !$_COOKIE['waf-token']) {
            setcookie('waf-token', 'singu_test_token', time() + 31536000, '/');
        }

        if ($request->isApi()) {
            $this
                ->setHeader('Pragma', 'no-cache')
                ->setHeader('Cache-Control', 'private, no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0')
                ->setHeader('Access-Control-Allow-Methods', '*')
                ->setHeader('Access-Control-Expose-Headers', '*')
                ->setHeader('Access-Control-Allow-Credentials', true)
                ->setHeader('Content-Type', 'application/json;charset=utf-8');

            if (App::$config->cors->allowedOrigins) {
                $origin = $request->getHeader('Origin');
                if (in_array($origin, Arrays::toArray(App::$config->cors->allowedOrigins))) {
                    $this->setHeader('Access-Control-Allow-Origin', $origin);
                }
            } else {
                $this->setHeader('Access-Control-Allow-Origin', '*');
            }

            if (App::$config->cors->allowedHeaders) {
                $headers = implode(', ', Arrays::toArray(App::$config->cors->allowedHeaders));
                $this->setHeader('Access-Control-Allow-Headers', $headers);
            } else {
                $this->setHeader('Access-Control-Allow-Headers', '*');
            }
        }
    }

    /**
     * Returns true if redirect is available
     * @return bool
     */
    public function canRedirect()
    {
        if (App::$session->redirectCalls <= (App::$config->settings->maxRedirects ?: 5)) {
            return true;
        } elseif (microtime(true) - App::$session->lastRedirectTime > 1) {
            $this->resetRedirectCalls();
            return true;
        }

        return false;
    }


    /**
     * Increase redirect counter
     */
    public function increaseRedirectCalls()
    {
        App::$session->redirectCalls = (App::$session->redirectCalls ?: 0) + 1;
        App::$session->lastRedirectTime = microtime(true);
    }


    /**
     * Reset redirect calls counter
     */
    public function resetRedirectCalls()
    {
        App::$session->redirectCalls    = 0;
        unset(App::$session->lastRedirectTime);
    }

    /**
     * Adds headers for file caching purpose
     * @param string $file
     * @param string $modifiedDate,
     * @param string $offset
     * @param string $mime
     *
     * @deprecated use BaseController::setCachedFileResponse() instead
     * @todo remove after merging #63003 into master
     */
    public function handleModifiedSince($file, $modifiedDate = null, $offset = '+1 year', $mime = 'image/jpeg', $fileSystemType = 'upload')
    {
        if ($fileSystemType == 'upload') {
            $filesystem = App::$di->get('filesystem');
        } else {
            $filesystem = App::$di->get('filesystem.app');
        }

        $request = App::getService('request');
        if ($filesystem->has($file)) {
            $lastModified = gmdate('D, d M Y H:i:s', $filesystem->mtime($file)) . ' GMT';
            $ifModifiedSince = $request->getHeader('If-Modified-Since');
            $expires = new DateTime();
            $expires->modify($offset);

            if ($ifModifiedSince && $ifModifiedSince >= $lastModified) {
                $this->setHeader('Cache-Control', 'private, max-age=31536000')
                    ->setStatusCode(304, 'Not Modified')
                    ->setExpires($expires);
            } else {
                $this->setContentType($mime)
                    ->setContent($filesystem->read($file));

                if ($modifiedDate) {
                    $this->setHeader('Cache-Control', 'private, max-age=31536000')
                        ->setHeader('Last-Modified', $lastModified)
                        ->setExpires($expires);
                }
            }
        } else {
            $this->setStatusCode(404, 'File not found');
        }
    }

    /**
     * Set postpone response to enable sending after doHandle
     *
     * @param bool $postpone
     * @return void
     */
    public function postpone(bool $postpone = true): void
    {
        $this->postpone = $postpone;
    }

    /**
     * Check if response is postponed
     *
     * @return bool
     */
    public function isPostponed(): bool
    {
        return $this->postpone;
    }

    /**
     * This method should not be used anywhere outside \Velis\App
     *
     * @return ResponseInterface
     */
    public function sendForce(): ResponseInterface
    {
        if ($this->file && !$this->content) {
            $this->setContent(file_get_contents($this->file));
            $this->file = null;
        }

        return parent::send();
    }

    /**
     * Prepares serialized Content Security Policy from configs and sets the headers.
     *
     * @return void
     */
    private function setCSPHeaders(): void
    {
        if (!isset(App::$cache['CSP']) || !is_array(App::$cache['CSP'])) {
            include_once(CONFIG_PATH . 'content-security-policy.php');

            if (!isset($CSPolicy)) {
                $CSPolicy = [];
            }

            if (App::$config->node->nodeAddress && App::$config->node->pushNotification) {
                $nodeDomain = parse_url(App::$config->node->nodeAddress);
                if (!isset($nodeDomain['host'])) {
                    $nodeDomain['host'] = explode('/node', $nodeDomain['path'])[0];
                }

                $CSPolicy['connect-src'][] = "{$nodeDomain['host']}:* wss://{$nodeDomain['host']}:*";
            }

            if (App::$config->settings->contentSecurityPolicyReport) {
                // report-uri is deprecated, but new report-to is not well-supported yet
                $CSPolicy['report-uri'][] = '/csp-violation-reporter';
            }

            array_walk($CSPolicy, function (&$sources, $directive) {
                $serializedSourceList = join(' ', $sources);
                $serializedDirective = "$directive $serializedSourceList";
                $sources = $serializedDirective;
            });

            App::$cache['CSP'] = $CSPolicy;
        } else {
            $CSPolicy = App::$cache['CSP'];
        }

        if (App::$config->allowedSources) {
            if (!is_array($CSPolicy)) {
                $CSPolicy = [];
            }
            $this->extendPolicy($CSPolicy);
        }

        $serializedPolicy = join('; ', $CSPolicy);

        $this->setHeader('Content-Security-Policy', $serializedPolicy);
    }

    /**
     * Extends CSP with the directives defined in .ini config files (i.e. server.ini, application.ini).
     * It does NOT override values, it's being used for backward compatibility and env based values.
     *
     * @param array $policy
     * @return void
     */
    private function extendPolicy(array &$policy): void
    {
        $allowedSources = Arrays::toArray(App::$config->allowedSources);

        foreach ($allowedSources as $directive => $sources) {
            if (array_key_exists($directive, $policy)) {
                $policy[$directive] .= ' ' . join(' ', $sources);
                continue;
            }

            if (array_key_exists("${directive}-src", $policy)) {
                $policy["${directive}-src"] .= ' ' . join(' ', $sources);
                continue;
            }

            // backward compatibility for application.ini config
            if (in_array($directive, ['default', 'connect', 'style', 'font', 'media', 'img'])) {
                $directive .= '-src';
            }

            $policy[$directive] = "$directive " . join(' ', $sources);
        }
    }

    /**
     * {@inheritDoc}
     */
    public function setJsonContent($content, int $jsonOptions = 0, int $depth = 512): ResponseInterface
    {
        return $this
            ->setContentType('application/json', 'UTF-8')
            ->setContent(Output::jsonEncode($content, $jsonOptions))
        ;
    }

    /**
     * {@inheritDoc}
     */
    public function setContent(string $content): ResponseInterface
    {
        /**
         * Phalcon\Mvc\Application calls $dispatcher->getReturnedValue() and sets results as response content.
         * The condition below is to suppress setting empty response, as our controller actions should return void.
         */
        if (!$this->content || $content) {
            parent::setContent($content);
        }

        return $this;
    }

    /**
     * @return ResponseInterface
     * @throws VelisException
     * @deprecated This method should not be called explicitly in controller action.
     */
    public function send(): ResponseInterface
    {
        if (App::$config->settings->sentResponseCheck) {
            VelisException::raise('Response::send() method should not be called explicitly in controller action.');
        }

        return $this;
    }

    public function setFileContent(FileResponseParams $params): ResponseInterface
    {
        if (is_string($params->content)) {
            $this->setContent($params->content);
            $length = strlen($params->content);
        } else {
            $this->setFileToSend($params->path, $params->attachment_name);
            $length = $params->length;
        }

        $this
            ->setContentType($params->mime_type)
            ->setHeader('Content-Length', $length)
        ;

        return $this;
    }
}
