package FS::TaxEngine::avalara;

use strict;
use base 'FS::TaxEngine';
use FS::Conf;
use FS::Record qw(qsearch qsearchs dbh);
use FS::cust_pkg;
use FS::cust_location;
use FS::cust_bill_pkg;
use FS::tax_rate;
use JSON;
use Geo::StreetAddress::US;

our $DEBUG = 0;
our $json = JSON->new->pretty(1);

our $conf;

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

FS::UID->install_callback( sub {
    $conf = FS::Conf->new;
});

#sub cust_tax_locations {
#}
# Avalara address standardization would be nice but isn't necessary

# nothing to do here
sub add_sale {}

sub build_request {
  my ($self, %opt) = @_;

  my $oldAutoCommit = $FS::UID::AutoCommit;
  local $FS::UID::AutoCommit = 0;
  my $dbh = dbh;

  my $cust_bill = $self->{cust_bill};
  my $cust_main = $cust_bill->cust_main;

  # unfortunately we can't directly use the Business::Tax::Avalara get_tax()
  # interface, because we have multiple customer addresses
  my %address_seen;
 
  # assemble invoice line items 
  my @lines;
  # conventions we are using here:
  # P#### = part pkg#
  # F#### = part_fee#
  # L#### = cust_location# (address code)
  # L0 = company address
  foreach my $sale ( $cust_bill->cust_bill_pkg ) {
    my $part = $sale->part_X;
    my $item_code = ($part->isa('FS::part_pkg') ? 'P'.$part->pkgpart :
                                                  'F'.$part->feepart
                    );
    my $addr_code = 'L'.$sale->tax_locationnum;
    my $taxproductnum = $part->taxproductnum;
    next unless $taxproductnum;
    my $taxproduct = FS::part_pkg_taxproduct->by_key($taxproductnum);
    my $itemdesc = $part->itemdesc || $part->pkg;

    $address_seen{$sale->tax_locationnum} = 1;

    my $line = {
      'LineNo'            => $sale->billpkgnum,
      'DestinationCode'   => $addr_code,
      'OriginCode'        => 'L0',
      'ItemCode'          => $item_code,
      'TaxCode'           => $taxproduct->taxproduct,
      'Description'       => $itemdesc,
      'Qty'               => $sale->quantity,
      'Amount'            => ($sale->setup + $sale->recur),
      # also available:
      # 'ExemptionNo', 'Discounted', 'TaxIncluded', 'Ref1', 'Ref2', 'Ref3',
      # 'TaxOverride'
    };
    push @lines, $line;
  }
  # don't make the request unless there are some eligible line items
  return '' if !@lines;

  # assemble address records for any cust_locations we used here, plus
  # the company address
  # XXX these should just be separate config opts
  my $our_address = join(' ', 
    $conf->config('company_address', $cust_main->agentnum)
  );
  my $company_address = Geo::StreetAddress::US->parse_location($our_address);
  my $address1 = join(' ', grep $_, @{$company_address}{qw(
      number prefix street type suffix
  )});
  my $address2 = join(' ', grep $_, @{$company_address}{qw(
      sec_unit_type sec_unit_num
  )});
  my @addrs = (
    {
      'AddressCode'       => 'L0',
      'Line1'             => $address1,
      'Line2'             => $address2,
      'City'              => $company_address->{city},
      'Region'            => $company_address->{state},
      'Country'           => ($company_address->{country}
                              || $conf->config('countrydefault')
                              || 'US'),
      'PostalCode'        => $company_address->{zip},
      'Latitude'          => ($conf->config('company_latitude') || ''),
      'Longitude'         => ($conf->config('company_longitude') || ''),
    }
  );

  foreach my $locationnum (keys %address_seen) {
    my $cust_location = FS::cust_location->by_key($locationnum);
    my $addr = {
      'AddressCode'       => 'L'.$locationnum,
      'Line1'             => $cust_location->address1,
      'Line2'             => $cust_location->address2,
      'Line3'             => '',
      'City'              => $cust_location->city,
      'Region'            => $cust_location->state,
      'Country'           => $cust_location->country,
      'PostalCode'        => $cust_location->zip,
      'Latitude'          => $cust_location->latitude,
      'Longitude'         => $cust_location->longitude,
      #'TaxRegionId', probably not necessary
    };
    push @addrs, $addr;
  }

  my @avalara_conf = $conf->config('avalara-taxconfig');
  # 1. company code
  # 2. user name (account number)
  # 3. password (license)
  # 4. test mode (1 to enable)

  # create the top level object
  my $date = DateTime->from_epoch(epoch => $self->{invoice_time});
  my $doctype = $self->{estimate} ? 'SalesOrder' : 'SalesInvoice';
  return {
    'CustomerCode'      => $cust_main->custnum,
    'DocDate'           => $date->strftime('%Y-%m-%d'),
    'CompanyCode'       => $avalara_conf[0],
    'Client'            => "Freeside $FS::VERSION",
    'DocCode'           => $cust_bill->invnum,
    'DetailLevel'       => 'Tax',
    'Commit'            => 'false',
    'DocType'           => $doctype,
    'CustomerUsageType' => $cust_main->taxstatus,
    # ExemptionNo, Discount, TaxOverride, PurchaseOrderNo,
    'Addresses'         => \@addrs,
    'Lines'             => \@lines,
  };
}

