#!/usr/bin/perl

use strict;
use warnings;
use lib "/opt/pos/lib";
use Data::Dumper;
use Redis::Fast;
#use Redis;
use File::Copy qw( copy move );
use File::Slurp qw( read_file write_file read_dir );
use Mojo::JSON qw( encode_json decode_json );
use File::Find qw( find );
use POSIX qw( strftime );
use Digest::MD5 qw( md5_hex );
use App::POS::Utils qw(
  post post_json log date_based_on_closing_hour my_conv between_dates
  area_nextid fix_encoding
);
use App::POS::Check;
use App::POS::Machine::Station;
use App::POS::Machine::Server;

my $SYNC_PRODUCTS_ONLY = shift;

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

$info->{ONLINE_ORDERS_TOKEN} //= "";
$info->{ONLINE_ORDERS_API_PASSWD} //= "";
$info->{ONLINE_ORDERS_AREA_NUM} //= 0;
$info->{ONLINE_ORDERS_AREA_NUM_DELIVERY} //= 0;
$info->{ONLINE_ORDERS_CLERK_ID} //= 1;
$info->{ONLINE_ORDERS_PRODUCTS_NOT_HOLDED} //= 1;
$info->{ONLINE_ORDERS_MINUTES_TO_HOLD} //= 0;
$info->{ONLINE_ORDERS_PRODUCTS_TO_CLOUD} //= 0;
$info->{ONLINE_ORDERS_ALERTS_IP} //= '127.0.0.1';
$info->{ONLINE_ORDERS_USE_PRODUCT_ID} //= 0;

die "Missing ONLINE_ORDERS_TOKEN or ONLINE_ORDERS_API_PASSWD in config.ini...\n"
  unless $info->{ONLINE_ORDERS_TOKEN} && $info->{ONLINE_ORDERS_API_PASSWD};

# deal with values that have @ on it
if (ref($info->{ONLINE_ORDERS_TOKEN}) eq 'ARRAY') {
  $info->{ONLINE_ORDERS_TOKEN} = join('@', @{$info->{ONLINE_ORDERS_TOKEN}});
}

# deal with values that have @ on it
if (ref($info->{ONLINE_ORDERS_API_PASSWD}) eq 'ARRAY') {
  $info->{ONLINE_ORDERS_API_PASSWD} = join('@', @{$info->{ONLINE_ORDERS_API_PASSWD}});
}

# auth related
my $TOKEN                   = $info->{ONLINE_ORDERS_TOKEN};
my $API_PASSWD              = $info->{ONLINE_ORDERS_API_PASSWD};
# area and clerk for new orders - store pickup
my $AREA_FOR_ONLINE_ORDERS  = $info->{ONLINE_ORDERS_AREA_NUM};
# area and clerk for new orders - delivery
my $AREA_FOR_ONLINE_ORDERS_DELIVERY  = $info->{ONLINE_ORDERS_AREA_NUM_DELIVERY};
# clerk to be used in orders
my $CLERK_FOR_ONLINE_ORDERS = $info->{ONLINE_ORDERS_CLERK_ID};
# when new orders come, products will not holded
my $PRODUCTS_ARE_NOT_HOLDED = $info->{ONLINE_ORDERS_PRODUCTS_NOT_HOLDED};
# automatically hold before pickup_hour
my $MINUTES_TO_HOLD         = $info->{ONLINE_ORDERS_MINUTES_TO_HOLD};
# to be used in loyalty app or website
my $SEND_PRODUCTS_TO_CLOUD  = $info->{ONLINE_ORDERS_PRODUCTS_TO_CLOUD};
# use id product field to identify products
my $ONLINE_ORDERS_USE_PRODUCT_ID = $info->{ONLINE_ORDERS_USE_PRODUCT_ID};

my $hardlock = `/opt/pos/common/bin/machine_id`;

my $machine = undef;

if (exists $info->{STATION_NUMBER} && $info->{STATION_NUMBER} > 0) {
  $machine = App::POS::Machine::Station->new( config => $cfg );
}
else {
  $machine = App::POS::Machine::Server->new( config => $cfg );
}

