#!/usr/bin/perl

use strict;
use warnings;
use lib "/opt/pos/lib";
use Template;
use File::Path qw( remove_tree make_path );
use File::Copy qw( move copy );
use File::Slurp qw( read_file write_file read_dir );
use Data::Dumper;
use JSON::XS;
use POSIX qw( strftime );
use App::POS::Utils qw( post_json post_json2 post log translate );
use App::POS::Check;
use App::POS::Config;
use App::POS::Machine::Server;
use Digest::MD5 qw( md5_hex );
use Storable;
use File::Find qw( find );
use Hash::Rename;
use Redis::Fast;

my $redis_host = 'redis.opalaconsult.com';
my $redis_port = 80;
my $redis_passwd = 'DkQJcHAwPGaY7Tpk04p27R2GHFAFSvO8jzVyXQ7/0z8RfQx8MNKDYgvvRVEd3/Leh5nHjvdTtvfYfXRS';
my $redis = undef;

die "Needs to be run by root...\n" unless $ENV{USER} eq 'root';

my $show_api_hardlock = shift;

my $sleep_loop = 5;

my $cfg = App::POS::Config->new( file => "/opt/pos/etc/config.ini" );
my $info = $cfg->load_fixed();

my %MAIN_PRINTERS = ();

my $SAFT_TaxRegistratioNumber = $info->{SAFT_TaxRegistratioNumber} || "";
my $SAFT_CompanyName = $info->{SAFT_CompanyName} || "";
my $SAFT_BusinessName = $info->{SAFT_BusinessName} || "";
my $SAFT_AddressDetail = $info->{SAFT_AddressDetail} || "";
my $SAFT_City = $info->{SAFT_City} || "";
my $SAFT_PostalCode = $info->{SAFT_PostalCode} || "";
my $DOC_TYPE_SERIE = $info->{DOC_TYPE_SERIE} || "";

my $machine = undef;

if (exists $info->{STATION_NUMBER} && $info->{STATION_NUMBER} == 0) {
  $machine = App::POS::Machine::Server->new( config => $cfg );
} else {
  die "Needs to be executed in the server machine...\n";
}

$info->{RFID_TOKEN} ||= "";
$info->{RFID_CLOUD_SYNC} ||= "";
$info->{POINTS_SYNC_TOKEN} ||= "";
$info->{POINTS_USAGE} ||= "";
$info->{VOUCHERS_SYNC_TOKEN} ||= "";
$info->{INTERNET_SMS_SYNC_TOKEN} ||= "";
$info->{QUIZ_TOKEN} ||= "";
$info->{QUIZ_QUESTIONS} ||= [];
$info->{QUIZ_DISCOUNT} ||= '';

my $db = $machine->database();
my $dbh = $db->dbh();

my %sign_keys = ();

my $hardlock = `/opt/pos/common/bin/machine_id`;
my $mac_address = `/opt/pos/common/bin/machine_id 1`;
my $file_control_sms = "/opt/pos/common/data/pos_sms.control";

my $api_hardlock = md5_hex($hardlock."#".$mac_address);

if (defined $show_api_hardlock) {
  #print $api_hardlock;
  print $hardlock;
  exit;
}

$dbh->do("LISTEN online_api_feeder");

my $first = 1;

if ($info->{INTERNET_SMS_SYNC_TOKEN}) {
  sync_sms_customers_up($info->{INTERNET_SMS_SYNC_TOKEN}, $hardlock, $machine, $file_control_sms);
  sync_sms_customers_down($info->{INTERNET_SMS_SYNC_TOKEN}, $hardlock, $machine);
}

my $count = 0;
_setup_redis($info, $hardlock, $machine);

