#!/usr/bin/env perl
BEGIN {
  if (@ARGV and @ARGV[0] =~ /^\w/) {
    @ARGV = grep { (/^-{1,2}h\w{0,3}$/ ? ($ENV{APP_TT_HELP} = $ARGV[0], 0) : (1, 1))[1] } @ARGV;
  }
}
use Applify;
use Cwd 'abs_path';
use File::Basename;
use File::Find;
use File::HomeDir;
use File::Path 'make_path';
use File::Spec;
use IO::Handle;
use JSON::XS;
use Time::Piece;
use constant DEBUG => $ENV{APP_TT_DEBUG} || 0;

option str => description => 'Description for an event',            alias => 'd';
option str => tag         => 'Tags for an event',                   alias => 't', n_of => '@';
option str => project     => 'Project name. Normally autodetected', alias => 'p';
option str => group_by    => 'Group log output: --group-by day',    alias => 'g';

documentation 'App::tt';
version 'App::tt';

$ENV{EDITOR}         ||= 'nano';
$ENV{TT_ROUND_UP_AT} ||= 30;
$SIG{__DIE__} = sub { Carp::confess($_[0]) }
  if DEBUG;

$ENV{TIMETRACKER_MIN_TIME} ||= 300;

sub cmd_edit {
  my $self = shift;
  my $dir = readlink($self->_root) || $self->_root;
  my ($code, $date, @files) = ('');

  if (@_ and -f $_[0]) {
    return $self->_edit_with_editor($_[0]);
  }
  elsif (-t STDIN) {
    1;    # edit last entry
  }
  else {
    $code .= $_ while <STDIN>;
    $code = "sub {$code}" unless $code =~ /^\s*sub\b/s;
    $code = eval "use 5.10.1;$code" or die "Could not compile code from STDIN: $@\n";
  }

  find {
    no_chdir => 0,
    wanted   => sub {
      return unless /^(\d+)-(\d+)_(.*)\.trc$/;
      return if $date and not /$date/;
      my $f     = abs_path $_;
      my $event = decode_json(_slurp($f));
      local %_ = (date => $1, file => $f, hms => $2, project => $3);
      $event->{tags} ||= [];
      push @files, $f if !$self->project or $event->{project} eq $self->project;
      return unless $code and $self->$code($event);
      my $trc_file
        = abs_path($self->_trc_path($event->{project}, $self->_from_iso_8601($event->{start})));
      $self->_fill_duration($event);
      _spurt(encode_json($event) => $trc_file);
      unlink $f or die "rm $f: $!" if $f ne $trc_file;
    }
  }, $dir;

  if (@files == 1 or !$code) {
    return $self->_edit_with_editor(+(sort { $b cmp $a } @files)[0]);
  }

  return 0;
}

sub cmd_export {
  my $self = shift;
  my $res  = $self->_log(@_);

  $res->{rounded} = 0;
  $self->_say('%s,%s,%s,%s,%s,%s', 'date', 'project', 'hours', 'rounded', 'tags', 'description');

  for my $event (sort { $a->{start} <=> $b->{start} } @{$res->{log}}) {
    my $seconds = $event->{seconds};
    my ($hours, $minutes, $rounded);

    $hours = int($seconds / 3600);
    $seconds -= $hours * 3600;
    $minutes = int($seconds / 60);
    $rounded = $hours + ($minutes >= $ENV{TT_ROUND_UP_AT} ? 1 : 0);
    $hours += sprintf '%.1f', $minutes / 60;
    $res->{rounded} += $rounded;

    $self->_say(
      '%s,%s,%s,%s,%s,%s',
      $event->{start}->ymd,
      $event->{project} || 'unknown',
      $hours, $rounded,
      join(' ', grep {$_} map { split /[, ]/ } @{$event->{tags}}),
      $event->{description} // ''
    );
  }

  $self->_diag(
    "\nExact hours: %s. Rounded hours: %s Events: %s",
    $self->_hms_duration($res->{seconds}, 'hm'),
    $res->{rounded}, $res->{events},
  );

  return 0;
}

