package BookmarkController;
use Moose;

use Encode;
use HTTP::Date qw{time2isoz time2iso time2str str2time};
use JSON;
use Bookmarks;
use URI;
use Template;

has dbname => (
    is => 'ro',
    required => 1,
);
has bookmarks => (
    is => 'ro',
    handles => [qw{get_bookmark}],
    builder => '_build_bookmarks',
    lazy => 1,
);
has base_uri => (
    is => 'ro',
    builder => '_build_base_uri',
    lazy => 1,
);
has request => (
    is => 'ro',
    required => 1,
);

sub _build_bookmarks {
    my $self = shift;
    return Bookmarks->new({
        dbname   => $self->dbname,
        base_uri => $self->base_uri,
    });
}

sub _build_base_uri {
    my $self = shift;
    my $url = $self->request->base;

    $url .= '/' unless $url =~ m{/$};
    return URI->new($url);
}

sub _get_list_links {
    my $self = shift;
    my ($self_type, $query) = @_;
    my @links = (
        {
            text => 'JSON',
            type => 'application/json',
            query => {
                %$query,
                format => 'json',
            },
        },
        {
            text => 'XBEL',
            type => 'application/xml',
            query => {
                %$query,
                format => 'xbel',
            },
        },
        {
            text => 'Atom',
            type => 'application/atom+xml',
            path => 'feed',
            query => {
                %$query,
            },
        },
        {
            text => 'CSV',
            type => 'text/csv',
            query => {
                %$query,
                format => 'csv',
            },
        },
        {
            text => 'URI List',
            type => 'text/uri-list',
            query => {
                %$query,
                format => 'text',
            },
        },
        {
            text => 'HTML',
            type => 'text/html',
            query => {
                %$query,
            },
        },
    );

    for my $link (@links) {
        $link->{rel}  = $link->{type} eq $self_type ? 'self' : 'alternate';
        $link->{href} = URI->new_abs($link->{path} || '', $self->base_uri);
        $link->{href}->query_form($link->{query});
    }

    return @links;
}

sub find_or_new {
    my $self = shift;

    my $bookmark = $self->bookmarks->get_bookmark({ uri => $self->request->param('uri') });
    if ($bookmark) {
        # redirect to the view of the existing bookmark
        return [301, [Location => $bookmark->bookmark_uri], []];
    } else {
        # bookmark was not found; show the form to create a new bookmark
        my $template = Template->new;
        $template->process(
            'bookmark.tt',
            { 
                bookmark => {
                    uri   => $self->request->param('uri'),
                    title => $self->request->param('title') || '',
                },
            },
            \my $output,
        );
        return [404, ['Content-Type' => 'text/html; charset=UTF-8'], [$output]];
    }
}

