use v5.26;
use warnings;
use utf8;

package App::LazyShoppingList::API::v1;
use App::LazyShoppingList::API;
use Dancer2;

our $VERSION = '0.1';

use JSON();

use experimental 'signatures';

set charset      => 'UTF-8';
set serializer => 'JSON';

hook before_request => sub {
    get_database->txn_begin;
};

hook after_request => sub {
    my $dbh = get_database;

    if (response->status =~ /^5/) {
        $dbh->txn_rollback;
    }
    else {
        $dbh->txn_commit;
    }
};

# get the URI for the list of shopping lists
get '/' => sub {
    return { lists => uri_for('/list') };
};

# get the list of shopping lists
get '/list' => sub {
    my $dbh = get_database;

    my %r = ( lists_version => get_lists_version($dbh), lists => [] );
    for my $list (
        $dbh->resultset('ShoppingList')
        ->search( undef,
            { order_by => { -asc => 'name' } } )->all
        )
    {
        push @{ $r{lists} },
            { uri => uri_for( "/list/" . $list->id ), name => $list->name };
    }

    return \%r;
};

# create shopping list
post '/list' => sub {
    my $req = request_data;

    my $name = $req->{name};
    unless ($name) {
        return exception 400, "Missing list name";
    }

    my $dbh = get_database;
    my $list = $dbh->resultset('ShoppingList')->create({
            name => $name});
    $list->discard_changes;

    my  $lists_ver = increment_lists_version($dbh);

    return {
        uri           => uri_for( '/list/' . $list->id ),
        version       => $list->version,
        lists_version => $lists_ver
    };
};

# get shopping list items
get '/list/:list_id' => sub {
    my $list_id = route_parameters->get('list_id');
    length($list_id) and $list_id =~ /^\d{1,18}$/ or return invalid_input;

    my $dbh = get_database;

    warn $dbh;
    my %r = ( items => [] );

    $r{lists_version} = get_lists_version($dbh);

    my $list = $dbh->resultset('ShoppingList')->find($list_id);

    unless ($list) {
        return exception 404, "No list with that ID found";
    }

    $r{version} = $list->version;

    my @items = $dbh->resultset('ShoppingListItem')->search(
        { shopping_list => $list->id },
        { order_by      => { -asc => 'id' } }
    )->all;
    for my $item (@items) {
        push @{ $r{items} },
            {   uri =>
                uri_for( sprintf( "/list/%s/%s", $list->id, $item->id ) ),
                description => $item->description,
                done        => JSON->boolean( $item->done ),
                version     => $item->version,
            };
    }

    return \%r;
};

