package FS::part_pkg::prorate_calendar;

use strict;
use vars qw(@ISA %info);
use DateTime;
use Tie::IxHash;
use base 'FS::part_pkg::flat';

# weird stuff in here

%info = (
  'name' => 'Prorate to specific calendar day(s), then flat-rate',
  'shortname' => 'Prorate (calendar cycle)',
  'inherit_fields' => [ 'flat', 'usage_Mixin', 'global_Mixin' ],
  'fields' => {
    'recur_temporality' => {'disabled' => 1},
    'sync_bill_date' => {'disabled' => 1},# god help us all

    'cutoff_day' => { 'name' => 'Billing day (1 - end of cycle)',
                      'default' => 1,
                    },

    # add_full_period is not allowed

    # prorate_round_day is always on
    'prorate_round_day' => { 'disabled' => 1 },
 
    'prorate_defer_bill'=> {
                        'name' => 'Defer the first bill until the billing day',
                        'type' => 'checkbox',
                        },
    'prorate_verbose' => {
                        'name' => 'Show prorate details on the invoice',
                        'type' => 'checkbox',
                        },
  },
  'fieldorder' => [ 'cutoff_day', 'prorate_defer_bill', 'prorate_round_day', 'prorate_verbose' ],
  'freq' => 'm',
  'weight' => 23,
);

my %freq_max_days = ( # the length of the shortest period of each cycle type
  '1'   => 28,
  '2'   => 59,   # Jan - Feb
  '3'   => 90,   # Jan - Mar
  '4'   => 120,  # Jan - Apr
  '6'   => 181,  # Jan - Jun
  '12'  => 365,
);

my %freq_cutoff_days = (
  '1'   => [ 31, 28, 31, 30, 31, 30,
             31, 31, 30, 31, 30, 31 ],
  '2'   => [ 59, 61, 61, 62, 61, 61 ],
  '3'   => [ 90, 91, 92, 92 ],
  '4'   => [ 120, 123, 122 ],
  '6'   => [ 181, 184 ],
  '12'  => [ 365 ],
);

sub check {
  # yes, this package plan is such a special snowflake it needs its own
  # check method.
  my $self = shift;

  if ( !exists($freq_max_days{$self->freq}) ) {
    return 'Prorate (calendar cycle) billing interval must be an integer factor of one year';
  }
  $self->SUPER::check;
}

sub cutoff_day {
  my( $self, $cust_pkg ) = @_;
  my @periods = @{ $freq_cutoff_days{$self->freq} };
  my $prorate_day = $cust_pkg->cust_main->prorate_day
                    || $self->option('cutoff_day')
                    || 1;

  my @cutoffs = ($prorate_day);
  pop @periods; # we don't care about the last one
  foreach (@periods) {
    push @cutoffs, $cutoffs[-1] + $_;
  }
  @cutoffs;
}

sub calc_prorate {
  # it's not the same algorithm
  my ($self, $cust_pkg, $sdate, $details, $param, @cutoff_days) = @_;
  die "no cutoff_day" unless @cutoff_days;
  die "prepaid terms not supported with calendar prorate packages"
    if $param->{freq_override}; # XXX if we ever use this again

  #XXX should we still be doing this with multi-currency support?
  my $money_char = FS::Conf->new->config('money_char') || '$';

  my $charge = $self->base_recur($cust_pkg, $sdate) || 0;
  my $now = DateTime->from_epoch(epoch => $$sdate, time_zone => 'local');

  my $add_period = 0;
  # if this is the first bill but the bill date has been set
  # (by prorate_defer_bill), calculate from the setup date,
  # append the setup fee to @$details, and make sure to bill for 
  # a full period after the bill date.

  if ( $self->option('prorate_defer_bill', 1)
    and !$cust_pkg->getfield('last_bill')
    and $cust_pkg->setup )
  {
    $param->{'setup_fee'} = $self->calc_setup($cust_pkg, $$sdate, $details);
    $now = DateTime->from_epoch(epoch => $cust_pkg->setup, time_zone => 'local');
    $add_period = 1;
  }

  # DON'T sync to the existing billing day; cutoff days work differently here.

  $now->truncate(to => 'day');
  my ($end, $start) = $self->calendar_endpoints($now, @cutoff_days);

  #warn "[prorate_calendar] now = ".$now->ymd.", start = ".$start->ymd.", end = ".$end->ymd."\n";

  my $periods = $end->delta_days($now)->delta_days /
                $end->delta_days($start)->delta_days;
  if ( $periods < 1 and $add_period ) {
    $periods++; # charge for the extra time
    $start->add(months => $self->freq); # and push the next bill date forward
  }
  if ( $self->option('prorate_verbose',1) and $periods > 0 ) {
    if ( $periods < 1 ) {
      push @$details,
        'Prorated (' . $now->strftime('%b %d') .
        ' - ' . $end->strftime('%b %d') . '): ' . $money_char .
        sprintf('%.2f', $charge * $periods + 0.00000001);
    } elsif ( $periods > 1 ) {
      push @$details,
        'Prorated (' . $now->strftime('%b %d') .
        ' - ' . $end->strftime('%b %d') . '): ' . $money_char .
        sprintf('%.2f', $charge * ($periods - 1) + 0.00000001),

        'First full period: ' . $money_char . sprintf('%.2f', $charge);
    } # else exactly one period
  }

  $$sdate = $start->epoch;
  return sprintf('%.2f', $charge * $periods + 0.00000001);
}