sub list {
    my $self = shift;

    # list all the bookmarks 
    my $mtime = $self->bookmarks->get_last_modified_time;

    my $format = $self->request->param('format') || 'html';

    my @tags = grep { $_ ne '' } $self->request->param('tag');
    my $query = $self->request->param('q');
    my $limit = $self->request->param('limit');
    my $offset = $self->request->param('offset');
    my @resources = $self->bookmarks->get_bookmarks({
        query  => $query,
        tag    => \@tags,
        limit  => $limit,
        offset => $offset,
    });
    my @all_tags = $self->bookmarks->get_tags({ selected => $tags[0] });
    my @cotags = $self->bookmarks->get_cotags({
        query  => $query,
        tag    => \@tags,
    });
    
    my $title = 'Bookmarks' . (@tags ? " tagged as " . join(' & ', @tags) : '') . ($query ? " matching '$query'" : '');

    if ($format eq 'json') {
        my $json = decode_utf8(
            JSON->new->utf8->convert_blessed->encode({
                bookmarks => \@resources,
            })
        );
        return [200, ['Content-Type' => 'application/json; charset=UTF-8'], [$json]];
    } elsif ($format eq 'xbel') {
        require XML::XBEL;
        #TODO: conditional support; if XML::XBEL is not present, return a 5xx response

        my $xbel = XML::XBEL->new;

        $xbel->new_document({
            title => $title,
        });

        for my $bookmark (@resources) {
            my $cdatetime = time2isoz $bookmark->ctime;
            my $mdatetime = time2isoz $bookmark->mtime;
            # make the timestamps W3C-correct
            s/ /T/ foreach ($cdatetime, $mdatetime);

            $xbel->add_bookmark({
                href     => $bookmark->uri,
                title    => $bookmark->title,
                desc     => 'Tags: ' . join(', ', @{ $bookmark->tags }),
                added    => $cdatetime,
                #XXX: are we sure that modified is the mtime of the bookmark or the resource?
                modified => $mdatetime,
            });
        }

        return [200, ['Content-Type' => 'application/xml; charset=UTF-8'], [$xbel->toString]];
    } elsif ($format eq 'text') {
        my $text = join '', 
            map {
                sprintf "# %s\n# Tags: %s\n%s\n",
                $_->title,
                join(', ', @{ $_->tags }), 
                $_->uri
            } @resources;
        return [200, ['Content-Type' => 'text/uri-list; charset=UTF-8'], [$text]];
    } elsif ($format eq 'csv') {
        require Text::CSV::Encoded;
        my $csv = Text::CSV::Encoded->new({ encoding_out => 'utf8' });
        my $text = qq{id,uri,title,tags,ctime,mtime\n};
        for my $bookmark (@resources) {
            my $success = $csv->combine(
                $bookmark->id,
                $bookmark->uri,
                $bookmark->title,
                join(' ', @{ $bookmark->tags }),
                $bookmark->ctime,
                $bookmark->mtime,
            );
            $text .= $csv->string . "\n" if $success;
        }

        # include the local timestamp in the attchment filename
        my $dt = time2iso;
        $dt =~ s/[^\d]//g;

        my $filename = sprintf(
            'bookmarks-%s-%s.csv',
            join('_', @tags),
            $dt,
        );

        return [200, ['Content-Type' => 'text/csv; charset=UTF-8', 'Content-Disposition' => sprintf('attachement; filename="%s"', $filename)], [$text]];
    } else {
        my $template = Template->new;

        $template->process(
            'list.tt',
            {
                base_url     => $self->base_uri,
                title        => $title,
                query        => $query,
                selected_tag => $tags[0],
                search_tags  => \@tags,
                links        => [ $self->_get_list_links('text/html', { q => $query, tag => \@tags }) ],
                all_tags     => \@all_tags,
                cotags       => \@cotags,
                resources    => \@resources,
            },
            \my $output,
        );
        return [200, ['Content-Type' => 'text/html; charset=UTF-8'], [$output]];
    }
}

sub feed {
    my $self = shift;

    my $query = $self->request->param('q');
    my @tags = grep { $_ ne '' } $self->request->param('tag');
    my $title = 'Bookmarks' . (@tags ? " tagged as " . join(' & ', @tags) : '');

    require XML::Atom;
    $XML::Atom::DefaultVersion = "1.0";

    require XML::Atom::Feed;
    require XML::Atom::Entry;
    require XML::Atom::Link;
    require XML::Atom::Category;

    my $feed = XML::Atom::Feed->new;
    $feed->title($title);

    for my $link ($self->_get_list_links('application/atom+xml', { q => $query, tag => \@tags })) {
        my $atom_link = XML::Atom::Link->new;
        $atom_link->type($link->{type});
        $atom_link->rel($link->{rel});
        $atom_link->href($link->{href}->canonical);
        $feed->add_link($atom_link);
    }

    # construct a feed from the most recent 12 bookmarks
    for my $bookmark ($self->bookmarks->get_bookmarks({ query => $query, tag => \@tags, limit => 12 })) {
        my $entry = XML::Atom::Entry->new;
        $entry->id($bookmark->bookmark_uri->canonical);
        $entry->title($bookmark->title);
        
        my $link = XML::Atom::Link->new;
        $link->href($bookmark->uri);
        $entry->add_link($link);
        
        $entry->summary('Tags: ' . join(', ', @{ $bookmark->tags }));

        my $cdatetime = time2isoz $bookmark->ctime;
        my $mdatetime = time2isoz $bookmark->mtime;
        # make the timestamp W3C-correct
        s/ /T/ foreach ($cdatetime, $mdatetime);
        $entry->published($cdatetime);
        $entry->updated($mdatetime);
        
        for my $tag (@{ $bookmark->tags }) {
            my $category = XML::Atom::Category->new;
            $category->term($tag);
            $entry->add_category($category);
        }

        $feed->add_entry($entry);
    }

    return [200, ['Content-Type' => 'application/atom+xml; charset=UTF-8'], [$feed->as_xml]];
}