while (1) {
  log("/tmp/online_api_feeder.log", "######################################################");
  log("/tmp/online_api_feeder.log", ">> Loop... (API KEY: $hardlock)");

  my $has_network = `/opt/pos/common/bin/has_network`;

  if ($has_network !~ /YES/) {
    log("/tmp/online_api_feeder.log", "No network...");
    sleep $sleep_loop;
    next;
  }

  if ($info->{POINTS_SYNC_TOKEN} && $info->{POINTS_USAGE}) {
    sync_points_run($machine, $hardlock);
  }

  my %tmp = ();

  while (my $notify = $dbh->pg_notifies) {
    my ($event, $pid, $payload) = @$notify;
    $tmp{$event."||".$payload}++;
    #$dbh->ping() or die qq{Ping failed!};
  }

  if (! $first && $info->{INTERNET_SMS_SYNC_TOKEN}) {
    # sends changes to server
    if (scalar keys %tmp) {
      sync_sms_customers_up($info->{INTERNET_SMS_SYNC_TOKEN}, $hardlock, $machine, $file_control_sms);
    }
  }

  if ($info->{VOUCHERS_SYNC_TOKEN}) {
    # sync vouchers
    sync_vouchers_up($machine, $hardlock);
  }

  if (scalar @{ $info->{QUIZ_QUESTIONS} } && $info->{QUIZ_DISCOUNT} && $info->{QUIZ_TOKEN}) {
    # sync vouchers
    sync_vouchers_up($machine, $hardlock, 'quizzes#' . $info->{QUIZ_TOKEN});
  }

  if ($info->{RFID_TOKEN} && $info->{RFID_CLOUD_SYNC}) {
    sync_rfid_transactions($machine, $hardlock, $info->{RFID_TOKEN});
  }

  $first = 0;

  # setup redis with subscribe
  if ($count == 12) {
    _setup_redis($info, $hardlock, $machine);
    $count = 0;
  }

  $count++;

  # check is redis sent something
  if (defined $redis) {
    $redis->wait_for_messages(1);
  }

  sleep $sleep_loop;
}

#
# Functions
#

sub sync_rfid_transactions {
  my( $machine, $hardlock, $token ) = @_;

  my $db = $machine->database();
  my $url = 'http://apisv2.net-bo.com/pos_rfid/transaction_v2';
  my $url_card_inits = 'http://apisv2.net-bo.com/pos_rfid/device_init_json';
  my $recs;

  # lets sync the card inits
  # we fetch records with more than 1 minute to give handlers the time to fill fo_doc_number
  $recs = $db->select(qq{
    SELECT *
    FROM rfid_operations
    WHERE synced = 0 AND operation_type = ? AND EXTRACT(EPOCH FROM (NOW() - datetime)) / 60 > 1
    ORDER BY id
    LIMIT 50
  }, 'card_init'); # this date is hardcoded because we've system with previous data that don't have the sync field

  if (scalar @$recs) {
    my $ret;
    eval { $ret = post_json($url_card_inits, { records => $recs, token => $token }) };

    if ($@ || ! defined $ret) {
      log("/tmp/online_api_feeder.log", "  >> sync_rfid_transactions error calling $url_card_inits - $@");
    }
    else {
      $ret->{success} //= 0;

      if ($ret->{success}) {

        log("/tmp/online_api_feeder.log", "  >> sync_rfid_transactions card_inits synced ".scalar(@$recs)." records...");

        my $ids_str = "";

        foreach my $r (@$recs) {
          $ids_str .= $r->{id} . ",";
        }

        chop($ids_str);
        $db->do("UPDATE rfid_operations SET synced = 1 WHERE id IN ($ids_str)");
      }
    }
  }

  # lets sync all recs wich arent card inits
  # we fetch records with more than 1 minute to give handlers the time to fill fo_doc_number
  $recs = $db->select(qq{
    SELECT *
    FROM rfid_operations
    WHERE synced = 0 AND operation_type <> ? AND EXTRACT(EPOCH FROM (NOW() - datetime)) / 60 > 1
    ORDER BY id
    LIMIT 50
  }, 'card_init'); # this date is hardcoded because we've system with previous data that don't have the sync field

  return unless scalar @$recs;

  my $ret;
  eval { $ret = post_json($url, { records => $recs, token => $token }) };

  if ($@ || ! defined $ret) {
    log("/tmp/online_api_feeder.log", "  >> sync_rfid_transactions error calling $url - $@");
  }
  else {
    $ret->{success} //= 0;

    if ($ret->{success}) {

      log("/tmp/online_api_feeder.log", "  >> sync_rfid_transactions transactions synced ".scalar(@$recs)." records...");

      my $ids_str = "";

      foreach my $r (@$recs) {
        $ids_str .= $r->{id} . ",";
      }

      chop($ids_str);
      $db->do("UPDATE rfid_operations SET synced = 1 WHERE id IN ($ids_str)");
    }
  }
}