# on start, send products to cloud
if ($SEND_PRODUCTS_TO_CLOUD) {
  sync_products($machine, $TOKEN, $API_PASSWD, $SYNC_PRODUCTS_ONLY, $hardlock);
}

# on start, fetch pending orders
sync_virtual_orders($machine, $TOKEN, $API_PASSWD, $hardlock);

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

my $redis = undef;

setup_redis();

my $sleep = 5;
my $count = 0;

while (1) {
  print STDERR "Loop... $count\n";
  $count++;

  # every minute, reconnect to redis
  if ($count == 12) {
    setup_redis();
    $count = 0;
  }

  # check if holds needs to happen based on pickup date/time
  if ($MINUTES_TO_HOLD > 0) {
    fire_holds_ontime($machine);
  }

  my $num = $redis->wait_for_messages($sleep);
  #print STDERR "### messages_processed: $num\n";
}

#
# Functions
#

sub sync_products {
  my( $machine, $token, $api_passwd, $sync_products_only ) = @_;

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

  log("/tmp/sync_online_orders.log", ">> sync_products...");

  my $recs = $db->select(qq{
    SELECT p.id, p.name, p.sell_price1, p.sell_price5, p.is_extra, p.ask_price, p.is_weight,
        p.priority, f.name AS pos_group, sf.name AS pos_dept 
    FROM products p
    LEFT JOIN (
      SELECT product_id, MAX(group_id) as group_id
      FROM product_groups
      GROUP BY product_id
    ) pg ON (p.id = pg.product_id)
    LEFT JOIN groups sf ON (pg.group_id = sf.id)
    LEFT JOIN groups f ON (sf.parent = f.id)
    WHERE p.deleted = 0 AND p.online = 1 AND sf.deleted = 0 AND f.deleted = 0
  });

  if (! scalar @$recs) {
    log("/tmp/sync_online_orders.log", "  >> no products to send to the cloud (online flag)...");
    return;
  }

  my $ret = post_json(
    'http://apisv2.net-bo.com/pos_orders/products', {
      token => $token,
      api_passwd => $api_passwd,
      hardlock => $hardlock,
      products => $recs,
    },
  );

  $ret->{success} //= 0;

  if (! $ret->{success}) {
    log("/tmp/sync_online_orders.log", ">> error syncing products...".Dumper($ret));
  }

  if (defined $sync_products_only) {
    exit;
  }
}

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

  print STDERR "#### sync_virtual_orders / $token / $api_passwd\n";

  my $ret = post(
    'http://apisv2.net-bo.com/pos_orders/pending',
    { token => $token, api_passwd => $api_passwd, hardlock => $hardlock },
  );

  my $orders = [];

  eval { $orders = decode_json($ret) };
  return unless ref($orders) eq 'ARRAY';

  print STDERR Dumper($orders);

  my @did = ();

  foreach my $o (@$orders) {
    push @did, $o->{uuid};
    sync_virtual_order($machine, $TOKEN, $API_PASSWD, $o->{json}, 0);
  }

  if (scalar @did) {
    _send_msg_to_ui($info->{ONLINE_ORDERS_ALERTS_IP}, "ENCOMENDAS ONLINE<BR>CHEGOU UMA ENCOMENDA");

    # mark orders as synced
    my $ret = post_json(
      'http://apisv2.net-bo.com/pos_orders/synced', {
        token => $token, api_passwd => $api_passwd,
        orders => \@did, hardlock => $hardlock,
      },
    );
  }
}