sub prorate_setup {
  my $self = shift;
  my ($cust_pkg, $sdate) = @_;
  my @cutoff_days = $self->cutoff_day;
  if ( ! $cust_pkg->bill
     and $self->option('prorate_defer_bill')
     and @cutoff_days )
  {
    my $now = DateTime->from_epoch(epoch => $sdate, time_zone => 'local');
    $now->truncate(to => 'day');
    my ($end, $start) = $self->calendar_endpoints($now, @cutoff_days);
    if ( $now->compare($start) == 0 ) {
      $cust_pkg->setup($start->epoch);
      $cust_pkg->bill($start->epoch);
    } else {
      $cust_pkg->bill($end->epoch);
    }
    return 1;
  } else {
    return 0;
  }
}

=item calendar_endpoints NOW CUTOFF_DAYS

Given a current date (DateTime object) and a list of cutoff day-of-year
numbers, finds the next upcoming cutoff day (in either the current or the 
upcoming year) and the cutoff day before that, and returns them both.

=cut

sub calendar_endpoints {
  my $self = shift;
  my $now = shift;
  my @cutoff_day = sort {$a <=> $b} @_;

  my $year = $now->year;
  my $day = $now->day_of_year;
  # Feb 29 = 60 
  # For cutoff day purposes, it's the same day as Feb 28
  $day-- if $now->is_leap_year and $day >= 60;

  # select the first cutoff day that's after the current day
  my $i = 0;
  while ( $cutoff_day[$i] and $cutoff_day[$i] <= $day ) {
    $i++;
  }
  # $cutoff_day[$i] is now later in the calendar than today
  # or today is between the last cutoff day and the end of the year

  my ($start, $end);
  if ( $i == 0 ) {
    # then today is on or before the first cutoff day
    $start = DateTime->from_day_of_year(year => $year - 1,
                                        day_of_year => $cutoff_day[-1],
                                        time_zone => 'local');
    $end =   DateTime->from_day_of_year(year => $year,
                                        day_of_year => $cutoff_day[0],
                                        time_zone => 'local');
  } elsif ( $i > 0 and $i < scalar(@cutoff_day) ) {
    # today is between two cutoff days
    $start = DateTime->from_day_of_year(year => $year,
                                        day_of_year => $cutoff_day[$i - 1],
                                        time_zone => 'local');
    $end =   DateTime->from_day_of_year(year => $year,
                                        day_of_year => $cutoff_day[$i],
                                        time_zone => 'local');
  } else {
    # today is after the last cutoff day
    $start = DateTime->from_day_of_year(year => $year,
                                        day_of_year => $cutoff_day[-1],
                                        time_zone => 'local');
    $end =   DateTime->from_day_of_year(year => $year + 1,
                                        day_of_year => $cutoff_day[0],
                                        time_zone => 'local');
  }
  return ($end, $start);
}

1;
