<?php

/**
 * \QUI\System\Console\Tools\Nginx
 */

namespace QUI\System\Console\Tools;

use QUI;

use function date;
use function define;
use function defined;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function is_dir;
use function mkdir;
use function str_replace;
use function trim;

/**
 * Generate the nginx.conf file
 */
class Nginx extends QUI\System\Console\Tool
{
    private const NGINX_CONFIG_DIRECTORY = ETC_DIR . "webserver/nginx";
    private const NGINX_CONFIG_FILE = self::NGINX_CONFIG_DIRECTORY . "/nginx.conf";
    private const NGINX_USER_CONFIG_DIRECTORY = self::NGINX_CONFIG_DIRECTORY . "/conf.d";

    public function __construct()
    {
        $this->systemTool = true;
        $this
            ->setName('quiqqer:nginx')
            ->setDescription('Generate the nginx config file.');
    }


    public function execute(): void
    {
        $this->writeLn('Generating nginx config…');

        // ************************************* //
        //              Sub Configs              //
        // ************************************* //
        $this->createUserConfigs();

        // ************************************* //
        //              Backup                   //
        // ************************************* //
        $this->createBackup();

        // ************************************* //
        //           Generate Template           //
        // ************************************* //
        $this->createConfigFile();

        $this->writeLn();
        $this->resetColor();
    }