sub sync_virtual_order {
  my $machine = shift;
  my $token = shift;
  my $api_passwd = shift;
  my $d = shift;
  my $ui_alert = shift // 1;

  log("/tmp/sync_online_orders.log", ">> New order...");
  
  my $r = $d;

  # we can receive a json str or an hashref
  if (! ref($d)) {
    $r = decode_json($d);
  }

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

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

  $r->{id} //= "";
  $r->{datetime} //= "";
  $r->{obs} //= "";
  $r->{total} //= 0;
  $r->{payed} //= 0;
  $r->{payment_method} //= "";
  $r->{customer_email} //= "";
  $r->{products} //= [];
  $r->{order_type} //= 1;
  
  if (! $r->{datetime} || ! scalar @{$r->{products}} || ! $r->{total} || ! $r->{customer_email}) {
    log("/tmp/sync_online_orders.log", ">> Missing datetime, total, customer_email or products parameter...");
    return;
  }

  my $dir = $machine->dir_areas()."/$AREA_FOR_ONLINE_ORDERS";

  my $id = $r->{id};

  if (! $id) {
    $id = area_nextid($dir);
  }

  if (-e "$dir/$id") {
    unlink "$dir/$id";
    unlink <$dir/$id.*>;
  }

  my( $date, $hour ) = $r->{datetime} =~ /^(.*?) (.*?)$/;

  my $business_date = date_based_on_closing_hour($r->{datetime}, $cfg->{END_BUSINESS_HOUR}.":00");

  my $area = $r->{order_type} == 1 ? $AREA_FOR_ONLINE_ORDERS : $AREA_FOR_ONLINE_ORDERS_DELIVERY;

  my $o = App::POS::Check->new( file => "$dir/$id" );
  my @header = $o->generate_empty_header($r->{datetime}, $business_date, $CLERK_FOR_ONLINE_ORDERS, $area);
  my @products = ();
  my @payments = ();
  my $products_extra_data = {};
  my $extra_data = {};

  $r->{customer_nif} //= "";
  $r->{customer_nif} = "" if $r->{customer_nif} && $r->{customer_nif} eq '999999990';
  $r->{customer_name} //= "";
  $r->{customer_address} //= "";
  $r->{customer_city} //= "";
  $r->{customer_postal_code} //= "";
  $r->{customer_phone} //= "";

  $r->{customer_name} = my_conv($r->{customer_name}) if $r->{customer_name};
  $r->{customer_address} = my_conv($r->{customer_address}) if $r->{customer_address};
  $r->{customer_city} = my_conv($r->{customer_city}) if $r->{customer_city};

  my $missed_orders = 0;

  if ($r->{customer_nif}) {
    my $customers = $db->select(
      "SELECT id, missed_orders FROM customers WHERE nif = ? AND deleted = 0",
      $r->{customer_nif},
    );

    for (@$customers) {
      $db->do(
        "UPDATE customers SET email = ?, name = ?, address = ?, city = ?, postal_code = ?, contact = ? WHERE id = ?",
        $r->{customer_email}, fix_encoding($r->{customer_name}), fix_encoding($r->{customer_address}),
        fix_encoding($r->{customer_city}), fix_encoding($r->{customer_postal_code}),
        fix_encoding($r->{customer_phone}), $_->{id}
      );
    }
  }

  # got a customer email
  if ($r->{customer_email}) {
    my $customer = $db->select(
      "SELECT id, missed_orders FROM customers WHERE email = ? AND deleted = 0 ORDER BY id",
      $r->{customer_email},
    );

    my $customer_id = "";

    if (scalar @$customer) {
      $customer_id = $customer->[0]->{id};
      $missed_orders = $customer->[0]->{missed_orders};
      log("/tmp/sync_online_orders.log", ">> customer exists $customer_id");
    } else {
      my $id = $db->next_id("customers");

      $db->do(qq{
        INSERT INTO customers (
          id, nif, name, address, city, postal_code, contact, email
        ) VALUES (
          ?, ?, ?, ?, ?, ?, ?, ?
        )
      }, $id, $r->{customer_nif},
        fix_encoding($r->{customer_name}), fix_encoding($r->{customer_address}),
        fix_encoding($r->{customer_city}), fix_encoding($r->{customer_postal_code}),
        fix_encoding($r->{customer_phone}), $r->{customer_email},
      );
 
      log("/tmp/sync_online_orders.log", ">> customer insert $customer_id");
    }

    $header[11] = $customer_id;
    $header[12] = $r->{customer_name};
    $header[13] = $r->{customer_nif};
  }

  $r->{delivery} //= 0;
  $r->{delivery_name} //= "";
  $r->{delivery_address} //= "";
  $r->{delivery_city} //= "";
  $r->{delivery_postal_code} //= "";

  $r->{delivery_name} = my_conv($r->{delivery_name}) if $r->{delivery_name};
  $r->{delivery_address} = my_conv($r->{delivery_address}) if $r->{delivery_address};
  $r->{delivery_city} = my_conv($r->{delivery_city}) if $r->{delivery_city};

  if ($r->{delivery}) {
    $extra_data->{obs} = "Delivery / Entregar em:§$r->{delivery_name}§".
      "$r->{delivery_address}§$r->{delivery_postal_code}§$r->{delivery_city}";
  }

  my $had_products = 0;
  my %invalid_alert = ();

  for (my $i=0; $i<scalar @{$r->{products}}; $i++) {
    my $r = $r->{products}->[$i];

    my $prod_id = $r->{id};
    my $pos_id = [];

    if ($ONLINE_ORDERS_USE_PRODUCT_ID && $prod_id =~ /^\d+$/) {
      $pos_id = $db->select(qq{
        SELECT id, sell_vat1
        FROM products
        WHERE id = ?
      }, $prod_id);
    }
    else {
      $pos_id = $db->select(qq{
        SELECT id, sell_vat1
        FROM products
        WHERE deleted = 0 AND (
          barcode = ? OR barcode LIKE '$prod_id,%' OR barcode LIKE '%,$prod_id'
          OR barcode LIKE '%,$prod_id,%'
        )
      }, $prod_id);
    }

    if (! scalar @$pos_id) {
      if (! exists $invalid_alert{$prod_id}) {
        $invalid_alert{$prod_id} = 1;
        _send_msg_to_ui($info->{ONLINE_ORDERS_ALERTS_IP}, "ENC ONLINE - ERRO PLU - ".$prod_id);
        sleep 1;
      }

      log("/tmp/sync_online_orders.log", ">> Can't find product with barcode $prod_id...");
      next;
    }

    $had_products = 1;

    $prod_id = $pos_id->[0]->{id};

    my $sell_vat1 = $pos_id->[0]->{sell_vat1};
    my $vat = $sell_vat1 || 13;

    my $holded = $PRODUCTS_ARE_NOT_HOLDED ? 0 : 1;

    $r->{obs} //= "";
    my $obs = $r->{obs} ? $r->{obs}.";" : '';
    $obs = my_conv($obs) if $obs;

    my $gbi = &strftime("%Y%m%d%H%M%S", localtime) . rand();
    $gbi =~ s/\.//g;

    $r->{extra} //= "";
    $r->{extra} = my_conv($r->{extra}) if $r->{extra};

    push @products, [
      $i,
      $prod_id,
      $hour,
      $r->{qty},
      $r->{price},
      0, # discount
      $vat,
      $CLERK_FOR_ONLINE_ORDERS,
      "",        # Order group description (Primeros, Secundos, etc)
      "",        # Group / dept where product was registered. Can be null if product was registered after a search, for example
      1,         # Operation Type
      "",        # Discount Txt
      0,         # station
      $r->{extra} ? 'EXTRA' : "", # Text Extra / Menu / Notas - EXTRA | MENU
      0,         # Inside Menu (0 / 1)
      $holded,   # Was holded already
      1,         # Price used on the sale
      $business_date,
      $gbi,
    ];

    if ($r->{obs}) {
      $products_extra_data->{$gbi} = $obs;
    }
  }

  if (! $had_products) {
    log("/tmp/sync_online_orders.log", ">> no valid products, aborting...");

    # mark order as synced
    my $ret = post_json(
      'http://apisv2.net-bo.com/pos_orders/synced', {
        token => $token, api_passwd => $api_passwd,
        orders => [ $r->{uuid} ], hardlock => $hardlock,
      },
    );

    return;
  }

  my $missed_orders_str = $missed_orders > 0 ? "<font color=red>$missed_orders</font>": "";

  if ($r->{payed}) {
    $extra_data->{online_order_payed} = 1;
    $extra_data->{invoice_is_payed} = 1;

    my $pmethod = "ONLINE";

    if ($r->{payment_method}) {
      $r->{payment_method} = my_conv($r->{payment_method});
      $pmethod = 'SITE_'.$r->{payment_method};
    }

    push @payments, [ $pmethod, $r->{total}, "" ];
  }

  $r->{pickup_date} //= "";
  $r->{pickup_time} //= "";

  my $pickup_info = "";

  if ($r->{pickup_date} ne $business_date) {
    if ($r->{pickup_time}) {
      $extra_data->{pickup_date} = $r->{pickup_date};
      $extra_data->{pickup_time} = $r->{pickup_time};
      $pickup_info = "<font size=1><br>$r->{pickup_date}<br>$r->{pickup_time}";
    } else {
      $extra_data->{pickup_date} = $r->{pickup_date};
      $pickup_info = "<br>$r->{pickup_date}";
    }
  }
  else {
    if ($r->{pickup_time}) {
      $extra_data->{pickup_time} = $r->{pickup_time};
      $pickup_info = "<br>$r->{pickup_time}";
    }
  }

  if ($r->{payed}) {
    create_file($o->file_name, "$missed_orders_str<font color=yellow>P</font>$id".$pickup_info);
  }
  else {
    create_file($o->file_name, $missed_orders_str.$id.$pickup_info);
  }

  if ($PRODUCTS_ARE_NOT_HOLDED) {
    $extra_data->{did_real_hold} = 0;
    create_file($o->file_has_unholded_products, 1);
  }

  $extra_data->{original_area} = $AREA_FOR_ONLINE_ORDERS;
  $extra_data->{online_order} = 1;

  my %data = (
    header   => \@header,
    details  => \@products,
    products_extra_data  => $products_extra_data,
    extra_data  => $extra_data,
    footers  => \@payments,
    payments => [],
  );

  #print STDERR Dumper(\%data);

  $o->set_data(\%data);

  $o->save();

  #if ($r->{payed}) {
  #  $o->finalize();
  #}

  create_file($machine->server_dir_tmp() . "/tables_refresh#".$cfg->{STATION_NUMBER}.".txt", $cfg->{STATION_NUMBER});

  if ($ui_alert) {
    _send_msg_to_ui($info->{ONLINE_ORDERS_ALERTS_IP}, "ENCOMENDAS ONLINE<BR>CHEGOU UMA ENCOMENDA");
  }

  tables_refresh();

  # mark order as synced
  my $ret = post_json(
    'http://apisv2.net-bo.com/pos_orders/synced', {
      token => $token, api_passwd => $api_passwd,
      orders => [ $r->{uuid} ], hardlock => $hardlock,
    },
  );

=pod
$VAR1 = {
          'pickup_time' => '17:30',
          'token' => 'auditorcliente18921',
          'customer_nif' => '123456789',
          'api_passwd' => '28d@dkeghe',
          'delivery_city' => 'Matosinhos',
          'customer_city' => 'Matosinhos',
          'customer_phone' => '93123456',
          'customer_postal_code' => '4450-159',
          'customer_name' => 'Nome Cliente',
          'products' => [
                          {
                            'id' => '1234',
                            'price' => '3.30',
                            'obs' => 'com picante, sem sal',
                            'qty' => '1'
                          },
                          {
                            'qty' => '3',
                            'price' => '0.50',
                            'id' => '1235'
                          }
                        ],
          'delivery_postal_code' => '4450-159',
          'total' => '3.80',
          'payed' => 1,
          'customer_address' => 'Rua de Cima, 234',
          'delivery_address' => 'Rua de Cima, 234',
          'payment_method' => 'MBWAY',
          'pickup_date' => '2020-06-26',
          'delivery' => 0,
          'id' => '1234'
        };

=cut

}