sub cmd_help {
  my $self  = shift;
  my $for   = shift || 'app';
  my $today = $self->_now->ymd;
  my @help;

  if ($for eq 'app') {
    $self->_script->print_help;
    return 0;
  }

  require App::tt;
  open my $POD, '<', $INC{'App/tt.pm'} or die "Cannot open App/tt.pm: $!";
  while (<$POD>) {
    s/\b2016-06-28(T\d+:)/$today$1/g;    # make "register" command easier to copy/paste
    push @help, $_ if /^=head2 $for/ ... /^=(head|cut)/;
  }

  # remove =head and =cut lines
  shift @help;
  pop @help;

  die "Could not find help for $for.\n" unless @help;
  $self->_say("@help");
  return 0;
}

sub cmd_log {
  my $self = shift;
  my $res  = $self->_log(@_);

  for my $event (sort { $a->{start} <=> $b->{start} } @{$res->{log}}) {
    my $start = $event->{start};
    $self->_say(
      "%3s %2s %02s:%02s  %5s  %-$res->{max_project_chr}s  %s",
      $start->month,
      $start->mday,
      $start->hour,
      $start->minute,
      $self->_hms_duration($event->{seconds}, 'hm'),
      $event->{project} || '---',
      join(',', @{$event->{tags}}),
    );
  }

  $self->_diag(
    "\nTotal for %s events since %s: %s",
    $res->{events},
    join(' ', $res->{when}->month, $res->{when}->year),
    $self->_hms_duration($res->{seconds}, 'hms')
  );

  return 0;
}

sub cmd_register {
  my ($self, $start, $stop, $project, $description, $tags) = @_;
  my ($trc_file, %event);

  if (@_ == 1 and !-t STDIN) {
    while (<STDIN>) {
      next if /^\s*#/;
      chomp;
      my @args = split /\t/;
      $self->cmd_register(@args) if $args[0] and $args[1] and $args[2];
    }
    return 0;
  }

  return $self->cmd_help('register') unless $start and $stop and $project;

  $description ||= '';
  $tags        ||= '';
  $trc_file = $self->_trc_path($project, $self->_from_iso_8601($start));

  if (my $hms = $stop =~ /^(\d+:\d+:\d+)$/ ? $1 : '') {
    $stop = $start;
    $stop =~ s!\d+:\d+:\d+$!$hms!;
  }

  %event = (
    __CLASS__   => 'App::TimeTracker::Data::Task',
    project     => $project,
    start       => $start,
    stop        => $stop,
    user        => scalar(getpwuid $<),
    tags        => [split /,/, $tags || ''],
    description => $description || $self->description,
  );

  if (-e $trc_file) {
    $self->_diag("Already registered: $start $stop $project $description $tags");
    return 1;
  }

  $self->_fill_duration(\%event);

  if ($event{seconds} < $ENV{TIMETRACKER_MIN_TIME}) {
    $self->_diag("Skipping $project - $start - $stop. Too short duration ($event{duration})");
    return 1;
  }

  make_path(dirname($trc_file));
  _spurt(encode_json(\%event) => $trc_file);
  $self->_say('Registered "%s" at %s with duration %s', @event{qw(project start duration)});
  return 0;
}

sub cmd_start {
  my ($self, @args) = @_;
  my $event = {};
  my $trc_file;

  $self->_set_now(@args);
  $self->project($args[0]) if $args[0] and $args[0] =~ /^[A-Za-z0-9-]+$/;
  $self->project(basename(Cwd::getcwd)) if -d '.git' and !$self->project;
  return $self->cmd_help('start') unless $self->project;

  $trc_file = $self->_trc_path($self->project, $self->_now);
  warn "[APP_TT] start $trc_file\n" if DEBUG;

  if (!$self->project) {
    $self->_diag(
      "Cannot 'start' with unknown project name. Are you sure you are inside a git project?");
    return 1;    # Operation not permitted
  }

  # change start time on current event
  if ($self->{custom_now}) {
    my ($trc_file, $e) = $self->_get_previous_event;
    if ($e->{start} and !$e->{stop}) {
      $event = $e;
      $event->{start} = $self->_now->datetime;
    }
  }

  $self->_stop_previous({start => 1}) unless $event->{start};
  $self->_add_event_info($event);
  make_path(dirname($trc_file));
  _spurt(encode_json($event) => $trc_file);
  _spurt($trc_file => File::Spec->catfile($self->_root, 'previous'));
  $self->_say('Started working on project "%s" at %s.', $event->{project}, $self->_now->hms(':'));
  return 0;
}

