use lib 'lib';
use Datastar::SDK;

constant HOST = '127.0.0.1';

class TestRequest {
    has Str $.method is required;
    has Str $.query;
    has Str $.body = '';
    has Str $.protocol = 'HTTP/1.1';

    method request-method(--> Str) { $!method }
    method query-parameter(Str:D $name) { $!query }
    method body-text(--> Str) { $!body }
    method protocol-version(--> Str) { $!protocol }
}

class TestResponse {
    has %.headers;
    has Str $.body is rw = '';

    method set-header(Str:D $name, Str:D $value) {
        %!headers{$name} = $value;
    }
    method write(Str:D $chunk) { $!body ~= $chunk }
    method flush() { }
}

sub percent-decode(Str:D $source --> Str) {
    my @chars = $source.comb;
    my @bytes;
    my $index = 0;

    while $index < @chars.elems {
        if @chars[$index] eq '%' && $index + 2 < @chars.elems {
            my $hex = @chars[$index + 1] ~ @chars[$index + 2];
            die 'invalid percent encoding' unless $hex ~~ /^ <[0..9A..Fa..f]> ** 2 $/;
            @bytes.push(:16($hex));
            $index += 3;
        }
        else {
            my $char = @chars[$index] eq '+' ?? ' ' !! @chars[$index];
            @bytes.append($char.encode('utf-8').list);
            $index++;
        }
    }

    Buf.new(@bytes).decode('utf-8');
}

sub query-value(Str:D $target, Str:D $name) {
    my $question = $target.index('?');
    return Nil unless $question.defined;

    for $target.substr($question + 1).split('&') -> $pair {
        my ($key, $value) = $pair.split('=', 2);
        return percent-decode($value // '')
            if percent-decode($key) eq $name;
    }
    Nil;
}

sub read-request($connection --> Str) {
    my $raw = $connection.recv // '';
    my $header-delimiter = "\r\n\r\n";
    my $separator = $raw.index($header-delimiter);
    return $raw unless $separator.defined;

    my $headers = $raw.substr(0, $separator);
    my $content-length = 0;
    if $headers ~~ /:i 'Content-Length:' \s* (\d+)/ {
        $content-length = (~$0).Int;
    }

    my $body-start = $separator + $header-delimiter.chars;
    my $body-bytes = $raw.substr($body-start).encode('utf-8').bytes;
    while $body-bytes < $content-length {
        my $more = $connection.recv // '';
        last if $more eq '';
        $raw ~= $more;
        $body-bytes += $more.encode('utf-8').bytes;
    }

    $raw;
}

sub parse-request(Str:D $raw --> Hash) {
    my $header-delimiter = "\r\n\r\n";
    my $separator = $raw.index($header-delimiter);
    die 'malformed HTTP request' unless $separator.defined;

    my $head = $raw.substr(0, $separator);
    my $body = $raw.substr($separator + $header-delimiter.chars);
    my ($method, $target, $protocol) = ($head.lines.head // '').split(' ');
    die 'malformed HTTP request line'
        unless $method.defined && $target.defined && $protocol.defined;

    %(
        'method' => $method,
        target => $target,
        protocol => $protocol,
        body => $body,
    );
}

sub html-response(
    Int:D $status,
    Str:D $body,
    Str:D :$content-type = 'text/plain; charset=utf-8',
    --> Str
) {
    my $reason = $status == 200 ?? 'OK' !! 'Bad Request';
    "HTTP/1.1 $status $reason\r\n"
    ~ "Content-Type: $content-type\r\n"
    ~ "Content-Length: {$body.encode('utf-8').bytes}\r\n"
    ~ "Connection: close\r\n"
    ~ "\r\n"
    ~ $body;
}

sub sse-response(TestResponse:D $response --> Str) {
    "HTTP/1.1 200 OK\r\n"
    ~ "Cache-Control: {$response.headers<Cache-Control>}\r\n"
    ~ "Content-Type: {$response.headers<Content-Type>}\r\n"
    ~ "Connection: close\r\n"
    ~ "\r\n"
    ~ $response.body;
}

sub option(%event, Str:D $name, $default = Nil) {
    %event{$name}:exists ?? %event{$name} !! $default;
}

sub send-event($generator, %event --> Nil) {
    given %event<type> {
        when 'patchElements' {
            my %options = %(
                mode => option(%event, 'mode', 'outer'),
                use-view-transition =>
                    option(%event, 'useViewTransition', False),
                namespace => option(%event, 'namespace', 'html'),
                retry-duration =>
                    option(%event, 'retryDuration', DEFAULT-RETRY-DURATION),
            );
            %options<selector> = %event<selector>
                if %event<selector>:exists;
            %options<view-transition-selector> =
                %event<viewTransitionSelector>
                if %event<viewTransitionSelector>:exists;
            %options<event-id> = %event<eventId>
                if %event<eventId>:exists;
            $generator.patch-elements(
                option(%event, 'elements', ''),
                |%options,
            );
        }
        when 'patchSignals' {
            my $signals = %event{'signals-raw'}:exists
                ?? %event{'signals-raw'}
                !! to-json(%event<signals>);
            my %options = %(
                only-if-missing => option(%event, 'onlyIfMissing', False),
                retry-duration =>
                    option(%event, 'retryDuration', DEFAULT-RETRY-DURATION),
            );
            %options<event-id> = %event<eventId>
                if %event<eventId>:exists;
            $generator.patch-signals(
                $signals,
                |%options,
            );
        }
        when 'executeScript' {
            my %options = %(
                auto-remove => option(%event, 'autoRemove', True),
                attributes => option(%event, 'attributes', {}),
                retry-duration =>
                    option(%event, 'retryDuration', DEFAULT-RETRY-DURATION),
            );
            %options<event-id> = %event<eventId>
                if %event<eventId>:exists;
            $generator.execute-script(
                %event<script>,
                |%options,
            );
        }
        default { die "unsupported test event: {%event<type>}" }
    }
}

sub handle-request(%parsed --> Str) {
    return html-response(400, 'expected /test')
        unless %parsed<target>.split('?').head eq '/test';

    my $request = TestRequest.new(
        :method(%parsed<method>),
        query => query-value(%parsed<target>, DATASTAR-KEY),
        body => %parsed<body>,
        protocol => %parsed<protocol>,
    );
    my $response = TestResponse.new;
    my $generator = ServerSentEventGenerator.new(:$request, :$response);
    my %signals = read-signals($request);
    die 'events must be an array' unless %signals<events> ~~ Positional;

    send-event($generator, $_) for %signals<events>.List;
    sse-response($response);
}

sub MAIN(Int:D :$port = 7331) {
    my $listener = IO::Socket::INET.new(
        localhost => HOST,
        localport => $port,
        listen => True,
        reuseaddr => True,
    );
    note "Datastar Raku SDK conformance server listening on http://{HOST}:$port";

    loop {
        my $connection = $listener.accept;
        my %parsed = parse-request(read-request($connection));
        $connection.print(handle-request(%parsed));
        $connection.close;
    }
}