sub sync_vouchers_up {
  my( $machine, $hardlock, $hardcoded_token ) = @_;

  log("/tmp/online_api_feeder.log", ">> sync_vouchers_up");

  my $db = $machine->database();

  my $cfg_o = $machine->config();
  my $cfg = $cfg_o->load_fixed();

  $cfg->{VOUCHERS_FOR_APP_ONLY} ||= 0;

  my $token = $cfg->{VOUCHERS_SYNC_TOKEN};
  $token = $hardcoded_token if defined $hardcoded_token;

  my $SAFT_BusinessName = $cfg->{SAFT_BusinessName} || "";
  my $SAFT_TaxRegistratioNumber = $cfg->{SAFT_TaxRegistratioNumber} || "";
  my $close_hour = $cfg->{END_BUSINESS_HOUR} || "05:00";
  my @tmp = split /:/, $close_hour;

  # vouchers to sync
  my $recs = $db->select(qq{
    SELECT v.*, c.app_sign_key, c.card_number
    FROM vouchers v
    LEFT JOIN customers c ON (v.customer_id = c.id)
    WHERE (v.synced = 0 AND v.date_used IS NULL) OR (v.date_used IS NOT NULL AND v.date_used_synced = 0) AND v.external_id <> ''
  });

  foreach my $rec (@$recs) {
    $rec->{external_id} ||= "";
    next unless $rec->{external_id};

    $rec->{app_sign_key} ||= "";
    $rec->{card_number} ||= "";

    # new voucher, need to create it
    if ($rec->{synced} == 0) {
      my $data = {
        store_name    => $SAFT_BusinessName,
        store_nif     => $SAFT_TaxRegistratioNumber,
        token         => $token,
        hardlock      => $hardlock,
        voucher       => $rec->{external_id},
        value         => $rec->{value},
        card_number   => $rec->{card_number},
        app_sign_key  => $rec->{app_sign_key},
      };

      my $ret = post(
        'https://apisv1.net-bo.com/shared_vouchers/create', $data
      );

      $ret ||= "";

      if (! $ret) {
        log("/tmp/online_api_feeder.log", "  >> ERROR - got empty answer from server...");
      }
      else {
        my $return = decode_json($ret);
        #print STDERR Dumper($return);

        if ($return->{_error} ne '') {
          log("/tmp/online_api_feeder.log", "  >> ERROR - ".$return->{_error});
        }
        else {
          $rec->{customer_id} ||= "";
          $return->{app_sign_key} ||= "";

          if ($cfg->{VOUCHERS_FOR_APP_ONLY}) {
            if ($rec->{customer_id} && $return->{app_sign_key}) {
              my $ctmp = $db->select("SELECT app_sign_key FROM customers WHERE id = ?", $rec->{customer_id});

              if (scalar @$ctmp && $return->{app_sign_key}) {
                $ctmp->[0]->{app_sign_key} ||= "";

                # app sign key was created or changed, print it
                if ($ctmp->[0]->{app_sign_key} ne $return->{app_sign_key}) {
                  _print_app_sign_key($machine, $return->{app_sign_key});
                  $db->do("UPDATE customers SET card_number = ?, app_sign_key = ? WHERE id = ?", $return->{app_sign_key}, $return->{app_sign_key}, $rec->{customer_id});
                }
              }
            }
          }

          $db->do("UPDATE vouchers SET synced = 1 WHERE id = ?", $rec->{id});
          log("/tmp/online_api_feeder.log", "  >> OK, voucher ".$rec->{external_id}." synced...");
        }
      }
    }
    # need to sync date used date only
    elsif ($rec->{synced} == 1 && ! defined $rec->{date_used_sync}) {
      log("/tmp/online_api_feeder.log", "SYNC date_used ONLY...");
      my $ret = post(
        'https://apisv1.net-bo.com/shared_vouchers/used', {
          token         => $token,
          voucher       => $rec->{external_id},
          date          => $rec->{date_used},
      });

      my $return = decode_json($ret);

      if ($return->{_error} ne '') {
        log("/tmp/online_api_feeder.log", "  >> ERROR - ".$return->{_error});
      }
      else {
        $db->do("UPDATE vouchers SET date_used_synced = 1 WHERE id = ?", $rec->{id});
        log("/tmp/online_api_feeder.log", "  >> OK, voucher ".$rec->{external_id}." date_used_synced...");
      }
    }
  }
}