#TODO: better method name
# returns 1 if there is an If-Modified-Since header and it is newer than the given $mtime
# returns 0 if there is an If-Modified-Since header but the $mtime is newer
# returns undef if there is no If-Modified-Since header
sub _check_modified {
    my $self = shift;
    my $mtime = shift;

    # check If-Modified-Since header to return cache response
    if ($self->request->env->{HTTP_IF_MODIFIED_SINCE}) {
        my $cache_time = str2time($self->request->env->{HTTP_IF_MODIFIED_SINCE});
        return $mtime <= $cache_time ? 1 : 0;
    } else {
        return;
    }
}

sub view {
    my ($self, $id) = @_;

    my $format = $self->request->param('format') || 'html';

    my $bookmark = $self->get_bookmark({ id => $id });
    if ($bookmark) {
        return [304, [], []] if $self->_check_modified($bookmark->mtime);

        my $last_modified = time2str($bookmark->mtime);
        
        if ($format eq 'json') {
            my $json = decode_utf8(JSON->new->utf8->convert_blessed->encode($bookmark));
            return [200, ['Content-Type' => 'application/json; charset=UTF-8', 'Last-Modified' => $last_modified], [$json]];
        } else {
            # display the bookmark form for this bookmark
            my $template = Template->new;
            $template->process(
                'bookmark.tt',
                { bookmark => $bookmark },
                \my $output,
            );
            return [200, ['Content-Type' => 'text/html; charset=UTF-8', 'Last-Modified' => $last_modified], [$output]];
        }
    } else {
        return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], ["Boomark $id not found"]];
    }
}

sub view_field {
    my ($self, $id, $field) = @_;

    my $bookmark = $self->get_bookmark({ id => $id });
    if ($bookmark) {
        return [304, [], []] if $self->_check_modified($bookmark->mtime);

        # check whether the requested field is part of the bookmark
        if (!$bookmark->meta->has_attribute($field)) {
            return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], [qq{"$field" is not a valid bookmark data field}]];
        }

        # respond with just the requested field as plain text
        my $value = $bookmark->$field;
        my $last_modified = time2str($bookmark->mtime);
        return [200, ['Content-Type' => 'text/plain; charset=UTF-8', 'Last-Modified' => $last_modified], [ref $value eq 'ARRAY' ? join(' ', @{ $value }) : $value]];
    } else {
        return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], ["Boomark $id not found"]];
    }
}

sub create_and_redirect {
    my $self = shift;

    my $uri   = $self->request->param('uri');
    my $title = $self->request->param('title');
    my @tags  = split ' ', $self->request->param('tags');

    my $bookmark = $self->bookmarks->add({
        uri   => $uri,
        title => $title,
        tags  => \@tags,
    });

    #TODO: not RESTful; the proper RESTful response would be a 201
    return [303, ['Location' => $bookmark->bookmark_uri->canonical], []];
}

sub update_and_redirect {
    my $self = shift;
    my $id = shift;

    my $bookmark = $self->bookmarks->get_bookmark({ id => $id });
    if ($bookmark) {
        # update the URI, title, and tags
        $bookmark->uri($self->request->param('uri'));
        $bookmark->title($self->request->param('title'));
        $bookmark->tags([ split ' ', $self->request->param('tags') || '' ]);

        # write to the database
        $self->bookmarks->update($bookmark);

        #TODO: not RESTful; proper response would be a 200
        return [303, ['Location' => $bookmark->bookmark_uri->canonical], []];
    } else {
        return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], ["Boomark $id not found"]];
    }
}

1;