    protected function getConfig(): string
    {
        $nginxConfigFile = self::NGINX_CONFIG_FILE;
        $userConfigDirectory = self::NGINX_USER_CONFIG_DIRECTORY;
        $fastCgiConfigSnippet = $this->getFastCgiConfig();

        $nginxConfig = <<<NGINX
            # ======================================================================================================== #
            # This config file was automatically generated by QUIQQER. 
            # Do not edit this file manually - your changes will be overwritten.
            # User customizations belong in "conf.d/*.conf"
            #
            # Check out the QUIQQER wiki on how to configure nginx for QUIQQER: https://dev.quiqqer.com/quiqqer/core/-/wikis/nginx-konfiguration
            #
            # How to use this nginx config file:
            # 1. Set the "\$php_fpm_backend" variable to a value that can be passed to "fastcgi_pass" (example below)
            # 2. Include this file inside the vhost's "server {}" block(s)
            #
            # Usage example:
            #
            #     server {
            #         server_name example.com;
            #         root /var/www/example.com;
            #         index index.php;
            #
            #         set \$php_fpm_backend unix:/run/php/php-fpm.sock;
            #         include $nginxConfigFile;
            #     }
            #
            # ======================================================================================================== #

            NGINX;

        $nginxConfig .= <<<NGINX

            # Keep generated redirects relative so non-standard ports from the incoming "Host" header are not dropped from "Location" headers.
            absolute_redirect off;

            NGINX;

        // Force redirect to HTTPS if configured
        if (QUI::conf("webserver", "forceHttps")) {
            $nginxConfig .= <<<NGINX

                if (\$scheme != "https") {
                    return 301 https://\$host\$request_uri;
                }

                NGINX;
        }

        $nginxConfig .= <<<NGINX

            # User-defined configuration
            include $userConfigDirectory/pre.conf;
                        
            # Hard denies.
            location ~ (^|/)\.git(/|$) {
                return 403;
            }
            
            location ^~ /console {
                return 403;
            }
            
            # /admin -> /admin/
            location = /admin {
                return 301 /admin/\$is_args\$args;
            }
            
            # /bin/* -> /packages/quiqqer/core/bin/*
            location ^~ /bin/ {
                rewrite ^/bin/(.*)$ /packages/quiqqer/core/bin/$1 last;
            }
            
            # /lib/* -> /packages/quiqqer/core/src/*
            location ^~ /lib/ {
                rewrite ^/lib/(.*)$ /packages/quiqqer/core/src/$1 last;
            }
            
            # /admin/ -> /packages/quiqqer/core/admin/index.php
            location = /admin/ {
                $fastCgiConfigSnippet
            
                include $userConfigDirectory/php.conf;
            
                fastcgi_param SCRIPT_FILENAME \$document_root/packages/quiqqer/core/admin/index.php;
                fastcgi_param SCRIPT_NAME /packages/quiqqer/core/admin/index.php;
            }
            
            # /admin/index.php -> /packages/quiqqer/core/admin/index.php
            location = /admin/index.php {
                $fastCgiConfigSnippet
            
                include $userConfigDirectory/php.conf;
            
                fastcgi_param SCRIPT_FILENAME \$document_root/packages/quiqqer/core/admin/index.php;
                fastcgi_param SCRIPT_NAME /packages/quiqqer/core/admin/index.php;
            }
            
            # /admin/image.php -> /image.php
            location = /admin/image.php {
                rewrite ^ /image.php last;
            }
            
            # /admin/ajax.php -> /packages/quiqqer/core/admin/ajax.php
            location = /admin/ajax.php {
                $fastCgiConfigSnippet
            
                include $userConfigDirectory/php.conf;
            
                fastcgi_param SCRIPT_FILENAME \$document_root/packages/quiqqer/core/admin/ajax.php;
                fastcgi_param SCRIPT_NAME /packages/quiqqer/core/admin/ajax.php;
            }
            
            # Additional generated or user-provided redirects.
            include $userConfigDirectory/redirects.conf;
            
            # Public static paths from the final htaccess allow-list.
            location /.well-known/ {
                try_files \$uri \$uri/ @quiqqer_front_controller;
            }
            
            location /media/cache/ {
                try_files \$uri \$uri/ @quiqqer_front_controller;
            }
            
            location /packages/ckeditor/ {
                try_files \$uri \$uri/ @quiqqer_front_controller;
            }
            
            location /packages/pcsg/ckeditor/ {
                try_files \$uri \$uri/ @quiqqer_front_controller;
            }
            
            location = /favicon.ico {
                try_files \$uri @quiqqer_front_controller;
            }
            
            location = /robots.txt {
                try_files \$uri @quiqqer_front_controller;
            }
            
            # Root PHP entry points allowed by the htaccess rules.
            location = /index.php {
                $fastCgiConfigSnippet
            
                include $userConfigDirectory/php.conf;
            
                fastcgi_param SCRIPT_FILENAME \$document_root/index.php;
                fastcgi_param SCRIPT_NAME /index.php;
            }
            
            location = /image.php {
                $fastCgiConfigSnippet
            
                include $userConfigDirectory/php.conf;
            
                fastcgi_param SCRIPT_FILENAME \$document_root/image.php;
                fastcgi_param SCRIPT_NAME /image.php;
            }
            
            location = /ajax.php {
                $fastCgiConfigSnippet
            
                include $userConfigDirectory/php.conf;
            
                fastcgi_param SCRIPT_FILENAME \$document_root/ajax.php;
                fastcgi_param SCRIPT_NAME /ajax.php;
            }
            
            location = /ajaxBundler.php {
                $fastCgiConfigSnippet
            
                include $userConfigDirectory/php.conf;
            
                fastcgi_param SCRIPT_FILENAME \$document_root/ajaxBundler.php;
                fastcgi_param SCRIPT_NAME /ajaxBundler.php;
            }
            
            # Allow execution of PHP scripts below "bin/" directories
            location ~ (^|/)bin/.*\.php$ {
                $fastCgiConfigSnippet
            
                include $userConfigDirectory/php.conf;
            
                fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
                fastcgi_param SCRIPT_NAME \$fastcgi_script_name;
            }
            
            # Targets reached by the /bin and /lib rewrites above.
            location ~ ^/packages/quiqqer/core/(?:bin|src)/.*\.php$ {
                $fastCgiConfigSnippet
            
                include $userConfigDirectory/php.conf;
            
                fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
                fastcgi_param SCRIPT_NAME \$fastcgi_script_name;
            }
            
            location ~ ^/packages/quiqqer/core/(?:bin|src)/(?!.*\.php$).+$ {
                try_files \$uri \$uri/ =404;
            }
            
            # Static file optimization, caching and compression snippets.
            # GZIP Compression
            gzip on;
            gzip_comp_level    5;
            gzip_min_length    256;
            gzip_proxied       any;
            gzip_vary          on;
            
            gzip_types
            application/atom_xml
            application/javascript
            application/rss+xml
            application/x-javascript
            application/x-shockwave-flash
            application/xhtml+xml
            application/xml
            image/svg+xml
            text/css
            text/javascript
            text/plain
            text/xml;
            
            # Cache Control
            location ~ ^/(?:[^/]+|.*bin.*|\.well-known/.+|media/cache/.+|packages/ckeditor/.+|packages/pcsg/ckeditor/.+)\.(ico|jpe?g|png|gif|swf|webp)$ {
                expires 1y;
                add_header Cache-Control "max-age=31536000, public";
                try_files \$uri @quiqqer_front_controller;
            }
            
            location ~ ^/(?:[^/]+|.*bin.*|\.well-known/.+|media/cache/.+|packages/ckeditor/.+|packages/pcsg/ckeditor/.+)\.css$ {
                expires 1y;
                add_header Cache-Control "max-age=31536000, public";
                try_files \$uri @quiqqer_front_controller;
            }
            
            location ~ ^/(?:[^/]+|.*bin.*|\.well-known/.+|media/cache/.+|packages/ckeditor/.+|packages/pcsg/ckeditor/.+)\.js$ {
                expires 1w;
                add_header Cache-Control "max-age=31536000, private";
                try_files \$uri @quiqqer_front_controller;
            }
            
            location ~ ^/(?:[^/]+|.*bin.*|\.well-known/.+|media/cache/.+|packages/ckeditor/.+|packages/pcsg/ckeditor/.+)\.(eot|svg|ttf)$ {
                expires 1y;
                add_header Cache-Control "max-age=31536000, public";
                try_files \$uri @quiqqer_front_controller;
            }
            
            location ~ ^/(?:[^/]+|.*bin.*|\.well-known/.+|media/cache/.+|packages/ckeditor/.+|packages/pcsg/ckeditor/.+)\.(woff|woff2)$ {
                expires 1y;
                add_header Cache-Control "max-age=31536000, public";
                try_files \$uri @quiqqer_front_controller;
            }
            
            location ~ ^/(?:[^/]+|.*bin.*|\.well-known/.+|media/cache/.+|packages/ckeditor/.+|packages/pcsg/ckeditor/.+)\.html$ {
                expires 1w;
                try_files \$uri @quiqqer_front_controller;
            }
            
            # ETag
            etag off;
            if_modified_since off;
            
            include $userConfigDirectory/optimizations.conf;
            
            # Top-level .html and .txt files allowed by the htaccess rules.
            location ~ ^/[A-Za-z0-9_+\s-]*\.(html|txt)$ {
                try_files \$uri @quiqqer_front_controller;
            }
            
            # Certificate files allowed by the htaccess rules.
            location ~ ^/.*\.(crt|pem)$ {
                try_files \$uri @quiqqer_front_controller;
            }
            
            # The htaccess allow-list excludes any URI containing "bin".
            location ~ bin {
                try_files \$uri \$uri/ @quiqqer_front_controller;
            }
            
            # Existing nested files/directories that did not match an allow-list above are
            # redirected to /?error=403. Non-existing nested paths go to the front controller.
            location ~ ^/.+/.+ {
                if (-e \$request_filename) {
                    return 301 /?error=403;
                }
            
                rewrite ^/(.*)$ /index.php?_url=$1&\$args last;
            }
            
            # Existing top-level PHP files are allowed by the htaccess rules.
            location ~ \.php$ {
                try_files \$uri @quiqqer_front_controller;
            
                $fastCgiConfigSnippet
            
                include $userConfigDirectory/php.conf;
            
                fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
                fastcgi_param SCRIPT_NAME \$fastcgi_script_name;
            }
            
            # Root and top-level paths. Existing top-level files/directories are accessible;
            # non-existing paths are rewritten to the QUIQQER front controller.
            location / {
                try_files \$uri \$uri/ @quiqqer_front_controller;
            }
            
            location @quiqqer_front_controller {
                rewrite ^/(.*)$ /index.php?_url=$1&\$args last;
            }
            
            # User-defined additions that do not need to beat generated regex locations.
            include $userConfigDirectory/post.conf;
            NGINX;

        return $nginxConfig;
    }

