unit class Nginx::Config;

has @.servers;
has @.comment;
has $.main_server_block;
has $.main_server_block_index;
has Bool $.is_read_only;
has $.domain_name;

submethod TWEAK {
    my @main_server_blocks;

    for @!servers.kv -> $index, $server {
        my $has_standard_ssl = $server.has-standard-ssl;
        if $has_standard_ssl.defined {
            @main_server_blocks.append: { $index => $has_standard_ssl};
            $!main_server_block = $server;
            $!main_server_block_index = $index;
        }
    }

    if @main_server_blocks.elems == 1 {
        $!is_read_only = False;
    }
    else {
        $!is_read_only = True;
        $!main_server_block = Nil;
        $!main_server_block_index = 'read only';
    }
}

method domain-name() {
    return @.servers[0].names[0];
}

method Str {
    return @.servers.join: "\n";
}

role Directive {
    method Str {...}
}

role Stanza[$file] does Directive {
    method Str {
        return "include stanzas/$file.conf;";
    }
}

method has-cms-stanza(--> Bool) {
    for @.servers -> $server {
        if $server.has-cms-stanza {
            return True;
        }
    }

    return False;
}

method stanzas(--> Hash()) {
    my %stanzas;

    my @main_server_block_stanzas =
        %(description => 'Themenseiten UTF8 Darstellung (statische Websites)', name => 'Seo', stanza_file => 'stanzas/seo.conf'),
        %(description => 'Google Verifizierung für GSC (dynamische Websites)', name => 'GoogleVerificationForCMS', stanza_file => 'stanzas/google_verification_for_cms.conf'),
        %(description => 'Startseite Rewrite zu index_ger.html', name => 'AvoidRedirectToIndexHtml', stanza_file => 'stanzas/avoid_redirect_to_index_html.conf'),
        %(description => 'Matomo proxy hinzufügen', name => 'MatomoProxy', stanza_file => 'stanzas/matomo_proxy.conf'),
    ;
    my @fourth_server_block_stanzas =
        %(description => 'Alte Domain Links/Redirects auf neue Domain weiterleiten', name => 'RedirectOldRedirectsToNewDomain', stanza_file => 'stanzas/redirect_old_redirects_on_new_domain.conf'),
    ;

    if $!main_server_block {
        for @main_server_block_stanzas -> $stanza {
            if $!main_server_block.has-stanza($stanza<name>) {
                %stanzas<added>.push: $stanza;
                next;
            }
            %stanzas<available>.push: $stanza;
        }
    }

    my $fourth_server_block = @!servers[3];

    return %stanzas unless $fourth_server_block;

    for @fourth_server_block_stanzas -> $stanza {
        if $fourth_server_block.has-stanza($stanza<name>) {
            %stanzas<added>.push: $stanza;
            next;
        }

        %stanzas<available>.push: $stanza;
    }

    return %stanzas;
}

method find-location(:@directives, :$type, :$path, :$subtype, :$value, :$http_status) {
    my @locations = self.filter-directives(@directives, $type);
    @locations = self.filter-directives(@locations, 'path', $path);
    if @locations.elems > 0 {
        for @locations -> $location {
            my @return_directives = self.filter-directives($location.directives, $subtype);
            @return_directives = self.filter-directives(@return_directives, 'value', $value);
            @return_directives = self.filter-directives(@return_directives, 'http_status', $http_status);

            if @return_directives.elems == 1 {
                return $location;
            }
        }
    }

    return;
}

method add-stanza($stanza_path) {
    my $main_server_block = $!main_server_block;
    die "Keinen main server block gefunden" unless $main_server_block;

    my $stanza = Nginx::Config.class-for-stanza($stanza_path);
    die "Keine Stanza '$stanza_path' gefunden" if $stanza.isa('Nginx::Config::StanzaGeneric');

    if $stanza_path ne 'stanzas/redirect_old_redirects_on_new_domain.conf' {
        return $main_server_block.ensure-directive-present($stanza);
    }

    my $fourth_server_block = @!servers[3];
        die "Keinen 4. Serverblock gefunden!" unless $fourth_server_block;

    $fourth_server_block.ensure-directive-present($stanza);
}