# create shopping list item
post '/list/:id' => sub {
    my $list_id = route_parameters->get('id');
    length($list_id) and $list_id =~ /^\d{1,18}$/
        or return invalid_input('bad list ID');

    my $req = request_data;

    my $descr = $req->{description};
    my $done  = JSON->boolean( $req->{done} // 0 );

    my $dbh = get_database;

    my %r = (
        lists_version => get_lists_version($dbh),
    );

    my $list = $dbh->resultset('ShoppingList')->find($list_id);

    unless ($list) {
        exception 404, "No such list";
    }

    my $item = $dbh->resultset('ShoppingListItem')->create(
        {   shopping_list => $list->id,
            description   => $descr,
            done          => $done,
        }
    );
    $item->discard_changes;

    $list->update( { version => $list->version + 1 } );

    $r{uri} = uri_for( sprintf( "list/%s/%s", $list->id, $item->id ) );
    $r{version} = $item->version;
    $r{list_version} = $list->version;

    return \%r;
};

# modify shopping list item
put '/list/:list_id/:item_id' => sub {
    my $list_id = route_parameters->get('list_id');
    length($list_id) and $list_id =~ /^\d{1,18}$/
        or return invalid_input('bad list ID');

    my $item_id = route_parameters->get('item_id');
    length($item_id) and $item_id =~ /^\d{1,18}$/
        or return invalid_input('bad item ID');

    my $req = request_data;

    my $descr = $req->{description};
    my $done =
        exists $req->{done} ? JSON->boolean( $req->{done} // 0 ) : undef;
    my $version = $req->{version};

    length($version) and $version =~ /^\d{1,18}$/
        or return invalid_input('bad version');

    my $dbh = get_database;

    my %r = (
        lists_version => get_lists_version($dbh),
    );

    my $list = $dbh->resultset('ShoppingList')->find($list_id);
    unless ($list) {
        return exception 404, "No such list";
    }

    my $item = $dbh->resultset('ShoppingListItem')->find({shopping_list => $list->id, id => $item_id});
    unless ($item) {
        return exception 404, "No such item";
    }

    # in case no real changes are needed will return the current state
    if (   defined($descr) and $descr ne ( $item->description // '' )
        or defined($done) and $done != $item->done )
    {
        unless ($version == $item->version) {
            return exception 409,
                sprintf( 'Outdated version (current is %d)', $item->version );
        }

        $item->update({
            defined($descr) ? (description  => $descr) : (),
            defined($done) ? (done =>  $done) : (),
            version => $version + 1,
        });

        $list->update( { version => $list->version + 1 } );
    }

    $r{uri} = uri_for( sprintf( "list/%s/%s", $list->id, $item->id ) );
    $r{version} = $item->version;
    $r{list_version} = $list->version;

    return \%r;
};

# modify shopping list
put '/list/:list_id' => sub {
    my $list_id = route_parameters->get('list_id');
    length($list_id) and $list_id =~ /^\d{1,18}$/
        or return invalid_input('bad list ID');

    my $req = request_data;

    my $name    = $req->{name}    // '';
    my $version = $req->{version} // -1;

    length($version) and $version =~ /^\d{1,18}$/
        or return invalid_input('bad version');

    my $dbh = get_database;

    my $list = $dbh->resultset('ShoppingList')->find($list_id);
    unless ($list) {
        return exception 404, "No such list";
    }

    # in case no real changes are needed will return the current state
    if ( $name ne ( $list->name // '') ) {
        unless ($version == $list->version) {
            return exception 409,
                sprintf( 'Outdated version (current is %d)', $list->version );
        }

        $list->update({
            name =>  $name // '',
            version => $version + 1,
        });
    }

    return {
        version       => $list->version,
        lists_version => increment_lists_version($dbh),
    };
};

# delete shopping list
del '/list/:list_id' => sub {
    my $list_id = route_parameters->get('list_id');
    length($list_id) and $list_id =~ /^\d{1,18}$/
        or return invalid_input('bad list ID');

    my $dbh = get_database;

    my %r = (
        lists_version => get_lists_version($dbh),
    );

    my $list = $dbh->resultset('ShoppingList')->find($list_id);
    if ($list) {
        $list->delete;
        $r{lists_version} = increment_lists_version($dbh)
    }

    return \%r;
};

# delete shopping list item
del '/list/:list_id/:item_id' => sub {
    my $list_id = route_parameters->get('list_id');
    length($list_id) and $list_id =~ /^\d{1,18}$/
        or return invalid_input('bad list ID');

    my $item_id = route_parameters->get('item_id');
    length($item_id) and $item_id =~ /^\d{1,18}$/
        or return invalid_input('bad item ID');

    my $dbh = get_database;

    my %r = (
        lists_version => get_lists_version($dbh),
    );

    my $list = $dbh->resultset('ShoppingList')->find($list_id)
        or return exception 404, 'No such list';

    my $item = $dbh->resultset('ShoppingListItem')
        ->find( { shopping_list => $list->id, id => $item_id } );

    if ($item) {
        $item->delete;
        $list->update({version => $list->version + 1});
    }

    $r{list_version} = $list->version;

    return \%r;
};

true;