    /**
     * Checks if the nginx config has been modified since the last time generating it.
     */
    public function hasModifications(): bool
    {
        if (!file_exists(self::NGINX_CONFIG_FILE)) {
            return true;
        }

        $oldContent = file_get_contents(self::NGINX_CONFIG_FILE);
        $content = $this->getConfig();

        if (trim((string)$oldContent) !== trim($content)) {
            return true;
        }

        return false;
    }

    private function createBackup(): void
    {
        if (!file_exists(self::NGINX_CONFIG_FILE)) {
            // nginx config file does not exist (yet)
            return;
        }

        $nginxBackupFile = VAR_DIR . 'backup/nginx.conf_' . date('Y-m-d__H_i_s');

        QUI\Utils\System\File::copy(self::NGINX_CONFIG_FILE, $nginxBackupFile);

        $this->writeLn("Storing current nginx config at: $nginxBackupFile");

        $this->resetColor();
    }

    private function createUserConfigs(): void
    {
        $header = <<<HEAD
            # This file will be included into the auto generated nginx config.
            # You can make changes to the nginx configuration by editing this file.
            HEAD;

        if (!is_dir(self::NGINX_USER_CONFIG_DIRECTORY)) {
            mkdir(self::NGINX_USER_CONFIG_DIRECTORY, 0755, true);
        }

        $userConfigFiles = [
            "pre",
            "php",
            "redirects",
            "whitelist",
            "optimizations",
            "post",
        ];

        foreach ($userConfigFiles as $userConfigFile) {
            $userConfigFile = self::NGINX_USER_CONFIG_DIRECTORY . "/$userConfigFile.conf";
            if (!file_exists($userConfigFile)) {
                file_put_contents($userConfigFile, $header);
            }
        }
    }