method remove-stanza($stanza_class_name) {
    my $main_server_block = $!main_server_block;

    if $stanza_class_name ne 'RedirectOldRedirectsToNewDomain' {
        return $main_server_block.remove-directive-by-class-name($stanza_class_name);
    }

    my $fourth_server_block = @!servers[3];
        die "'$stanza_class_name' Keinen 4. Serverblock gefunden!" unless $fourth_server_block;

    $fourth_server_block.remove-directive-by-class-name($stanza_class_name);
}

#| This represents the 'server' block in a Nginx configuration file.
class Server {
    has @.names;
    has @.directives;
    has @.comments;

    method json {
        my @server_config;
        @server_config.push: { server_name => @.names };
        for @.directives -> $directive {
            @server_config.append: { $directive.WHO => $directive.Str };
        }
        #check class Comment. Str returns an array.
        for @.comments -> $comment {
            @server_config.append: { $comment.WHO => $comment.Str[0] };
        }


        return @server_config;
    }

    method stanza_list {
        my @stanzas;
        for @.directives -> $directive {
             if $directive.does(Nginx::Config::Stanza) {
                @stanzas.push: $directive.Str;
            }
        }
        return @stanzas;
    }

    method location {
        my @location_directives;
        for @.directives -> $directive {

            if ($directive.WHO eq 'Nginx::Config::Location') {

                my %nested_directive;
                for $directive.directives -> $nested_directive {
                    if ($nested_directive.VAR ~~ /'Nginx::Config::Return'/) {
                        %nested_directive = $nested_directive.attributes;
                    }
                }

                @location_directives.push: {
                    path     => $directive.path.Str,
                    operator => $directive.op ?? $directive.op.Str !! '',
                    directive_inside => {
                        %nested_directive,
                    }

                }
            }

        }
        return @location_directives;
    }

    method has-standard-ssl {
        my $main_config_index;
        for @.directives.kv -> $index, $directive {
            if $directive.isa('Nginx::Config::StandardConfigSSL') {
                return $index;
            }
        }

        return;
    }

    method has-generic-stanza {
        my $generic_stanza;
        for @.directives.kv -> $index, $directive {
            if  $directive.isa('Nginx::Config::StanzaGeneric') {
                return True;
            }
        }
        return;
    }

    method has-stanza(Str:D $name --> Bool) {
        for @.directives -> $directive {
            return True if $directive.isa("Nginx::Config::$name");
        }

        return False;
    }

    method has-cms-stanza(--> Bool) {
        return self.has-stanza('CMS');
    }

    method Str {
        return qq:heredoc/CONFIG/;
            server \{
                    {
                        if @.names.elems < 3 {
                            { "server_name " ~ @.names.join(' ') ~ ';' };
                        } else {
                            { "server_name " ~ @.names.join("\n").indent(8) ~ ';' };
                        }
                    }
            { @.directives».Str.join("\n").indent(8) }
            \}
            @.comments».Str.join("\n")
            CONFIG
    }

    multi method remove-directive($matcher) {
        @!directives .= grep({ $_ !~~ $matcher });
    }

    multi method remove-directive($matcher, $class) {
        return unless $matcher;

        @!directives .= grep({
                $_.WHO !~~ 'Nginx::Config::' ~ $class
                ||
                $_.path !~~ m/^ $matcher $/
        });
    }

    method remove-directive-by-class-name(Str:D $class) {
        @!directives .= grep({ $_.WHO !~~ "Nginx::Config::$class" });
    }

    multi method ensure-directive-present($directive) {
        @.directives.push: $directive
            unless @.directives.any ~~ $directive.WHAT;

    }