sub cmd_stop {
  my $self = shift;
  my ($trc_file, $event) = $self->_get_previous_event;

  if ($event->{start}) {
    my $now = Time::Piece->new;
    $self->{now} = $self->_from_iso_8601($event->{start});
    $self->{now} = $self->_tp(H => $now->hour, M => $now->minute, S => $now->second);
  }

  $self->_set_now(@_);
  $self->_stop_previous;
}

sub cmd_status {
  my $self = shift;
  my ($trc_file, $event) = $self->_get_previous_event;

  warn "[APP_TT] status $trc_file\n" if DEBUG;

  if (!$event->{start}) {
    $self->_say('No event is being tracked.');
    return 3;    # No such process
  }
  elsif ($event->{stop}) {
    $self->_say('Stopped working on "%s" at %s after %s',
      $event->{project}, $event->{stop}, $event->{duration});
    return 0;
  }
  else {
    my $duration = $self->_now - $self->_from_iso_8601($event->{start}) + $self->_tzoffset;
    $self->_say('Been working on "%s", for %s',
      $event->{project}, $self->_hms_duration($duration, 'hms'));
    return 0;
  }
}

sub _add_event_info {
  my ($self, $event) = @_;
  my $tags = $self->tag || [];

  $event->{__CLASS__} ||= 'App::TimeTracker::Data::Task';
  $event->{project}   ||= $self->project;
  $event->{seconds}   ||= undef;
  $event->{start}     ||= $self->_now->datetime;
  $event->{user}      ||= scalar(getpwuid $<);
  $event->{tags}      ||= [];

  $event->{description} = $self->description if $self->description;

  for my $t (ref $tags ? @$tags : $tags) {
    push @{$event->{tags}}, $t;
  }
}

sub _edit_with_editor {
  require File::Temp;
  my ($self, $trc_file) = @_;
  my $fh    = File::Temp->new;
  my $event = decode_json(_slurp($trc_file));

  printf $fh "# %s\n", $trc_file;

  for my $k (qw(project tags start stop user)) {
    $event->{$k} = join ', ', @{$event->{$k} || []} if $k eq 'tags';
    printf $fh "%-8s %s\n", "$k:", $event->{$k};
  }

  close $fh;
  system $ENV{EDITOR} => "$fh";

  for (split /\n/, _slurp("$fh")) {
    my ($k, $v) = /^(\w+)\s*:\s*(.+)/ or next;
    $v = [split /\W+/, $v] if $k eq 'tags';
    $event->{$k} = $v;
  }

  $self->_fill_duration($event);
  _spurt(encode_json($event) => $trc_file);

  return 0;
}

sub _diag {
  my ($self, $format) = (shift, shift);
  warn "$format\n" unless @_;
  warn sprintf "$format\n", @_;
}

sub _say {
  my ($self, $format) = (shift, shift);
  print "$format\n" unless @_;
  print sprintf "$format\n", @_ if @_;
}

sub _fill_duration {
  my ($self, $event) = @_;
  my $start    = $self->_from_iso_8601($event->{start});
  my $stop     = $self->_from_iso_8601($event->{stop});
  my $duration = $stop - $start;

  $event->{seconds}  = $duration->seconds;
  $event->{duration} = $self->_hms_duration($duration);
}

sub _fill_log_days {
  my ($self, $last, $now) = @_;
  my $interval = int(($now - $last)->days);

  map {
    my $t = $last + $_ * 86400;
    +{seconds => 0, start => $t, tags => [$t->day]}
  } 1 .. $interval;
}

sub _from_iso_8601 {
  my ($self, $str) = @_;
  $str =~ s/(\d)\s(\d)/${1}T${2}/;
  $str =~ s/\.\d+$//;
  return Time::Piece->strptime($str, '%Y-%m-%dT%H:%M:%S');
}

