#!/usr/bin/perl

use strict;
use warnings;

use Perl6::Parameters;

use Getopt::Long::Descriptive qw/ :all      /;
use Hash::Merge               qw/ merge     /;
use Pod::Usage                qw/ pod2usage /;
use WWW::PivotalTracker qw/
    add_story
    all_stories
    delete_story
    project_details
    show_story
/;

use aliased 'Config::Any' => 'Config';
use aliased 'File::HomeDir';

=head1 NAME

pivotal_tracker - Command-line interface to L<http://www.pivotaltracker.com>

=head1 SYNOPSIS

pivotal_tracker [options]

 General options:
      --help             Brief help
      --man              Full documentation
   -v --verbose          Make noise
   -p --project          Named project (from config file) to query/update
   -P --project-id       Project ID to query/update
   -i --story-id         Story ID to query/update

 Actions:
      --list-projects    List all named projects, and their project IDs
      --show-project     Display the current settings for a project
   -a --add-story        Add a new story
   -D --delete-story     Delete an existing story
   -s --show-story       Show single story
   -A --all-stories      Show all stories for project
   -c --comment          Comment on story
   -u --update-story     Update the details of a story
      --deliver-all      Deliver all deliverable stories

 Story Display Options:
   -n --show-notes       Show stories' notes (if any)
   -1 --one-line         Show stories one per line

 Story Attributes:
   -S --story            Story title to create
   -b --requested-by     Who requested the story
   -l --label            Label to apply (May appear more than once, or be a single comma separated list
   -e --estimate         Point estimate for story
   -C --created-at       Date/Time story was created (Defaults to 'now')
   -d --deadline         Deadline of the story (Only applicable to 'release' story type)

 Story Type Options:
   --feature             Set story type to 'feature' (Default for new stories)
   --release             Set story type to 'release'
   --bug                 Set story type to 'bug'
   --chore               Set story type to 'chore'

 Story State Options:
   --unscheduled         Story has not been scheduled, and is in the icebox
   --unstarted           Story is in the backlog
   --started             Work has started on the story
   --finished            Work has been completed on the story
   --delivered           The story has been delivered for review
   --accepted            The story has been accepted after review
   --rejected            The story has been rejected after review

=head1 OPTIONS

=over 8

=item B<--help>

Print a brief help message and exit.

=item B<--man>

Print the man page, and exit.

=item B<-v> B<--verbose>

Show more verbose output.

=item B<-P> B<--project-id>

Use this Project ID, instead of the one speficied in the config file (if any).

=back

=head1 DESCRIPTION

B<pivotal_tracker> provides a command line interface to the Pivotal Tracker
API (L<http://www.pivotaltracker.com/>).  This is meant to be used by humans.
For a programmatic interface, please see L<WWW::PivotalTracker>.

=head1 CONFIG

B<~/.pivotal_tracker.yml> contains the configuration of default values, and 
project name, to project ID mappings.

    ---
    General:
      APIKey: 'c0ffe'
      Me: Jacob Helwig
      DefaultProject: Testing
    Projects:
      Testing: 1
      Another Project: 2

=head1 BUGS

Many flags aren't implemented yet, and are merely placeholders at this point.

=cut

our $VERSION = "0.11";

my $options_format = "usage: %c %o";
my @options = (
    [                                                                          ],
    [ 'General options:',                                                      ],
    [ 'help'           => 'Show brief help',                                   ],
    [ 'man'            => 'Full documentation',                                ],
    [ 'verbose|v'      => 'Make noise',                                        ],
    [ 'project|p=s'    => 'Named project (from config file) to query/update',  ],
    [ 'project-id|P=i' => 'Project ID to query/update (Required unless --project is specified, or General.DefaultProject is set', ],
    [ 'story-id|i=i'   => 'Story ID to query/update',                          ],
    [                                                                          ],
    [ 'Actions',                                                               ],
    [ 'list-projects'  => 'List all named projects, and their project IDs',    ],
    [ 'show-project'   => 'Display the current settings for a project',        ],
    [ 'add-story|a'    => 'Add a new story',                                   ],
    [ 'delete-story|D' => 'Delete an existing story',                          ],
    [ 'show-story|s'   => 'Show single story',                                 ],
    [ 'all-stories|A'  => 'Show all stories for project',                      ],
    [ 'update-story|u' => 'Update the details of a story',                     ],
    [ 'comment|c'      => 'Comment on story',                                  ],
    [ 'deliver-all'    => 'Deliver all deliverable stories',                   ],
    [                                                                          ],
    [ ' Story Display Options:',                                               ],
    [ 'show-notes|n'   => "Show stories' notes (if any)",                      ],
    [ 'one-line|1'     => "Show stories one per line",                         ],
    [                                                                          ],
    [ 'Story Attributes:',                                                     ],
    [ 'story|S=s'        => 'Story title to create',                           ],
    [ 'requested-by|b=s' => 'Who requested the story',                         ],
    [ 'label|l=s@'       => 'Label to apply (May appear more than once, or be a single comma separated list', ],
    [ 'estimate|e=s'     => "Point estimate for story",                        ],
    [ 'created-at|C=s'   => "Date/Time story was created (Defaults to 'now')", ],
    [ 'deadline|d=s'     => "Deadline of the story (Only applicable to 'release' story type)", ],
    [                                                                          ],
    [ 'Story Type Options:',                                                   ],
    [ 'story-type=s'   => [
            [ "feature" => "Set story type to 'feature' (Default for new stories)", ],
            [ "release" => "Set story type to 'release'", ],
            [ "bug"     => "Set story type to 'bug'",     ],
            [ "chore"   => "Set story type to 'chore'",   ],
        ],
    ],
    [                                                                          ],
    [ 'Story State Options:',                                                  ],
    [ 'state=s'          => [
            [ "unscheduled" => "Story has not been scheduled, and is in the icebox", ],
            [ "unstarted"   => "Story is in the backlog",                            ],
            [ "started"     => "Work has started on the story",                      ],
            [ "finished"    => "Work has been completed on the story",               ],
            [ "delivered"   => "The story has been delivered for review",            ],
            [ "accepted"    => "The story has been accepted after review",           ],
            [ "rejected"    => "The story has been rejected after review",           ],
        ],
    ],
);

sub _parse_config()
{
    my $cfg = Config->load_files({
        files           => [HomeDir->my_home() . '/.pivotal_tracker.yml'],
        flatten_to_hash => 1,
        use_ext         => 1,
    });

    my $config = {};
    foreach my $file (keys %$cfg) {
        $config = merge($config, $cfg->{$file});
    }

    return $config;
}

my $cfg = _parse_config();

sub _named_projects_string()
{
    return "No named projects found." unless scalar keys %{$cfg->{'Projects'}};

    my ($max_project_name) = sort { $b <=> $a } map { length($_) } keys %{$cfg->{'Projects'}};

    my @projects = ();
    foreach my $project (keys %{$cfg->{'Projects'}}) {
        my $default_marker = $cfg->{'General'}->{'DefaultProject'} eq $project ? '*' : ' ';
        push @projects, sprintf("%s%-" . $max_project_name . "s %s", $default_marker, $project, $cfg->{'Projects'}->{$project});
    }

    my $projects_text = "Configured Projects:\n\n"
        . join("\n", @projects) . "\n";

    return $projects_text;
}

sub _api_key()
{
    return $cfg->{'General'}->{'APIKey'};
}

sub _get_project_details($project_id)
{
    my $response = project_details(_api_key(), $project_id);

    return $response;
}

sub _get_all_stories($project_id)
{
    return all_stories(_api_key(), $project_id);
}

sub _get_story($project_id, $story_id)
{
    return show_story(_api_key(), $project_id, $story_id);
}

sub _create_story($story_opts)
{
    my $project_id = delete $story_opts->{'project_id'};

    my $response = add_story(_api_key(), $project_id, $story_opts);

    return $response;
}

sub _delete_story($project_id, $story_id)
{
    return delete_story(_api_key(), $project_id, $story_id);
}

sub _display_error($result)
{
    print STDERR "Unable to process request:\n";
    printf STDERR ("  %s\n", $_) foreach $result->{'errors'};
}

sub _display_project($project)
{
    my $project_text =
          sprintf("               Name: %s\n", $project->{'name'})
        . sprintf("        Point Scale: %s\n", $project->{'point_scale'});
    $project_text .=
          sprintf("   Iterations Start: %s\n", $project->{'week_start_day'})
            if defined $project->{'week_start_day'};
    $project_text .=
          sprintf("Weeks per Iteration: %s\n", $project->{'iteration_weeks'})
        . "\n";

    print $project_text;
}

sub _display_story($story; $options)
{
    $options->{'show_notes'} ||= 0;

    my $story_text =
          sprintf("Story %s (%s) < %s >:\n", $story->{'id'}, $story->{'story_type'}, $story->{'url'})
        . sprintf("           Name: %s\n", $story->{'name'})
        . sprintf("       Estimate: %s\n", $story->{'estimate'})
        . sprintf("          State: %s\n", $story->{'current_state'});
    $story_text .=
          sprintf("    Description: %s\n", $story->{'description'})
            if defined $story->{'description'};
    $story_text .=
          sprintf("   Requested By: %s\n", $story->{'requested_by'})
        . sprintf("        Created: %s\n", $story->{'created_at'});
    $story_text .=
          sprintf("       Deadline: %s\n", $story->{'deadline'})
            if defined $story->{'deadline'};
    $story_text .=
          sprintf("       Label(s): %s\n", join(", ", @{$story->{'labels'}}))
            if defined $story->{'labels'};

    if ($options->{'show_notes'} eq '1' && defined $story->{'notes'}) {
        $story_text .= "          Notes:\n";

        my $notes = 0;
        foreach my $note (@{$story->{'notes'}}) {
            $notes++;

            $story_text .= sprintf("            %s (%s):\n", $note->{'author'}, $note->{'date'});
            foreach my $note_line (split("\n", $note->{'text'})) {
                $story_text .= sprintf("                 %s\n", $note_line);
            }

            $story_text .= "\n" if $notes < @{$story->{'notes'}};
        }
    }

    $story_text .= "\n";

    print $story_text;
}

sub _response_was_successful($response)
{
    return $response->{'success'} eq "true";
}

sub _main()
{
    $Getopt::Long::Descriptive::MungeOptions = 1;
    my ($opts, $usage) = describe_options(
        $options_format,
        @options,
        {
            getopt_conf => [
                'gnu_getopt',
                'auto_abbrev',
                'auto_version',
            ],
        }
    );

    pod2usage(1) if $opts->{'help'};
    pod2usage(-exitstatus => 0, -verbose => 2) if $opts->{'man'};

    my $project_id = exists $opts->{'project_id'}
        ? $opts->{'project_id'}
        : $cfg->{'Projects'}->{$cfg->{'General'}->{'DefaultProject'}};

    if (exists $opts->{'list_projects'}) {
        print _named_projects_string();
    }
    if (exists $opts->{'show_project'}) {
        my $result = _get_project_details($project_id);

        if (_response_was_successful($result)) {
            _display_project($result);
            exit 0;
        }
        else {
            _display_error($result);
            exit 1;
        }
    }
    if (exists $opts->{'show_story'}) {
        unless (exists $opts->{'story_id'} || exists $opts->{'all_stories'}) {
            print STDERR "Missing --story-id <id> or --all-stories.\n";
            exit 1;
        }

        my $result;
        if (exists $opts->{'all_stories'}) {
            $result = _get_all_stories($project_id);

            if (_response_was_successful($result)) {
                my $stories = 0;
                foreach my $story (@{$result->{'stories'}}) {
                    print "=" x 50 . "\n\n" if $stories++ >= 1;
                    _display_story($story, { show_notes => $opts->{'show_notes'} });
                }
                exit 0;
            }
        }
        else {
            $result = _get_story($project_id, $opts->{'story_id'});

            if (_response_was_successful($result)) {
                _display_story($result, { show_notes => $opts->{'show_notes'} });
                exit 0;
            }
        }

        _display_error($result);
        exit 1;
    }
    elsif (exists $opts->{'add_story'}) {
        my $result = _create_story({
            project_id    => $project_id,
            name          => $opts->{'story'},
            requested_by  => ($opts->{'requested_by'} || $cfg->{'General'}->{'Me'}),
            labels        => (exists $opts->{'label'} ? join(",", @{$opts->{'label'}}) : undef),
            estimate      => $opts->{'estimate'},
            created_at    => $opts->{'created_at'},
            deadline      => $opts->{'deadline'},
            story_type    => $opts->{'story_type'},
            current_state => $opts->{'state'},
        });

        if (_response_was_successful($result)) {
            _display_story($result);
            exit 0;
        }
        else {
            _display_error($result);
            exit 1;
        }
    }
    elsif (exists $opts->{'delete_story'}) {
        my $result = _delete_story($project_id, $opts->{'story_id'});

        if (_response_was_successful($result)) {
            print $result->{'message'} . "\n";
            exit 0;
        }
        else {
            _display_error($result);
            exit 1;
        }
    }
}

_main();

# vim: set tabstop=4 shiftwidth=4: 