sub sync_sms_customers_down {
  my( $token, $hardlock, $machine ) = @_;

  my $db = $machine->database();

  my $cfg_o = $machine->config();
  my $cfg = $cfg_o->load_fixed();

  log("/tmp/online_api_feeder.log", ">> sync_sms_customers_down");

  my $ret = post('http://apisv2.net-bo.com/pos_sms/get', {
    token => $info->{INTERNET_SMS_SYNC_TOKEN},
    hardlock => $hardlock,
    check_sync => 1,
  });

  if (defined $ret && $ret) {
    my $ids_tmp = $db->select(qq{
        SELECT id, email, contact, card_number
        FROM customers
        WHERE deleted = 0
    });

    my %ids_by_contact;
    my %ids_by_email;
    my %ids_by_card_number;

    for (@$ids_tmp) {
      $_->{card_number} //= "";
      $_->{email} //= "";
      $_->{contact} //= "";

      $ids_by_contact{$_->{contact}} = $_->{id} if $_->{contact};
      $ids_by_email{$_->{email}} = $_->{id} if $_->{email};
      $ids_by_card_number{$_->{card_number}} = $_->{id} if $_->{card_number};
    }

    my $customers = decode_json($ret);
    $customers ||= {};

    if (exists $customers->{records} && scalar @{ $customers->{records} }) {
      foreach my $c (@{ $customers->{records} }) {
        $c->{card_number} //= "";
        $c->{email} //= "";
        $c->{contact} //= "";

        my $cur_id = undef;
        my $did = 0;

        if (exists $ids_by_card_number{$c->{card_number}}) {
          $cur_id = $ids_by_card_number{$c->{card_number}};
          $did = 1;
        }
        elsif (exists $ids_by_email{$c->{email}}) {
          $cur_id = $ids_by_email{$c->{email}};
          $did = 1;
        }
        elsif (exists $ids_by_contact{$c->{contact}}) {
          $cur_id = $ids_by_contact{$c->{contact}};
          $did = 1;
        }

        # insert
        if (! defined $cur_id) {
          print STDERR "#### insert - $c->{name}\n";

          my $id = $db->next_id("customers");

          $db->do(qq{
            INSERT INTO customers (
              id, nif, city, contact, address, deleted, birthdate, points, email,
              name, postal_code, card_number
            ) VALUES (
              ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
            )
          }, $id, $c->{nif}, $c->{city}, $c->{contact}, $c->{address},
            $c->{deleted}, $c->{birthdate}, $c->{points}, $c->{email}, $c->{name},
            $c->{postal_code}, $c->{card_number});
        }
        # update
        else {
          print STDERR "#### update - $c->{name}\n";

          $db->do(qq{
            UPDATE customers SET nif = ?, city = ?, contact = ?, address = ?,
              deleted = ?, birthdate = ?, points = ?, email = ?, name = ?,
              postal_code = ?, card_number = ?
            WHERE id = ?
          }, $c->{nif}, $c->{city}, $c->{contact}, $c->{address}, $c->{deleted},
            $c->{birthdate}, $c->{points}, $c->{email}, $c->{name}, $c->{postal_code},
            $c->{card_number}, $cur_id);
        }
      }
    }
    else {
      log("/tmp/online_api_feeder.log", "  >> no records");
    }
  }
}