sub fire_holds_ontime {
  my $machine = shift;

  my $dir = $machine->dir_areas()."/$AREA_FOR_ONLINE_ORDERS";

  find(sub {
    return unless /^\d+$/;
    return unless -f $File::Find::name;

    my $check_id = $_;

    return unless -e $File::Find::name . ".unholded";

    my $o = App::POS::Check->new( file => $File::Find::name );
    my $d = $o->data();
    my $dt = $d->{header}->[0];

    $d->{extra_data}->{did_real_hold} //= 0;

    # means check was holded already
    if ($d->{extra_data}->{did_real_hold}) {
      return;
    }

    my $biz_date = $d->{header}->[23];
    my $pickup_date = $d->{extra_data}->{pickup_date} // $biz_date;
    my $pickup_time = $d->{extra_data}->{pickup_time} // "";

    if ($pickup_date && $pickup_time) {
      if ($pickup_time =~ /^\d\d\:\d\d$/) {
        $pickup_time .= ":00";
      }

      if ($pickup_time =~ /^\d\d:\d\d:\d\d$/) {
        my $now = strftime("%Y-%m-%d %H:%M:%S", localtime);
        my( $days, $hours, $minutes, $seconds ) = between_dates($now, "$pickup_date $pickup_time");

        if ($minutes >= $MINUTES_TO_HOLD) {
          log("/tmp/sync_online_orders.log", ">> will hold check $check_id in area $AREA_FOR_ONLINE_ORDERS...");
          _check_hold($AREA_FOR_ONLINE_ORDERS, $check_id);
        }
      }
    }
  }, ( $dir ));
}

