package FS::TaxEngine::compliance_solutions;

#some false laziness w/ suretax... uses/based on cch data?  or just imitating
# parts of their interface?

use strict;
use base qw( FS::TaxEngine );
use FS::Conf;
use FS::Record qw( dbh ); #qw( qsearch qsearchs dbh);
use Data::Dumper;
use Date::Format;
use Cpanel::JSON::XS;
use SOAP::Lite;

our $DEBUG = 1; # prints progress messages
   $DEBUG = 2; # prints decoded request and response (noisy, be careful)
#   $DEBUG = 3; # prints raw response from the API, ridiculously unreadable

our $json = Cpanel::JSON::XS->new->pretty(1);

our %taxproduct_cache;

our $conf;

FS::UID->install_callback( sub {
    $conf = FS::Conf->new;
    # should we enable conf caching here?
});

our %REGCODE = ( # can be selected per agent
#  ''          => '99',
  'ILEC'      => '00',
  'IXC'       => '01',
  'CLEC'      => '02',
  'VOIP'      => '03',
  'ISP'       => '04',
  'Wireless'  => '05',
);

sub info {
  { batch    => 0,
    override => 0, #?
  }
}

sub add_sale { } # nothing to do here

sub build_input {
  my( $self, $cust_bill ) = @_;

  my $cust_main = $cust_bill->cust_main;

  %taxproduct_cache = ();

  # assemble invoice line items 
  my @lines = map { $self->build_input_item($_, $cust_bill, $cust_main) }
                  $cust_bill->cust_bill_pkg;

  return if !@lines;

  return \@lines;

}

sub build_input_item {
  my( $self, $cust_bill_pkg, $cust_bill, $cust_main ) = @_;

  # get the part_pkg/fee for this line item, and the relevant part of the
  # taxproduct cache
  my $part_item = $cust_bill_pkg->part_X;
  my $taxproduct_of_class = do {
    my $part_id = $part_item->table . '#' . $part_item->get($part_item->primary_key);
    $taxproduct_cache{$part_id} ||= {};
  };

  my @items = ();

  my $recur_without_usage = $cust_bill_pkg->recur;

  ###
  # Usage charges
  ###

  # cursor all this stuff; data sets can be LARGE
  # (if it gets really out of hand, we can also incrementally write JSON
  # to a file)

  my $details = FS::Cursor->new('cust_bill_pkg_detail', {
      billpkgnum  => $cust_bill_pkg->billpkgnum,
      amount      => { op => '>', value => 0 }
  }, dbh() );
  while ( my $cust_bill_pkg_detail = $details->fetch ) {

    # look up the tax product for this class
    my $classnum = $cust_bill_pkg_detail->classnum;
    my $taxproduct = $taxproduct_of_class->{ $classnum } ||= do {
      my $part_pkg_taxproduct = $part_item->taxproduct($classnum);
      $part_pkg_taxproduct ? $part_pkg_taxproduct->taxproduct : '';
    };
    die "no taxproduct configured for pkgpart ".$part_item->pkgpart.
        ", usage class $classnum\n"
        if !$taxproduct;

    my $cdrs = FS::Cursor->new('cdr', {
        detailnum       => $cust_bill_pkg_detail->detailnum,
        freesidestatus  => 'done',
    }, dbh() );
    while ( my $cdr = $cdrs->fetch ) {
      push @items, {
        $self->generic_item($cust_bill, $cust_main),
        record_type   => 'C',
        unique_id     => 'cdr ' . $cdr->acctid.
                         ' cust_bill_pkg '.$cust_bill_pkg->billpkgnum, 
        productcode   => substr($taxproduct,0,4),
        servicecode   => substr($taxproduct,4,3),
        orig_Num      => $cdr->src,
        term_Num      => $cdr->dst,
        bill_Num      => $cdr->charged_party,
        charge_amount => $cdr->rated_price, # 4 decimal places
        minutes       => sprintf('%.1f', $cdr->billsec / 60 ),
      };

    } # while ($cdrs->fetch)

    # decrement the recurring charge
    $recur_without_usage -= $cust_bill_pkg_detail->amount;

  } # while ($details->fetch)

  ###
  # Recurring charge
  ###

  if ( $recur_without_usage > 0 ) {
    my $taxproduct = $taxproduct_of_class->{ 'recur' } ||= do {
      my $part_pkg_taxproduct = $part_item->taxproduct('recur');
      $part_pkg_taxproduct ? $part_pkg_taxproduct->taxproduct : '';
    };
    die "no taxproduct configured for pkgpart ".$part_item->pkgpart.
        " recurring charge\n"
        if !$taxproduct;

    # when billing on cancellation there are no units
    my $units = $self->{cancel} ? 0 : $cust_bill_pkg->units;
    unshift @items, {
      $self->generic_item($cust_bill, $cust_main),
      record_type     => 'S',
      unique_id       => 'cust_bill_pkg '. $cust_bill_pkg->billpkgnum. ' recur',
      charge_amount   => $recur_without_usage,
      location_a      => $cust_bill_pkg->tax_location->zip,
      productcode     => substr($taxproduct,0,4),
      servicecode     => substr($taxproduct,4,3),
      units           => $units,
    };
  }

  ###
  # Setup charge
  ###

  if ( $cust_bill_pkg->setup > 0 ) {
    my $taxproduct = $taxproduct_of_class->{ 'setup' } ||= do {
      my $part_pkg_taxproduct = $part_item->taxproduct('setup');
      $part_pkg_taxproduct ? $part_pkg_taxproduct->taxproduct : '';
    };
    die "no taxproduct configured for pkgpart ".$part_item->pkgpart.
        " setup charge\n"
        if !$taxproduct;

    unshift @items, {
      $self->generic_item($cust_bill, $cust_main),
      record_type     => 'S',
      unique_id       => 'cust_bill_pkg '. $cust_bill_pkg->billpkgnum. ' setup',
      charge_amount   => $cust_bill_pkg->setup,
      location_a      => $cust_bill_pkg->tax_location->zip,
      productcode     => substr($taxproduct,0,4),
      servicecode     => substr($taxproduct,4,3),
      units           => $cust_bill_pkg->units,
    };
  }

  return @items;

}