sub sync_sms_customers_up {
  my( $token, $hardlock, $machine, $file, $sync_all ) = @_;

  my $db = $machine->database();

  my $cfg_o = $machine->config();
  my $cfg = $cfg_o->load_fixed();

  log("/tmp/online_api_feeder.log", ">> sync_sms_customers_up");

  my $control = {};

  if (-e $file) {
    eval { $control = retrieve($file); };
  }

  my $recs = $db->select(qq{
    SELECT h.customer_id, c.contact, c.name, c.card_number, c.nif, c.address, c.postal_code, c.city,
      c.birthdate, c.points, c.email, c.deleted,
      SUM(h.value) AS total_spent, MIN(h.business_date) AS first_visit, MAX(h.business_date) AS last_visit
    FROM sales_headers h
    LEFT JOIN customers c ON (h.customer_id = c.id)
    WHERE h.customer_id != 0 AND h.doc_type IN (1,5,90,12,7) AND (contact <> '' OR email <> '' OR card_number <> '')
    GROUP BY h.customer_id, c.contact, c.name, c.card_number, c.nif, c.address, c.postal_code, c.city,
      c.birthdate, c.points, c.email, c.deleted
  });

  my %c_info = ();

  for (@$recs) {
    $c_info{$_->{customer_id}}->{last_visit} = $_->{last_visit};
    $c_info{$_->{customer_id}}->{first_visit} = $_->{first_visit};
    $c_info{$_->{customer_id}}->{total_spent} = $_->{total_spent};
  }

  my $recs2 = $db->select(qq{
    SELECT id AS customer_id, contact, name, card_number, nif, address, postal_code, city,
      birthdate, points, email, deleted
    FROM customers
    WHERE contact <> '' OR email <> '' OR card_number <> ''
  });

  my @to_sync = ();

  foreach my $r (@$recs2) {
    my %h_tmp = %$r;

    #delete $h_tmp{points};
    #delete $h_tmp{total_spent};
    #delete $h_tmp{first_visit};
    #delete $h_tmp{last_visit};

    my @keys = sort keys %h_tmp;
    my $key_str = "";

    for (@keys) {
      $key_str .= (defined $h_tmp{$_} ? $h_tmp{$_} : "") ."#";
    }

    my $md5 = md5_hex($key_str);
    $r->{md5} = $md5;

    if (defined $sync_all || (! exists $control->{$r->{customer_id}} || $control->{$r->{customer_id}} ne $md5)) {
      $r->{total_spent} = exists $c_info{$r->{customer_id}} ? $c_info{$r->{customer_id}}->{total_spent} : 0;
      $r->{first_visit} = exists $c_info{$r->{customer_id}} ? $c_info{$r->{customer_id}}->{first_visit} : undef;
      $r->{last_visit} = exists $c_info{$r->{customer_id}} ? $c_info{$r->{customer_id}}->{last_visit} : undef;
      $r->{biz_name} = $cfg->{SAFT_BusinessName} // "";

      push @to_sync, $r;
    }
  }

  if (! scalar @to_sync) {
    log("/tmp/online_api_feeder.log", "  >> no records to sync");
  }
  else {
    log("/tmp/online_api_feeder.log", "  >> will sync ".scalar(@to_sync)." records...");

    #my $ret = post_json('http://apisv1.net-bo.com/pos_sms/customers/set', {
    my $ret = post_json('http://apisv2.net-bo.com/pos_sms/set', {
      token => $info->{INTERNET_SMS_SYNC_TOKEN},
      hardlock => $hardlock,
      customers => \@to_sync,
    });

    if (defined $ret && $ret) {
      $ret->{_error} ||= "";

      if (! $ret->{_error}) {
        log("/tmp/online_api_feeder.log", "  >> sync success");

        for (@to_sync) {
          $control->{$_->{customer_id}} = $_->{md5};
        }

        store $control, $file
      }
      else {
        log("/tmp/online_api_feeder.log", "  >> sync error - ".$ret->{_error});
      }
    }
    else {
        log("/tmp/online_api_feeder.log", "  >> sync error");
    }
  }
}