    multi method ensure-directive-present($new_directive, $duplicate) {
        my $duplicate_check = 0;
        for @.directives -> $directive {
            if $directive.Str eq $new_directive.Str {
                $duplicate_check = 1;
            }
        }
        if $duplicate_check == 0 {
            @.directives.push: $new_directive;
        }
        else {
            return 'Directive already exists';
        }
    }

    multi method ensure-directive-present($new_directive, 'location') {
        my $duplicate_check = 0;
        for @.directives -> $directive {
            next unless $directive.WHO eq 'Nginx::Config::Location';
            if $new_directive.path.Str eq $directive.path.Str {
                $duplicate_check = 1;
            }
        }
        if $duplicate_check == 0 {
            @.directives.push: $new_directive;
            return 'Done';
        }
        else {
            return 'Directive already exists';
        }
    }

    method list-directive_type($directive_type) {
        my @needed_directive_types.append: self!directive-search($directive_type, @.directives);

        return @needed_directive_types;
    }
    method !directive-search($directive_type, @directives) {
        my @needed_directive_types;

        for @directives -> $directive {

            if ($directive.WHO eq 'Nginx::Config::' ~ $directive_type) {
                @needed_directive_types.append: $directive.Str;
            } elsif $directive.WHO eq 'Nginx::Config::If' {
                @needed_directive_types.append: self!directive-search($directive_type, $directive.directives);
            } elsif $directive.WHO eq 'Nginx::Config::Location' {
                @needed_directive_types.append: self!directive-search($directive_type, $directive.directives);
            }
        }
        return @needed_directive_types;
    }

    multi method modify-directive($directive_type, $current, $new, $new_value?) {

        for @.directives -> $directive {
            if $directive.WHO eq 'Nginx::Config::' ~ 'Location' {
                if $directive.path  ~~ m/$current/ {
                    $directive.path = $new;

                    if $new_value {self.modify-nested-directive($directive_type, $directive.directives[0], $new_value, '');}

                }
            }
        }
    }

    multi method modify-directive('Location', $current_location_path, %new_parameters ) {

        for @.directives -> $directive {
            if $directive.WHO eq 'Nginx::Config::' ~ 'Location' {

                if $directive.path ~~ m/$current_location_path/ {
                    if %new_parameters<path> {
                        $directive.path = %new_parameters<path>;
                    }
                    if %new_parameters<operator> {
                        $directive.op = %new_parameters<operator>;
                    }

                    if %new_parameters<return_uri> {
                        self.modify-nested-directive(
                            'Return', $directive.directives[0],
                            %new_parameters<return_uri>, %new_parameters<http_status>
                        );
                    }
                }
            }
        }
        return 'Done';
    }

    multi method modify-nested-directive('Return', $directive, $new_return_uri, $new_http_status) {
        $directive.value       = $new_return_uri;
        $directive.http_status = $new_http_status;
    }

    multi method modify-nested-directive('Nginx::Config::Rewrite', $directive, $new_value) {
        $directive.replacement = $new_value;
    }
}

class Listen does Directive {
    has $.host;
    has $.port = 80;
    has $.flag;

    method Str {
        if $.flag {
            return "listen $.host:$.port" ~ "$.flag;";
        } else {
            return "listen $.host:$.port;";
        }
    }
}

class ErrorPage does Directive {
    has $.status;
    has $.uri;

    method Str {
        return "error_page $.status $.uri;";
    }
}

class Root does Directive {
    has $.path;

    method Str {
        return "root $.path;";
    }
}


class CMS does Stanza['cms'] {
}

class DomainRedirect does Stanza['domain_redirect'] {
}

class MobileRedirect does Stanza['mobile_redirect'] {
}

class AppWebViewRedirect does Stanza['app_web_view_redirect'] {
}

class InAppRedirect does Stanza['in_app_redirect'] {
}

class ActivateStandardCert does Stanza['activate_standard_cert'] {
}