sub create_file {
  my( $file, $cnt ) = @_;

  my $tmp_file = "/tmp/" . strftime("%Y%m%d%H%M%S", localtime) . rand();
  write_file($tmp_file, $cnt);
  move $tmp_file, $file;
}

sub _send_msg_to_ui {
  my $ip = shift;
  my $msg = shift;

  my $sock = IO::Socket::INET->new(
    PeerAddr => $ip,
    PeerPort => '8088',
    Proto => 'tcp',
    Timeout => 5,
  );

  if (defined $sock && $sock) {
    print $sock "#ALERT#$msg\n" if defined $sock && $sock;
    $sock->close();
  }
  else {
    log("/tmp/sync_online_orders.log", ">> _send_msg_to_ui - error connecting to $ip, port 8088...");
  }
}

sub _check_hold {
  my( $area, $checknum ) = @_;

  my $sock = IO::Socket::INET->new(
    PeerAddr => '127.0.0.1',
    PeerPort => '8088',
    Proto => 'tcp',
    Timeout => 5,
  );

  if (defined $sock && $sock) {
    print $sock "#CHECKHOLD#$area#$checknum\n" if defined $sock && $sock;
    $sock->close();
  }
  else {
    log("/tmp/sync_online_orders.log", ">> _check_hold - error connecting to port 8088...");
  }
}

sub setup_redis {
  undef $redis;

  print STDERR "### setup_redis()\n";

  # to subscribe
  $redis = Redis::Fast->new(
    server => $redis_host.":".$redis_port,
    debug => 0,
    password => $redis_passwd,
    name => 'my_connection_name',
  );

  # to publish
  #my $redis_p = Redis::Fast->new(
  #  server => $redis_host.":".$redis_port,
  #  debug => 0,
  #  password => $redis_passwd,
  #  name => 'my_connection_name',
  #);

  print STDERR "#### will subscribe - pos_orders::action_new_order::$TOKEN\n";

  $redis->subscribe(
    "pos_orders::action_new_order::".$TOKEN,
    sub {
      sync_virtual_order($machine, $TOKEN, $API_PASSWD, @_);
    },
  );
}

sub tables_refresh {
  my @stations = read_dir('/opt/pos/server/data/registered_stations');
  write_file("/opt/pos/server/data/tmp/tables_refresh#0.txt", "1");

  foreach my $st (@stations) {
    write_file("/opt/pos/server/data/tmp/tables_refresh#$st.txt", "1");
  }
}