    private function createConfigFile(): void
    {
        file_put_contents(self::NGINX_CONFIG_FILE, $this->getConfig());

        $this->writeLn("Created new nginx config at: " . self::NGINX_CONFIG_FILE);
    }

    private function getFastCgiConfig(): string
    {
        // The indent is "wrong" intentionally, so that it looks nice in the generated config file
        return <<<FASTCGI
            fastcgi_param QUERY_STRING       \$query_string;
                fastcgi_param REQUEST_METHOD     \$request_method;
                fastcgi_param CONTENT_TYPE       \$content_type;
                fastcgi_param CONTENT_LENGTH     \$content_length;
                
                fastcgi_param REQUEST_URI        \$request_uri;
                fastcgi_param DOCUMENT_URI       \$document_uri;
                fastcgi_param DOCUMENT_ROOT      \$document_root;
                fastcgi_param SERVER_PROTOCOL    \$server_protocol;
                
                fastcgi_param GATEWAY_INTERFACE  CGI/1.1;
                fastcgi_param SERVER_SOFTWARE    nginx/\$nginx_version;
                
                fastcgi_param REMOTE_ADDR        \$remote_addr;
                fastcgi_param REMOTE_PORT        \$remote_port;
                fastcgi_param SERVER_ADDR        \$server_addr;
                fastcgi_param SERVER_PORT        \$server_port;
                fastcgi_param SERVER_NAME        \$server_name;
                
                fastcgi_param HTTPS              \$https if_not_empty;
                fastcgi_param REDIRECT_STATUS    200;
                
                # Equivalent of the Apache htaccess environment rules:
                # SetEnv HTTP_MOD_REWRITE On
                # RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
                fastcgi_param HTTP_AUTHORIZATION \$http_authorization;
                fastcgi_param HTTP_MOD_REWRITE   On;
                
                fastcgi_pass \$php_fpm_backend;
            FASTCGI;
    }
}