sub _get_previous_event {
  my $self = shift;
  my $trc_file = File::Spec->catfile($self->_root, 'previous');

  warn "[APP_TT] _get_previous_event $trc_file\n" if DEBUG;

  return $trc_file, {} unless -r $trc_file;
  $trc_file = _slurp($trc_file);    # $ROOT/previous contains path to last .trc file
  $trc_file =~ s!\s*$!!;
  return $trc_file, {} unless -r $trc_file;
  return $trc_file, decode_json(_slurp($trc_file)); # slurp $ROOT/2015/08/20150827-085643_app_tt.trc
}

sub _group_by_day {
  my ($self, $res) = @_;
  my $pl = 0;
  my %log;

  for my $e (@{$res->{log}}) {
    my $k = $e->{start}->ymd;
    $log{$k} ||= {%$e, seconds => 0};
    $log{$k}{seconds} += $e->{seconds};
    $log{$k}{_project}{$e->{project}} = 1;
    $log{$k}{_tags}{$_} = 1 for @{$e->{tags}};
  }

  $res->{log} = [
    map {
      my $p = join ', ', keys %{$_->{_project}};
      $pl = length $p if $pl < length $p;
      +{%$_, project => $p, tags => [keys %{$_->{_tags}}]};
    } map { $log{$_} } sort keys %log
  ];

  $res->{max_project_chr} = $pl;
}

sub _hms_duration {
  my ($self, $duration, $sep) = @_;
  my $seconds = int(ref $duration ? $duration->seconds : $duration);
  my ($hours, $minutes);

  $hours = int($seconds / 3600);
  $seconds -= $hours * 3600;
  $minutes = int($seconds / 60);
  $seconds -= $minutes * 60;

  return sprintf '%s:%02s:%02s', $hours, $minutes, $seconds if !$sep;
  return sprintf '%2s:%02s', $hours, $minutes if $sep eq 'hm';
  return sprintf '%sh %sm %ss', $hours, $minutes, $seconds;
}

sub _log {
  my $self       = shift;
  my $tags       = join ',', @{$self->tag};
  my @project_re = map {qr{^$_\b}} split /,/, $self->project || '.+';

  my $res = {
    events          => 0,
    fill            => 0,
    interval        => 'month',
    log             => [],
    max_project_chr => 0,
    seconds         => 0,
    start_at        => 0,
  };

  for (@_) {
    /^(-\d+)(m|y|month|year)$/ and ($res->{start_at} = $1 and $res->{interval} = $2);
    /^(-\d+)$/      and $res->{start_at} ||= $1;
    /^(month|year)/ and $res->{interval} ||= $1;
    /^--fill/       and $res->{fill}     ||= 1;
  }

  if ($res->{interval} =~ m!^y!) {
    $res->{when} = $self->_tp(Y => $self->_now->year + $res->{start_at}, m => 1, d => 1);
    $res->{path} = File::Spec->catdir($self->_root, $res->{when}->year);
  }
  else {
    $res->{when} = $self->_tp(m => $self->_now->mon + $res->{start_at}, d => 1);
    $res->{path}
      = File::Spec->catdir($self->_root, $res->{when}->year, sprintf '%02s', $res->{when}->mon);
  }

  -d $res->{path} and find {
    no_chdir => 0,
    wanted   => sub {
      my ($date, $hms, $project) = /^(\d+)-(\d+)_(.*)\.trc$/ or return;
      my $event = decode_json(_slurp($_));
      $event->{tags} ||= [];
      return if @project_re and !grep { $event->{project} =~ $_ } @project_re;
      return if $tags       and !grep { $tags =~ /\b$_\b/ } @{$event->{tags}};
      return unless $event->{seconds};
      $event->{start} = $self->_from_iso_8601($event->{start});
      push @{$res->{log}},
        $self->_fill_log_days(@{$res->{log}} ? $res->{log}[-1]{start} : $res->{when},
        $event->{start})
        if $res->{fill};
      pop @{$res->{log}}
        if @{$res->{log}}
        and !$res->{log}[-1]{project}
        and $res->{log}[-1]{start}->mday == $event->{start}->mday;
      push @{$res->{log}}, $event;
      $res->{max_project_chr} = length $event->{project}
        if $res->{max_project_chr} < length $event->{project};
      $res->{events}++;
      $res->{seconds} += $event->{seconds};
    }
    },
    $res->{path};

  if (my $method = $self->can(sprintf '_group_by_%s', $self->group_by || 'nothing')) {
    $self->$method($res);
  }

  return $res;
}

