<?php

namespace QUI\System\Console\Tools;

use QUI;

use function date;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function is_dir;
use function mkdir;
use function trim;

/**
 * Generate the FrankenPHP include config file.
 */
class Frankenphp extends QUI\System\Console\Tool
{
    private const FRANKENPHP_CONFIG_DIRECTORY = ETC_DIR . "webserver/frankenphp";
    private const FRANKENPHP_CONFIG_FILE = self::FRANKENPHP_CONFIG_DIRECTORY . "/Caddyfile.include";
    private const FRANKENPHP_USER_CONFIG_DIRECTORY = self::FRANKENPHP_CONFIG_DIRECTORY . "/conf.d";

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

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

        $this->createUserConfigs();
        $this->createBackup();
        $this->createConfigFile();

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

    protected function getConfig(): string
    {
        $frankenphpConfigFile = self::FRANKENPHP_CONFIG_FILE;
        $userConfigDirectory = self::FRANKENPHP_USER_CONFIG_DIRECTORY;
        $cmsDir = CMS_DIR;
        $phpDirective = $this->getPhpDirective();
        $binPhpDirective = $this->getPhpDirective('@quiqqer_bin_php');
        $libPhpDirective = $this->getPhpDirective('@quiqqer_lib_php');
        $adminPhpDirective = $this->getPhpDirective('@quiqqer_admin_php');
        $frontControllerPhpDirective = $this->getPhpDirective('@quiqqer_php');

        $frankenphpConfig = <<<CADDY
            # ======================================================================================================== #
            # 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/*.caddy"
            #
            # Check out the QUIQQER wiki on how to configure FrankenPHP for QUIQQER: https://dev.quiqqer.com/quiqqer/core/-/wikis/FrankenPHP-Konfiguration
            #
            # How to use this FrankenPHP config file:
            # 1. Include this file inside the site's Caddyfile block.
            # 2. Keep domain names, TLS, logging and other server-wide settings in the user-owned Caddyfile.
            #
            # Usage example:
            #
            #     example.com {
            #         import $frankenphpConfigFile
            #     }
            #
            # ======================================================================================================== #

            root * $cmsDir
            encode zstd gzip

            # User-defined configuration that should run before generated routing rules.
            import $userConfigDirectory/pre.caddy

            CADDY;

        if (QUI::conf("webserver", "forceHttps")) {
            $frankenphpConfig .= <<<CADDY

                @quiqqer_http {
                    protocol http
                }
                redir @quiqqer_http https://{host}{uri} permanent

                CADDY;
        }

        $frankenphpConfig .= <<<CADDY

            @quiqqer_git {
                path_regexp (^|/)\\.git(/|$)
            }

            # Static file cache headers.
            @quiqqer_static_images {
                path *.ico *.jpg *.jpeg *.png *.gif *.swf *.webp
            }
            header @quiqqer_static_images Cache-Control "max-age=31536000, public"

            @quiqqer_static_css {
                path *.css
            }
            header @quiqqer_static_css Cache-Control "max-age=31536000, public"

            @quiqqer_static_js {
                path *.js
            }
            header @quiqqer_static_js Cache-Control "max-age=31536000, private"

            @quiqqer_static_fonts {
                path *.eot *.svg *.ttf *.woff *.woff2
            }
            header @quiqqer_static_fonts Cache-Control "max-age=31536000, public"

            @quiqqer_bin_php path *.php
            @quiqqer_lib_php path *.php
            @quiqqer_admin_php path *.php
            @quiqqer_php path *.php

            @quiqqer_certificate_file {
                path *.crt *.pem
            }

            @quiqqer_path_contains_bin {
                path_regexp bin
            }

            @quiqqer_nested_existing {
                path_regexp ^/.+/.+
                file {
                    try_files {path} {path}/
                }
            }

            import $userConfigDirectory/optimizations.caddy

            route {
                # Hard denies.
                respond @quiqqer_git 403
                respond /console* 403

                # /admin -> /admin/
                redir /admin /admin/ 301

                # Additional generated or user-provided redirects.
                import $userConfigDirectory/redirects.caddy

                # /bin/* -> /packages/quiqqer/core/bin/*
                handle /bin/* {
                    uri replace /bin/ /packages/quiqqer/core/bin/
                    $binPhpDirective
                    file_server
                }

                # /lib/* -> /packages/quiqqer/core/src/*
                handle /lib/* {
                    uri replace /lib/ /packages/quiqqer/core/src/
                    $libPhpDirective
                    file_server
                }

                # /admin/ -> /packages/quiqqer/core/admin/index.php
                handle /admin/ {
                    rewrite * /packages/quiqqer/core/admin/index.php
                    $phpDirective
                }

                # /admin/image.php -> /image.php
                handle /admin/image.php {
                    rewrite * /image.php
                    $phpDirective
                }

                # /admin/ajax.php -> /packages/quiqqer/core/admin/ajax.php
                handle /admin/ajax.php {
                    rewrite * /packages/quiqqer/core/admin/ajax.php
                    $phpDirective
                }

                # /admin/* -> /packages/quiqqer/core/admin/*
                handle /admin/* {
                    uri replace /admin/ /packages/quiqqer/core/admin/
                    $adminPhpDirective
                    file_server
                }

                # Public static paths from the final htaccess allow-list.
                handle /.well-known/* {
                    try_files {path} {path}/ /index.php?{query}&_url={path}
                    $frontControllerPhpDirective
                    file_server
                }

                handle /media/cache/* {
                    try_files {path} {path}/ /index.php?{query}&_url={path}
                    $frontControllerPhpDirective
                    file_server
                }

                handle /packages/ckeditor/* {
                    try_files {path} {path}/ /index.php?{query}&_url={path}
                    $frontControllerPhpDirective
                    file_server
                }

                handle /packages/pcsg/ckeditor/* {
                    try_files {path} {path}/ /index.php?{query}&_url={path}
                    $frontControllerPhpDirective
                    file_server
                }

                # The htaccess allow-list permits certificate files and any URI containing "bin".
                handle @quiqqer_certificate_file {
                    try_files {path} {path}/ /index.php?{query}&_url={path}
                    $frontControllerPhpDirective
                    file_server
                }

                handle @quiqqer_path_contains_bin {
                    try_files {path} {path}/ /index.php?{query}&_url={path}
                    $frontControllerPhpDirective
                    file_server
                }

                # Additional user-provided public paths. Put "handle" blocks here.
                import $userConfigDirectory/whitelist.caddy

                # 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.
                redir @quiqqer_nested_existing /?error=403 301

                # The document root itself is an existing directory. Dispatch it explicitly
                # to index.php so the homepage is handled by QUIQQER instead of file_server.
                handle / {
                    rewrite * /index.php
                    $phpDirective
                }

                # Root and top-level paths. Existing files/directories are served directly;
                # non-existing paths are rewritten to the QUIQQER front controller.
                handle {
                    try_files {path} {path}/ /index.php?{query}&_url={path}
                    $frontControllerPhpDirective
                    file_server
                }
            }

            # User-defined additions that do not need to beat generated route handling.
            import $userConfigDirectory/post.caddy
            CADDY;

        return $frankenphpConfig;
    }

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

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

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

        return false;
    }

    private function createBackup(): void
    {
        if (!file_exists(self::FRANKENPHP_CONFIG_FILE)) {
            return;
        }

        $frankenphpBackupFile = VAR_DIR . 'backup/frankenphp.Caddyfile.include_' . date('Y-m-d__H_i_s');

        QUI\Utils\System\File::copy(self::FRANKENPHP_CONFIG_FILE, $frankenphpBackupFile);

        $this->writeLn("Storing current FrankenPHP config at: $frankenphpBackupFile");

        $this->resetColor();
    }

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

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

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

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

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

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

    private function getPhpDirective(string $matcher = ''): string
    {
        // The indent is "wrong" intentionally, so that it looks nice in the generated config file.
        $matcher = $matcher !== '' ? ' ' . $matcher : '';
        $userConfigDirectory = self::FRANKENPHP_USER_CONFIG_DIRECTORY;

        return <<<CADDY
            php$matcher {
                            env HTTP_MOD_REWRITE On
                            import $userConfigDirectory/php.caddy
                        }
            CADDY;
    }
}