class CachingDirectives does Stanza['caching'] {
}

class Let'sEncryptWellKnown does Stanza['le_well_known'] {
}

class RewriteToSsl does Stanza['rewrite_to_ssl'] {
}

class StandardDirectives does Stanza['standard_directives'] {
}

class Search does Stanza['search'] {
}

class Statistics does Stanza['statistics'] {
}

class ActivateSsl does Stanza['activate_ssl'] {
}

class Static does Stanza['static'] {
}

class GoogleVerification does Stanza['google_verification'] {
}

class GoogleVerificationForCMS does Stanza['google_verification_for_cms'] {}

class Seo does Stanza['seo'] {
}

class MatomoLog does Stanza['matomo_log'] {
}

class MatomoProxy does Stanza['matomo_proxy'] {
}

class CORS does Stanza['cors'] {
}

class StandardConfigSSL does Stanza['standard_config_ssl'] {
}

class ErrorPageStanza does Stanza['error_page'] {
}

#| Sometimes the redirect from webpage root url to index_ger.html should be prevented. Instead the
#| nginx config 'rewrite ^/$ /index_ger.html' should be used and added to a master server block.
class AvoidRedirectToIndexHtml does Stanza['avoid_redirect_to_index_html'] {}

#| In case of a new domain all old links and redirects have to be redirected to the new domain.
class RedirectOldRedirectsToNewDomain does Stanza['redirect_old_redirects_on_new_domain'] {}

class StanzaGeneric does Directive {
    has $.file;

    method Str {
        return "include $.file;";
    }
}

method class-for-stanza($stanza) {
    my $class = Nginx::Config.WHO.values
        .grep({$_.HOW ~~ Metamodel::ClassHOW})
        .grep(Stanza)
        .first({"stanzas/{ $_.^roles[0].^role_arguments[0] }.conf" eq $stanza});

    if $class ~~ Stanza {

        return $class;

    }
    else {

        $class = Nginx::Config::StanzaGeneric.new(
            :file($stanza),
        );

        return $class;
    }
}

class Comment does Directive {
    has $.content;

    method Str {
        my $comment_output;
        for $.content.lines -> $line {
            $comment_output.push: '#' ~ $line;
        }
        return $comment_output;
    }
}

class Deny does Directive {
    has $.rule;
    has $.comment;

    method Str {
        return qq/deny $.rule;/ ~ ($.comment ?? ' #' ~ $.comment !! '');
    }
}

class Allow does Directive {
    has $.rule;
    has $.comment;

    method Str {
        return qq/allow $.rule;/ ~ ($.comment ?? ' #' ~ $.comment !! '');
    }
}

class Add_Header does Directive {
    has $.header_values;

    method Str {
        return qq/add_header $.header_values;/;
    }
}

class Add_Trailer does Directive {
    has $.trailer_values;

    method Str {
        return qq/add_trailer $.trailer_values;/;
    }
}

class Expires does Directive {
    has $.expire_values;

    method Str {
        return qq/expires $.expire_values;/;
    }
}

class Generic does Directive {
    has $.content;

    method Str {
        return '#' ~ $.content;
    }
}

class Location does Directive {
    has $.path is rw;
    has $.op is rw;
    has @.directives;

=begin comment

Special location, removes the operator from social media location blocks.
This is done because it is not needed and works better than a = or a regular expression.
This action is only done on paths that are like the following two, "/facebook" and "/twitter/".
Any other path will be treated normally.

=end comment

    method !special_location {
        my @locations = ('facebook', 'twitter', 'xing', 'linkedin', 'googleplus', 'vimeo');
        if $.path ~~ m/\/?@locations\/?$/ {
            $.op = '';
        }
    }