sub _now { shift->{now} ||= localtime }

sub _root {
  shift->{root} ||= $ENV{TIMETRACKER_HOME} || do {
    my $home = File::HomeDir->my_home || File::Spec->curdir;
    File::Spec->catdir($home, '.TimeTracker');
  };
}

sub _set_now {
  my $self = shift;

  if (my ($hm) = grep {/^\d+:\d+$/} @_) {
    $hm =~ /^(\d+):(\d+)$/;
    $self->{custom_now} = $hm;
    $self->{now} = $self->_tp(H => $1, M => $2);
  }

  return $self;
}

sub _tzoffset { shift->_now->tzoffset }

# From Mojo::Util
sub _slurp {
  my $path = shift;
  die qq{Can't open file "$path": $!} unless open my $file, '<', $path;
  my $content = '';
  while ($file->sysread(my $buffer, 131072, 0)) { $content .= $buffer }
  return $content;
}

# From Mojo::Util
sub _spurt {
  my ($content, $path) = @_;
  die qq{Can't open file "$path": $!} unless open my $file, '>', $path;
  die qq{Can't write to file "$path": $!} unless defined $file->syswrite($content);
  return $content;
}

sub _stop_previous {
  my ($self,     $args)  = @_;
  my ($trc_file, $event) = $self->_get_previous_event;

  if (!$event->{start} or $event->{stop}) {
    return 0 if $args->{start};
    $self->_diag("No previous event to stop.");
    return 3;    # No such process
  }

  my $duration = $self->_now - $self->_from_iso_8601($event->{start}) + $self->_tzoffset;

  # Probably some invalid timestamp was given as input
  if ($duration->seconds < 0) {
    die "Cannot log event shorter than a second! Need to manually fix $trc_file";
  }

  $event->{duration} = $self->_hms_duration($duration);
  $event->{seconds}  = $duration->seconds;
  $event->{stop}     = $self->_now->datetime;

  if ($event->{seconds} < $ENV{TIMETRACKER_MIN_TIME}) {
    $self->_say('Dropping log event for "%s" since worked less than five minutes.',
      $event->{project});
    unlink $trc_file or die "rm $trc_file: $!";
    return 52;
  }
  else {
    $self->_add_event_info($event);
    _spurt(encode_json($event) => $trc_file);
    $self->_say('Stopped working on "%s" after %s',
      $event->{project}, $self->_hms_duration($duration, 'hms'));
    return 0;
  }
}

sub _tp {
  my ($self, %t) = @_;

  $t{Y} ||= $self->_now->year;
  $t{m} ||= $self->_now->mon;
  $t{d} ||= $self->_now->mday;
  $t{H} ||= 0;
  $t{M} ||= 0;
  $t{S} ||= 0;

  if ($t{m} < 0) {
    $t{m} = 12 - $t{m};
    $t{Y}--;
  }

  Time::Piece->strptime(sprintf('%s-%s-%sT%s:%s:%s+0000', @t{qw(Y m d H M S)}),
    '%Y-%m-%dT%H:%M:%S%z');
}

sub _trc_path {
  my ($self, $project, $t) = @_;
  my $month = sprintf '%02s', $t->mon;
  my $file;

  $project =~ s!\W!_!g;
  $file = sprintf '%s-%s_%s.trc', $t->ymd(''), $t->hms(''), $project;

  return File::Spec->catfile($self->_root, $t->year, $month, $file);
}

app {
  my $self = shift;
  my $action = sprintf 'cmd_%s', shift || 'status';

  if ($ENV{APP_TT_HELP}) {
    return $self->cmd_help($ENV{APP_TT_HELP});
  }
  if (!$self->description) {
    my ($description) = grep {/^\w\S*\s/} @_;
    $self->description($description) if $description;
  }

  return $self->cmd_help unless $self->can($action);
  return $self->$action(@_);
};