sub generic_item {
  my( $self, $cust_bill, $cust_main ) = @_;

  warn 'regcode '. $self->{regcode} if $DEBUG;

  (
    account_number            => $cust_bill->custnum,
    customer_type             => ( $cust_main->company =~ /\S/ ? '01' : '00' ),
    invoice_date              => time2str('%Y%m%d', $cust_bill->_date),
    invoice_number            => $cust_bill->invnum,
    provider                  => $self->{regcode},
    safe_harbor_override_flag => 'N',
    exempt_code               => $cust_main->tax,
  );

}

sub make_taxlines {
  my( $self, $cust_bill ) = @_;

  die "compliance_solutions-regulatory_code setting is not configured\n"
    unless $conf->config('compliance_solutions-regulatory_code', $cust_bill->cust_main->agentnum);

  $self->{regcode} = $REGCODE{ $conf->config('compliance_solutions-regulatory_code', $cust_bill->cust_main->agentnum) };

  warn 'regcode '. $self->{regcode} if $DEBUG;

  # assemble the request hash
  my $input = $self->build_input($cust_bill);
  if (!$input) {
    warn "no taxable items in invoice; skipping Compliance Solutions request\n" if $DEBUG;
    return;
  }

  warn "sending Compliance Solutions request\n" if $DEBUG;
  my $request_json = $json->encode(
    {
      'access_code' => $conf->config('compliance_solutions-access_code'),
      'reference'   => 'Invoice #'. $cust_bill->invnum,
      'input'       => $input,
    }
  );
  warn $request_json if $DEBUG > 1;

  my $soap = SOAP::Lite->service("http://tcms1.csilongwood.com/cgi-bin/taxcalc.wsdl");

  $soap->soapversion('1.2'); #service appears to be flaky with the default 1.1

  my $results = $soap->tax_rate($request_json);

  my %json_result = %{ $json->decode( $results ) };
  warn Dumper(%json_result) if $DEBUG > 1;

  # handle $results is empty / API/connection failure?

  # status OK
  unless ( $json_result{status} =~ /^\s*OK\s*$/i ) {
    warn Dumper($json_result{error_codes}) unless $DEBUG > 1;
    die 'Compliance Solutions returned status '. $json_result{status}.
           "; see log for error_codes detail\n";
  }

  # transmission_error No errors.
  unless ( $json_result{transmission_error} =~ /^\s*No\s+errors\.\s*$/i ) {
    warn Dumper($json_result{error_codes}) unless $DEBUG > 1;
    die 'Compliance Solutions returned transmission_error '. $json_result{transmission_error}.
           "; see log for error_codes detail\n";
  }


  # error_codes / No errors (for all records... check them individually in loop?

  my @elements = ();

  #handle the response
  foreach my $tax_data ( @{ $json_result{tax_data} } ) {

    # create a tax rate location if there isn't one yet
    my $taxname = $tax_data->{descript};
    my $tax_rate = FS::tax_rate->new({
        data_vendor   => 'compliance_solutions',
        taxname       => $taxname,
        taxclassnum   => '',
        taxauth       => $tax_data->{'taxauthtype'}, # federal / state / city / district
        geocode       => $tax_data->{'geocode'},
        tax           => 0, #not necessary because we query for rates on the
        fee           => 0, # fly and only store this for the name -> code map??
    });
    my $error = $tax_rate->find_or_insert;
    die "error inserting tax_rate record for '$taxname': $error\n"
      if $error;
    $tax_rate = $tax_rate->replace_old;

    my $tax_rate_location = FS::tax_rate_location->new({
        data_vendor => 'compliance_solutions',
        geocode     => $tax_data->{'geocode'},
        district    => $tax_data->{'geo_district'},
        state       => $tax_data->{'geo_state'},
        county      => $tax_data->{'geo_county'},
        country     => 'US',
    });
    $error = $tax_rate_location->find_or_insert;
    die 'error inserting tax_rate_location record for '.  $tax_data->{state}.
        '/'. $tax_data->{country}. ' ('. $tax_data->{'geocode'}. "): $error\n"
      if $error;
    $tax_rate_location = $tax_rate_location->replace_old;

    #unique id: a cust_bill_pkg (setup/recur) or cdr record

    my $taxable_billpkgnum = '';
    if ( $tax_data->{'unique_id'} =~ /^cust_bill_pkg (\d+)/ ) {
      $taxable_billpkgnum = $1;
    } elsif ( $tax_data->{'unique_id'} =~ /^cdr (\d+) cust_bill_pkg (\d+)$/ ) {
      $taxable_billpkgnum = $2;
    } else {
      die 'unparseable unique_id '. $tax_data->{'unique_id'};
    }

    push @elements, FS::cust_bill_pkg_tax_rate_location->new({
      taxable_billpkgnum  => $taxable_billpkgnum,
      taxnum              => $tax_rate->taxnum,
      taxtype             => 'FS::tax_rate',
      taxratelocationnum  => $tax_rate_location->taxratelocationnum,
      amount              => sprintf('%.2f', $tax_data->{taxamount}),
    });

  }

  return @elements;
}

sub add_taxproduct {
  my $class = shift;
  my $desc = shift; # tax code and description, separated by a space.
  if ($desc =~ s/^(\w{7}+) //) {
    my $part_pkg_taxproduct = FS::part_pkg_taxproduct->new({
        'data_vendor' => 'compliance_solutions',
        'taxproduct'  => $1,
        'description' => $desc,
    });
    # $obj_or_error
    return $part_pkg_taxproduct->insert || $part_pkg_taxproduct;
  } else {
    return "illegal compliance solutions tax code '$desc'";
  }
}

1;