    method Str {
        my $location_string;
        self!special_location;
        $.path ~~ s/\s$//;
        if $.op {
            $location_string = qq[location $.op "$.path" \{\n];
        } else {
            $location_string = qq[location "$.path" \{\n];
        }

        return
            $location_string
            ~ (@.directives ?? @.directives».Str.join("\n").indent(12) ~ "\n" !! '')
            ~ '}'.indent(8);
    }
}

class Alias does Directive {
    has $.target;

    method Str {
        return "alias $.target;";
    }
}

class Return does Directive {
    has $.value is rw;
    has $.http_status is rw;

    method Str {
        my $http_output = '';
        if $.http_status {
            $http_output = $.http_status ~ ' ';
        }
        return "return " ~ $http_output ~ "$.value;" if $.value ~~ /^ http | '$scheme'/;
        return "return " ~ $http_output ~ "\$scheme://\$host$.value;" if $.value ~~ m!^\/!;
        return "return " ~ $http_output ~ "\$scheme://\$host/$.value;";
    }
    method attributes {
        my %return.append: (
            directive => 'Return',
            return => $.value,
            http_status => $.http_status ?? $.http_status !! '',
        );

        return %return;
    }

}

class Rewrite does Directive {
    has $.regex is rw;
    has $.replacement;
    has $.redirect;

    method Str {
        return qq/rewrite $.regex $.replacement/ ~ ($.redirect ?? ' ' ~ $.redirect !! '') ~ ';';
    }
}

class Set does Directive {
    has $.variable;
    has $.value;

    method Str {
        return qq/set $.variable "$.value";/;
    }
}

class SslCert does Directive {
    has $.certificate_path;

    method Str {
        return "ssl_certificate $.certificate_path;";
    }
}

class SslCertKey does Directive {
    has $.certificate_path;

    method Str {
        return "ssl_certificate_key $.certificate_path;";
    }
}

class ProxySetHeader does Directive {
    has $.command;
    has $.variable;

    method Str {
        return "proxy_set_header $.command$.variable;";
    }
}

class ProxyPass does Directive {
    has $.address;
    has $.comment;

    method Str {
        return qq/proxy_pass $.address;/ ~ ($.comment ?? ' #' ~ $.comment !! '');
    }
}

class ProxyRedirect does Directive {
    has @.path;

    method Str {
        return "proxy_redirect " ~ @.path.join(" ") ~ ";";
    }
}

class TryFiles does Directive {
    has $.variable;
    has $.uri;
    has $.status;

    method Str {
        return "try_files $.uri =$.status;" if $.status ~~ /\d+/;
        return "try_files $.variable $.uri;";
    }
}

class Internal does Directive {
    has $.word;

    method Str {
        return qq/$.word;/;
    }
}

class DefaultType does Directive {
    has $.default_type;

    method Str {
        return qq/default_type $.default_type;/;
    }
}

class CharSet does Directive {
    has $.character_set;

    method Str {
        return qq/charset $.character_set;/;
    }
}

class Index does Directive {
    has @.path;

    method Str {
        return "index " ~ @.path.join(" ") ~ ";";
    }
}

class If does Directive {
    has $.variable;
    has $.op;
    has $.value;
    has @.directives;

    method Str {
        return
            qq/if ($.variable $.op "$.value") \{\n/
            ~ (@.directives ?? @.directives».Str.join("\n").indent(8) ~ "\n" !! '')
            ~ '}';
    }
}

class NestedIf does Directive {
    has $.index = 0;
    has @.conditions;
    has @.directives;

    method add-if(If $if) {
        @.conditions.push: $if;
        @.directives.append: $if.directives.splice(
            0,
            *,
            qq/set \$if_cond_$.index "\$\{if_cond_$.index\}1";/,
        );
    }

    method Str() {
        return qq/set \$if_cond_$.index 1;\n/
            ~ @.conditions.map(*.Str).join("\n") ~ "\n"
            ~ If.new(
                :variable("\$if_cond_$.index"),
                :op<=>,
                :value('1' x (@.conditions.elems + 1)),
                :@!directives,
            ).Str;
    }
}

# vim: ft=perl6