sub calculate_taxes {
  $DB::single = 1; # XXX
  my $self = shift;

  my $cust_bill = shift;
  if (!$cust_bill->invnum) {
    warn "FS::TaxEngine::avalara: can't calculate taxes on a non-inserted invoice";
    return;
  }
  $self->{cust_bill} = $cust_bill;

  my $invnum = $cust_bill->invnum;
  if (FS::cust_bill_pkg->count("invnum = $invnum") == 0) {
    # don't even bother making the request
    return [];
  }

  # instantiate gateway
  eval "use Business::Tax::Avalara";
  die "error loading Business::Tax::Avalara:\n$@\n" if $@;

  my @avalara_conf = $conf->config('avalara-taxconfig');
  if (scalar @avalara_conf < 3) {
    die "Your Avalara configuration is incomplete.
The 'avalara-taxconfig' parameter must have three rows: company code, 
account number, and license key.
";
  }

  my $gateway = Business::Tax::Avalara->new(
    customer_code   => $self->{cust_main}->custnum,
    company_code    => $avalara_conf[0],
    user_name       => $avalara_conf[1],
    password        => $avalara_conf[2],
    is_development  => ($avalara_conf[3] ? 1 : 0),
  );

  # assemble the request hash
  my $request = $self->build_request;
  if (!$request) {
    warn "no tax-eligible items on this invoice\n" if $DEBUG;
    return [];
  }

  warn "sending Avalara tax request\n" if $DEBUG;
  my $request_json = $json->encode($request);
  warn $request_json if $DEBUG > 1;

  my $response_json = $gateway->_make_request_json($request_json);
  warn "received response\n" if $DEBUG;
  warn $response_json if $DEBUG > 1;
  my $response = $json->decode($response_json);
 
  my %tax_item_named;

  if ( $response->{ResultCode} ne 'Success' ) {
    return "invoice#".$cust_bill->invnum.": ".
           join("\n", @{ $response->{Messages} });
  }
  warn "creating taxes for inv#$invnum\n" if $DEBUG > 1;
  foreach my $TaxLine (@{ $response->{TaxLines} }) {
    my $taxable_billpkgnum = $TaxLine->{LineNo};
    warn "  item #$taxable_billpkgnum\n" if $DEBUG > 1;
    foreach my $TaxDetail (@{ $TaxLine->{TaxDetails} }) {
      # in this case the tax doesn't apply (just informational)
      next unless $TaxDetail->{Taxable};

      my $taxname = $TaxDetail->{TaxName};
      warn "    $taxname\n" if $DEBUG > 1;

      # create a tax line item
      my $tax_item = $tax_item_named{$taxname} ||= FS::cust_bill_pkg->new({
          invnum    => $cust_bill->invnum,
          pkgnum    => 0,
          setup     => 0,
          recur     => 0,
          itemdesc  => $taxname,
          cust_bill_pkg_tax_rate_location => [],
      });
      # create a tax_rate record if there isn't one yet.
      # we're not actually going to do anything with it, just tie related
      # taxes together.
      my $tax_rate = FS::tax_rate->new({
          data_vendor => 'avalara',
          taxname     => $taxname,
          taxclassnum => '',
          geocode     => $TaxDetail->{JurisCode},
          location    => $TaxDetail->{JurisName},
          tax         => 0,
          fee         => 0,
      });
      my $error = $tax_rate->find_or_insert;
      return "error inserting tax_rate record for '$taxname': $error\n"
        if $error;
      $tax_rate = $tax_rate->replace_old; # get its taxnum if there wasn't one

      # create a tax_rate_location record
      my $tax_rate_location = FS::tax_rate_location->new({
          data_vendor => 'avalara',
          geocode     => $TaxDetail->{JurisCode},
          state       => $TaxDetail->{Region},
          city        => ($TaxDetail->{JurisType} eq 'City' ?
                          $TaxDetail->{JurisName} : ''),
          county      => ($TaxDetail->{JurisType} eq 'County' ?
                          $TaxDetail->{JurisName} : ''),
                        # country?
      });
      $error = $tax_rate_location->find_or_insert;
      return "error inserting tax_rate_location record for ".
              $TaxDetail->{JurisCode} .": $error\n"
        if $error;

      # create a link record
      my $tax_link = FS::cust_bill_pkg_tax_rate_location->new({
          tax_cust_bill_pkg   => $tax_item,
          taxtype             => 'FS::tax_rate',
          taxnum              => $tax_rate->taxnum,
          taxratelocationnum  => $tax_rate_location->taxratelocationnum,
          amount              => $TaxDetail->{Tax},
          taxable_billpkgnum  => $taxable_billpkgnum,
      });

      # append the tax link and increment the amount
      push @{ $tax_item->get('cust_bill_pkg_tax_rate_location') }, $tax_link;
      $tax_item->set('setup', $tax_item->get('setup') + $TaxDetail->{Tax});
    } # foreach $TaxDetail
  } # foreach $TaxLine

  return [ values(%tax_item_named) ];
}

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

1;