sub sync_points_run {
  my( $machine, $hardlock ) = @_;

  log("/tmp/online_api_feeder.log", ">> sync_points_run");

  my $db = $machine->database();

  my $cfg_o = $machine->config();
  my $cfg = $cfg_o->load_fixed();

  my $SAFT_BusinessName = $cfg->{SAFT_BusinessName} || "";
  my $SAFT_TaxRegistratioNumber = $cfg->{SAFT_TaxRegistratioNumber} || "";

  my $value_per_point = $cfg->{POINTS_VALUE_PER_POINT};
  my $value_per_point_on_sale = $cfg->{POINTS_VALUE_PER_POINT_ON_SALE};
  my $allow_partial = $cfg->{POINTS_ALLOW_PARTIAL};
  my $minimum_to_use = $cfg->{POINTS_MINIMUM_TO_USE};

  find(sub {
    return unless -f $File::Find::name;

    my $cnt = read_file($File::Find::name);

    my @f = split /#/, $cnt;

    my(
      $hrd, $nif, $card, $name, $fo_doc_number, $business_date, $check_value,
      $birthdate, $address, $city, $postal_code, $contact, $email, $id, $points,
      $app_sign_key
    ) = @f;

    my $post_info = {
      token         => $cfg->{POINTS_SYNC_TOKEN},
      hardlock      => $hrd,
      nif           => $nif,
      name          => $name,
      card_number   => $card,
      app_sign_key  => $app_sign_key,
      points        => $points,
      fo_doc_number => $fo_doc_number,
      business_date => $business_date,
      birthdate     => $birthdate,
      address       => $address,
      city          => $city,
      postal_code   => $postal_code,
      contact       => $contact,
      email         => $email,
      check_value   => $check_value,
      store_name    => $SAFT_BusinessName,
      store_nif     => $SAFT_TaxRegistratioNumber,
      store_address     => $SAFT_AddressDetail,
      store_city        => $SAFT_City,
      store_postal_code => $SAFT_PostalCode,
      store_serie       => $DOC_TYPE_SERIE,
      points_value_per_point         => $value_per_point,
      points_value_per_point_on_sale => $value_per_point_on_sale,
      points_allow_partial           => $allow_partial,
      points_minimum_to_use          => $minimum_to_use,
    };

    # customer used points
    if ($_ =~ /^points_used/) {
      $post_info->{operation} = 'use';
    }
    # customer was assinged points
    else {
      $post_info->{operation} = 'assign';
    }

    log("/tmp/online_api_feeder.log", "  operation: $post_info->{operation}, points: $points");

    my $url = 'http://apisv1.net-bo.com/shared_points';

    my $r = post("$url/points_operations", $post_info);

    # success, delete file
    if (defined $r && $r eq 'OK') {
      #_print_app_sign_key($machine, $app_sign_key);

      log("/tmp/online_api_feeder.log", "  >> POST success! Will delete file $File::Find::name");
      unlink $File::Find::name;
    }
    else {
      log("/tmp/online_api_feeder.log", "  >> Got an error from server ($r)... won't delete file $File::Find::name");
    }
  }, ( "/opt/pos/server/data/cloud_points" ) );
}

sub sync_quiz_questions {
  my( $machine, $hardlock ) = @_;

  log("/tmp/online_api_feeder.log", ">> sync_quiz_questions");

  my $db = $machine->database();

  my $cfg_o = $machine->config();
  my $cfg = $cfg_o->load_fixed();

  my $token     = $cfg->{QUIZ_TOKEN} || "";
  my $questions = $cfg->{QUIZ_QUESTIONS} || [];

  my $post_info = {
    hardlock          => $hardlock,
    store_name        => $SAFT_BusinessName,
    store_address     => $SAFT_AddressDetail,
    store_city        => $SAFT_City,
    store_postal_code => $SAFT_PostalCode,
    store_nif         => $SAFT_TaxRegistratioNumber,
    store_serie       => $DOC_TYPE_SERIE,
    questions         => $questions,
    token             => $token,
  };

  my $url = 'http://apisv1.net-bo.com/quiz/hardlock_register';

  my $ret;
  eval { $ret = post_json2($url, $post_info) };

  if ($@) {
    log("/tmp/online_api_feeder.log", "  >> error calling $url");
  }
  else {
    log("/tmp/online_api_feeder.log", "  >> success sending questions to $url");
  }
}


sub _setup_redis {
  my( $info, $hardlock, $machine ) = @_;

  log("/tmp/online_api_feeder.log", ">> _setup_redis");

  # add more conditions to activate redis connection
  if ($info->{INTERNET_SMS_SYNC_TOKEN}) {
    $redis = Redis::Fast->new(
      server => $redis_host.":".$redis_port,
      debug => 0,
      password => $redis_passwd,
      name => 'my_connection_name',
    );
  }

  # subscribe for the sms sync token
  if ($info->{INTERNET_SMS_SYNC_TOKEN}) {
    $redis->subscribe(
      "pos_sms::harlock_notify",
      sub {
        sync_sms_customers_down($info->{INTERNET_SMS_SYNC_TOKEN}, $hardlock, $machine);
      },
    );
  }
}
