package Spike::Site::Router;

use strict;
use warnings;

use syntax 'try';

use base qw(Spike::Site::Handler);

use Spike::Error;
use Spike::Site::Router::Route;
use Spike::Cache;

use Carp;
use HTTP::Status qw(:constants);
use Plack::MIME;
use POSIX qw(strftime);
use Date::Parse;
use List::Util qw(first uniq);
use List::MoreUtils qw(zip);

sub route { (shift->{route} ||= Spike::Site::Router::Route->new)->route(@_) }

sub config {
    my $self = shift;
    my $config = $self->{config} ||= Spike::Config->new;

    return $config->(@_) if @_;
    return $config;
}

sub cache {
    my $self = shift;
    my $config = $self->config->spike_site_router;

    return $self->{cache} ||= Spike::Cache->new(
        name          => 'Route cache',
        debug         => $self->debug,

        max_records   => $config->max_records('int', 10240),
        max_ttl       => $config->max_ttl('int', 3600),
        max_idle_time => $config->max_idle_time('int', 300),
        purge_time    => $config->purge_time('int', 300),
    );
}

sub _init_config {
    my $self = shift;

    (my $site_name = lc ref $self) =~ s/::.*$//;
    my $site_home = "$FindBin::Bin/..";

    my %defaults = (
        site_name       => $site_name,
        config_path     => "$site_home/conf",
        template_path   => "$site_home/tmpl/$site_name",
        static_path     => "$site_home/web/$site_name",
    );

    $self->config->site->$_(set => $defaults{$_})
        for keys %defaults;
}

sub init {
    my $self = shift;

    $self->_init_config;
    $self->startup;
}

sub _find_handlers {
    my ($self, $route) = @_;

    my (@routes, %errors, @names);

    while ($route) {
        unshift @routes, $route;
        unshift @names, $route->name;

        $route = $route->parent;
    }

    shift @names; # exclude root

    my $prepare = sub { @_ };
    my $finalize = sub {};

    for $route (@routes) {
        $prepare = $route->prepare || $prepare;
        $finalize = $route->finalize || $finalize;

        $errors{$_} = [ $route->error($_), $prepare, $finalize ]
            for $route->errors;
    }

    return $prepare, $finalize, \%errors, \@names;
}

sub _try_static {
    my ($self, $req, $res, $path) = @_;

    return if $path =~ m!(^|/)\.!;

    my @files = length $path ? ($path, "$path/index.html") : ("index.html");

    for my $file (@files) {
        my $full_path = $self->config->site->static_path."/".$file;

        next unless -f $full_path && open my $fh, '<', $full_path;

        return 1 if $req->method ne 'GET';

        my ($size, $mtime) = (stat $fh)[7, 9];

        $res->header('Last-Modified' => strftime('%c', localtime $mtime));

        my $req_mtime = first { defined $_ } map { str2time($_) }
            $req->header('If-Modified-Since');

        if (!$req_mtime || $req_mtime < $mtime) {
            my $suffix = ($file =~ m!(\.[^/.]+)?$!)[0] || '.html';

            $res->content_length($size);
            $res->content_type(Plack::MIME->mime_type($suffix));

            $res->body($fh);
        }
        else {
            $res->status(HTTP_NOT_MODIFIED);
        }

        throw Spike::Error::HTTP_OK;
    }

    return;
}

sub _handle_error {
    my ($self, $req, $res, $error, $errors) = @_;

    $error = new Spike::Error::HTTP($error) if !ref $error;

    my $class = ref $error;
    my $is_http = $error->isa('Spike::Error::HTTP');

    my $handlers = $errors->{$class} || ($is_http && $errors->{$error->value});

    throw $error if !$handlers;

    %$res = %{ $is_http ?
        $req->new_response($error->value, $error->headers) :
        $req->new_response(HTTP_INTERNAL_SERVER_ERROR)
    };

    my ($handler, $prepare, $finalize) = @$handlers;

    $finalize->($req, $res,
        $handler->($req, $res,
            $prepare->($req, $res, $error)
        )
    );
}

sub handler {
    my ($self, $req, $res) = @_;

    my $path = $req->safe_path;
    my @handlers = $self->cache->get($path);

    if (!@handlers) {
        my ($found, $last_found) = $self->route->find($path);
        @handlers = ($found, $self->_find_handlers($found || $last_found));

        $self->cache->store($path, @handlers);
    }

    my ($route, $prepare, $finalize, $errors, $names) = @handlers;

    $req->_bind_named_url_parameters(zip @$names, @{[ split m!/!, $path ]});

    my @methods = $self->debug && $self->_try_static($req, $res, $path) ? qw(GET) : ();
    my $handler;

    if ($route) {
        $handler = $route->method($req->method) || $route->method;
        push @methods, $route->methods;
    }

    if (!$handler) {
        if (@methods) {
            $self->_handle_error($req, $res, new Spike::Error::HTTP(
                HTTP_METHOD_NOT_ALLOWED, allow => join(', ', uniq @methods)
            ), $errors);
        }
        else {
            $self->_handle_error($req, $res, HTTP_NOT_FOUND, $errors);
        }
        return;
    }

    try {
        $finalize->($req, $res,
            $handler->($req, $res,
                $prepare->($req, $res)
            )
        );
    }
    catch (Spike::Error::HTTP_OK $error) {
        throw $error;
    }
    catch (Spike::Error::HTTP $error) {
        carp "HTTP error: status=".$error->value.", text=\"".$error->text."\"";
        $self->_handle_error($req, $res, $error, $errors);
    }
    catch (Spike::Error $error) {
        carp "Error: class=".ref($error).", text=\"".$error->text."\"";
        $self->_handle_error($req, $res, $error, $errors);
    }
    catch ($error) {
        carp $error;
        $self->_handle_error($req, $res, HTTP_INTERNAL_SERVER_ERROR, $errors);
    }
}

sub startup {}

1;
