package App::POS::Machine;

use strict;
use warnings;
use File::Slurp qw( read_dir read_file write_file );
use POSIX qw( strftime );
use Data::Dumper;
use App::POS::Utils qw( my_conv memcache );
use App::POS::Area;
use App::POS::Counters;
use App::POS::DB::Postgres;
use Benchmark ':hireswallclock';
use Encode qw( encode decode );

use base qw( App::Class );

my %MEMC_CONNECTIONS;

sub init {
  my $self = shift;

  $self->{__printing_zones} = undef;
}

sub is_64_bits {
  my $self = shift;

  if (! exists $self->{__is_64_bits}) {
    my $is_64_bits = `uname -i | grep x86_64`;
    $self->{__is_64_bits} = $is_64_bits ? 1 : 0;
  }

  @_ ? $self->{__64_bits} = shift : $self->{__is_64_bits};
}

sub is_arm {
  my $self = shift;

  if (! exists $self->{__is_arm}) {
    my $is_arm = `uname -a | grep arm | grep -v grep`;
    $self->{__is_arm} = $is_arm ? 1 : 0;
  }

  @_ ? $self->{__is_arm} = shift : $self->{__is_arm};
}

sub dir_tmp {
  my $self = shift;
  my $ds = $self->directory_separator();
  return $self->dir().$ds.'common'.$ds.'tmp';
}

sub server_config {
  my $self = shift;

  my $f = $self->dir_server().'/etc/config.ini';
  return App::POS::Config->new( file => $f );
}


# being a station or a server allways return the tmp folder of the server
sub server_dir_tmp {
  my $self = shift;
  my $ds = $self->directory_separator();
  return $self->dir_server().$ds.'server'.$ds.'data'.$ds.'tmp';
}

sub server_dir_data {
  my $self = shift;
  my $ds = $self->directory_separator();
  return $self->dir_server().$ds.'server'.$ds.'data';
}

sub _db_cache {
  my $self = shift;
  @_ ? $self->{_db_cache} = shift : $self->{_db_cache};
}

sub _db_backup_cache {
  my $self = shift;
  @_ ? $self->{_db_backup_cache} = shift : $self->{_db_backup_cache};
}

sub dir {
  my $self = shift;
  return "/opt/pos";
}

sub dir_server {
  die "You must override the 'dir_server' method...";
}

sub dir_areas {
  my $self = shift;
  my $ds = $self->directory_separator();
  return $self->dir_server().$ds.'server'.$ds.'data'.$ds.'areas';
}

sub config {
  my $self = shift;
  @_ ? $self->{config} = shift : $self->{config};
}

sub get_series_codes {
  my $self = shift;
  return $self->{__series_codes} if defined $self->{__series_codes};

  my $server_config = $self->server_config();
  my $c = $server_config->load();

  my $series_codes = $c->{SAFT_SERIES_CODES} // [];
  my %series_codes = ();

  if (scalar @$series_codes) {
    foreach my $r (@$series_codes) {
      my( $serie, $code ) = split /,/, $r;
      $series_codes{$serie} = $code if defined $serie && defined $code;
    }
  }

  $self->{__series_codes} = \%series_codes;
  return $self->{__series_codes};
}

sub is_server {
  my $self = shift;

  #$self->debug("is_server");

  my $c = $self->config()->load();

  if (! exists $c->{STATION_NUMBER} || $c->{STATION_NUMBER} == 0 || $c->{STATION_NUMBER} == 0) {
    return 1;
  } else {
    return 0;
  }
}

sub is_station {
  my $self = shift;

  #$self->debug("is_station");

  my $c = $self->config()->load();

  return exists $c->{STATION_NUMBER} && $c->{STATION_NUMBER} ? 1 : 0;
}

# full info for printing zones
sub printing_zones {
  my $self = shift;
  return $self->{__printing_zones} if defined $self->{__printing_zones};

  my $cfg = "";

  if ($self->is_station()) {
    $cfg = $self->server_config();
  }
  else {
    $cfg = $self->config();
  }

  my $i = $cfg->load_fixed();

  $i->{PrintingZones}->{Zone} ||= [];

  $self->{__printing_zones} = $i->{PrintingZones}->{Zone};
  return $self->{__printing_zones};
}

sub printing_zones_filters {
  my $self = shift;

  my $pz = $self->printing_zones();
  my $r = {};

  foreach my $p (@$pz) {
    my $printers = [];

    if (exists $p->{Printer}) {
      if (ref $p->{Printer} eq 'HASH') {
        push @$printers, $p->{Printer}
      } elsif (ref $p->{Printer} eq 'ARRAY') {
        $printers = $p->{Printer};
      }
    }

    my $c = 0;

    for (@$printers) {
      my $from_station = exists $_->{FromStation} ? $_->{FromStation} : undef;
      my $from_area = exists $_->{FromArea} ? $_->{FromArea} : undef;
      my $copies = exists $_->{Copies} ? $_->{Copies} : 1;
      $copies ||= 1;

      push @{$r->{ $p->{Name} }->{Copies}}, $copies;
      $r->{ $p->{Name} }->{$c}->{FromStation} = $from_station;
      $r->{ $p->{Name} }->{$c}->{FromArea} = $from_area;

      $c++;
    }
  }

  return $r;
}

# tiny info for printing purposes
sub printing_zones_quick {
  my $self = shift;

  my $pz = $self->printing_zones();
  my $r = {};

  foreach my $p (@$pz) {
    my $printers = [];

    if (exists $p->{Printer}) {
      if (ref $p->{Printer} eq 'HASH') {
        push @$printers, $p->{Printer}
      } elsif (ref $p->{Printer} eq 'ARRAY') {
        $printers = $p->{Printer};
      }
    }

    for (@$printers) {
      $_->{Station} ||= 0;
      $_->{Number} ||= 1;
      push @{ $r->{ $p->{Name} } }, $_->{Station}."#".$_->{Number};
    }
  }

  return $r;
}

sub printers {
  my $self = shift;
  return $self->{__printers} if defined $self->{__printers};

  my $cfg = $self->config();
  my $i = $cfg->load_fixed();

  $i->{Hardware}->{Printers}->{Device} ||= [];
  $self->{__printers} = $i->{Hardware}->{Printers}->{Device};

  return $self->{__printers};
}

sub areas {
  my $self = shift;

  my $dir = $self->dir_server()."/server/data/areas";

  my @areas = read_dir($dir);
  my @a = ();

  foreach my $a (sort @areas) {
    next unless $a =~ /^\d+$/;
    push @a, App::POS::Area->new( machine => $self, number => $a );
  }

  return \@a;
}

sub hash_to_hdd {
  my $self = shift;

  my( $f, $i ) = @_;

  return unless scalar keys %$i;

  open(F, "> $f");

  foreach my $k (keys %$i) {
    print F $k."§".$i->{$k}."\n";
  }

  close(F);
}

sub hash_from_hdd {
  my $self = shift;

  my( $f ) = @_;

  my $i = {};

  if (open(F, "< $f")) {
    while (<F>) {
      chop;
      my @t = split /§/;
      $i->{$t[0]} = $t[1];
    }

    close(F);
  }

  return $i;
}

sub database_dsn {
  my $self = shift;
  my $dsn = 'Pg:dbname=pos;host='.$self->database_host();
  return $dsn;
}

sub database {
  my $self = shift;

  my $dsn = "DBI:".$self->database_dsn();
  my $user = 'root';
  my $passwd = '';

  #my $pg_options = {PrintError => 0, RaiseError => 1, pg_server_prepare => 1, pg_enable_utf8 => 1};
  my $pg_options = {PrintError => 0, RaiseError => 1, pg_server_prepare => 1};

  $self->debug("database");

  eval {
    if (defined $self->{__pg_conn}) {
      $self->debug("REUSING DATABASE CONNECTION\n");

      if ($self->{__pg_conn}->{dbh}->ping()) {
        $self->debug("PING ACTIVE REUSING DATABASE CONNECTION\n");
      } else {
        $self->{__pg_conn} = App::POS::DB::Postgres->new(dbh => DBI->connect($dsn,$user,$passwd,$pg_options));
        $self->debug("NEW DATABASE CONNECTION\n");
      }
    } else {
      $self->debug("NEW DATABASE CONNECTION\n");
      $self->{__pg_conn} = App::POS::DB::Postgres->new(dbh => DBI->connect($dsn,$user,$passwd,$pg_options));
    }
  };

  while ($@) {
    print STDERR "DB ERROR: $@\n";
    $self->debug("Got $@\nRetrying\n");

    open(F, "> /tmp/server_db_error.txt");
    close(F);

    eval {
      if (defined $self->{__pg_conn}) {
        $self->debug("REUSING DATABASE CONNECTION\n");

        if ($self->{__pg_conn}->{dbh}->ping()) {
          $self->debug("PING ACTIVE REUSING DATABASE CONNECTION\n");
        } else {
          $self->{__pg_conn} = App::POS::DB::Postgres->new(dbh => DBI->connect($dsn,$user,$passwd,$pg_options));
          $self->debug("NEW DATABASE CONNECTION\n");
        }
      } else {
        $self->debug("NEW DATABASE CONNECTION\n");
        $self->{__pg_conn} = App::POS::DB::Postgres->new(dbh => DBI->connect($dsn,$user,$passwd,$pg_options));
      }
    };

    sleep 2;
  }

  unlink "/tmp/server_db_error.txt" if -e "/tmp/server_db_error.txt";

  return $self->{__pg_conn};
}

sub card_sets_information {
  my $self = shift;

  my $r = {};

  my $file_card_sets = $self->dir_server() . "/server/data/areas/card_sets.txt";
  return $r unless -e $file_card_sets;

  my $idx = 0;

  if (open(F, "< $file_card_sets")) {
    while (<F>) {
      chop;
      my $line = $_;
      next unless defined $line && $line ne '';
      my @t = split /#/, $line;
      $r->{sets}->{$idx} = \@t;
      push @{ $r->{by_name}->{$t[0]} }, $idx;

      my $tables = _decode_set_tables($t[0]);

      foreach (@$tables) {
        push @{ $r->{by_table}->{$_} }, $idx;
      }

      $idx++;
    }
    close(F);
  }

  return scalar keys %$r ? $r : undef;
}

sub sign_string {
  my $self = shift;
  my $str_to_sign = shift;

  my $c = $self->config()->load_fixed();

  if (! exists $self->{__sign_app}) {
    if ($self->is_64_bits()) {
      $self->{__sign_app} = $self->dir()."/common/skins/".$c->{LANG}."/sign_server.src";
    }
    else {
      $self->{__sign_app} = $self->dir()."/common/skins/".$c->{LANG}."/sign_server";
    }
  }

  my $sign_app = $self->{__sign_app};

  my $socket = IO::Socket::INET->new(
    PeerAddr => '127.0.0.1',
    PeerPort =>  8050,
    Proto    => "tcp",
  );

  my $str = "";

  if (! defined $socket) {
    print STDERR "!!!! Couldn't connect to SIGN server !!!!\n";
    $str = `$sign_app --cli --string=\"$str_to_sign\"`;
  }
  else {
    print $socket "--string=\"".$str_to_sign."\"\n";
    $str = <$socket>;
    $socket->close();
  }

  $str =~ s/[\r\n]+$//g;
  $str ||= "";

  my $count = 0;

  while (length($str) != 172) {
    print STDERR "We arrived here and we should have a signature... if not, let's generate one without server...\n";
    $str = `$sign_app --cli --string=\"$str_to_sign\"`;
    $str =~ s/[\r\n]+$//g;
    last if $count++ == 10;
  }

  return $str;
}

sub _decode_set_tables {
  my $str = shift;
  my $r = [];

  my @t = split /,/, $str;

  for my $tmp (@t) {
    if ($tmp =~ /^\d+$/) {
      push @$r, $tmp;
    }
    elsif (my( $start, $end ) = $tmp =~ /^(\d+)\-(\d+)$/) {
      if ($start*1 <= $end*1) {
        for (my $i=$start; $i<=$end; $i++) {
          push @$r, $i;
        }
      }
    }
  }

  return $r;
}

sub hardlock_information {
  my $self = shift;
  return $self->{__hardlock_information} if exists $self->{__hardlock_information};

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

  my $line = "";

  if ($sock) {
    print $sock "\n";
    my $line = <$sock>;
    $sock->close();

    $line //= "";
    my @a = split /#/, $line;

    if (scalar @a) {
      $self->{__hardlock_information} = \@a;
      return return \@a;
    }
  }

  return [];
}

sub pt_certification_info {
  my $self = shift;
  return $self->{__pt_certification_info} if exists $self->{__pt_certification_info};

  my $cfg = $self->config()->load_fixed();
  $cfg->{LANG} //= "";

  my $pt_certified_software = -e "/opt/pos/common/skins/".$cfg->{LANG}."/sign_server" ? 1 : 0;
  my @info = ();

  if ($pt_certified_software) {
    my $cmd = "";

    if ($self->is_64_bits()) {
      $cmd = "/opt/pos/common/skins/".$cfg->{LANG}."/sign_server.src --saft --cli";
    }
    else {
      $cmd = "/opt/pos/common/skins/".$cfg->{LANG}."/sign_server --saft --cli";
    }

    my $info = `$cmd`;
    chop($info);
    @info = split /#/, $info;
  }

  $self->{__pt_certification_info} = \@info;
  return return \@info;
}

sub software_version_info {
  my $self = shift;
  return $self->{__software_version_info} if exists $self->{__software_version_info};

  my @a = ();
  my $file = '/opt/pos/version';

  if (-e $file) {
    my $str = read_file($file);
    chop($str);
    @a = split /#/, $str;
  }

  $self->{__software_info} = \@a;
  return return \@a;
}

sub software_version {
  my $self = shift;
  my $svi = $self->software_version_info();
  return 201707 unless scalar @$svi;
  return $svi->[0]*1;
}

sub send_alert_to_ui {
  my $self = shift;
  my $msg = shift;

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

  if ($sock) {
    print $sock "#ALERT#$msg\n";
    $sock->close();
  }
}

sub find_next_doc_id {
  my $self = shift;
  my( $doc_type, $serie, $business_date, $doc_tmp, $pt_certified_software ) = @_;

  my $t1 = new Benchmark;

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

  # =====================================================================
  my $doc_counters = App::POS::Counters->new(
    db => $db,
    name => 'new_doc_counters',
  );

  my $counter_value = $doc_counters->cur_value($doc_type . "___" . $serie) || "";

  if ($counter_value) {
    my $doc_hashes = App::POS::Counters->new(
      db => $db,
      name => 'doc_hashes',
    );

    my $prev_hash = $doc_hashes->cur_value($doc_type . "___" . $serie) || "";

    $counter_value++;
    $doc_counters->set_value($doc_type . "___" . $serie, $counter_value);

    #print STDERR "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX counter_value: $counter_value, prev_hash: $prev_hash\n";

    return( $counter_value, $prev_hash );
  }
  # =====================================================================

  my $doc_types = $self->config()->doc_types_by_id();
  my $doc_type_str = $doc_types->{$doc_type};
  $doc_type_str = $doc_tmp if defined $doc_tmp;

  my $memc = memcache($self->server_ip(), 'pos_files', \%MEMC_CONNECTIONS);

  my $last_date;
  eval { $last_date = $memc->get("last_business_date#$doc_type#$serie") // ""; };
  $last_date = "" unless $last_date;

  my $q = "";

  my $where_str = "";

  if ($last_date) {
    $where_str = " AND business_date >= '$last_date'";
  }

  if ($pt_certified_software) {
    $q = qq{
      SELECT fo_doc_number, hash
      FROM sales_hashes
      WHERE doc_type = $doc_type AND fo_doc_number LIKE '$doc_type_str $serie/%' $where_str
      ORDER BY date_inserted DESC LIMIT 1
    };
  }
  else {
    $q = qq{
      SELECT fo_doc_number
      FROM sales_hashes
      WHERE doc_type = $doc_type AND fo_doc_number LIKE '$doc_type_str $serie/%' $where_str
      ORDER BY date_inserted DESC LIMIT 1
    };
  }

  #print STDERR $q;

  my $recs = $db->select($q);

  $memc->set("last_business_date#$doc_type#$serie", $business_date);

  my $t2 = new Benchmark;
  #print STDERR "####### TIME GETTING DOC ID - ".timestr(timediff($t2, $t1))."\n";

  if (! scalar @$recs) {
    $doc_counters->set_value($doc_type . "___" . $serie, 1);
    return ( 1, "" );
  }

  my @a = split /\//, $recs->[0]->{fo_doc_number};

  my $c = ++$a[1];

  $doc_counters->set_value($doc_type . "___" . $serie, $c);

  if ($pt_certified_software) {
    return ( $c, $recs->[0]->{hash} );
  }
  else {
    return ( $c, "" );
  }
}

sub find_next_doc_id_old {
  my $self = shift;
  my( $doc_type, $serie, $business_date, $doc_tmp ) = @_;

  my $recs = [];

  my @date_split = split /\-/, $business_date;
  my $table_name = "sales_hashes_".$date_split[0].$date_split[1];

  my $prev_year  = $date_split[0];
  my $prev_month = $date_split[1];

  if ($prev_month eq '01') {
    $prev_month = 12;
    $prev_year--;
  }
  else {
    $prev_month = sprintf("%02d", $prev_month - 1);
  }

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

  my $doc_types = $self->config()->doc_types_by_id();
  my $doc_type_str = $doc_types->{$doc_type};
  $doc_type_str = $doc_tmp if defined $doc_tmp;

  #print STDERR "### doc_type: $doc_type, serie: $serie, doc_type_str: $doc_type_str, table_name: $table_name\n";

  my $q = qq{
    SELECT fo_doc_number, hash
    FROM $table_name
    -- FROM sales_hashes
    WHERE doc_type = $doc_type AND fo_doc_number LIKE '$doc_type_str $serie/%'
    ORDER BY date_inserted DESC LIMIT 1
  };

  eval { $recs = $db->select($q); };

  if (! scalar @$recs) {
    $table_name = "sales_hashes_".$prev_year.$prev_month;

    my $q = qq{
      SELECT fo_doc_number, hash
      FROM $table_name
      -- FROM sales_hashes
      WHERE doc_type = $doc_type AND fo_doc_number LIKE '$doc_type_str $serie/%'
      ORDER BY date_inserted DESC LIMIT 1
    };

    eval { $recs = $db->select($q); };

    if (! scalar @$recs) {
      return ( 1, "" );
    }
    else {
      my @a = split /\//, $recs->[0]->{fo_doc_number};
      return ( ++$a[1], $recs->[0]->{hash} );
    }
  }
  else {
    my @a = split /\//, $recs->[0]->{fo_doc_number};
    return ( ++$a[1], $recs->[0]->{hash} );
  }
}

sub get_customer_info {
  my $self = shift;
  my( $db, $id, $info ) = @_;

  my $cust = $db->select("SELECT * FROM customers WHERE id = ? ORDER BY deleted ASC LIMIT 1", $id);

  for (@$cust) {
    foreach my $k (keys %$_) {
      my $val = defined $_->{$k} && $_->{$k} ? $_->{$k} : "";
      $info->{"customer_".$k} = my_conv($val);

      if ($k eq 'aux' && $val) {
        my @a = split /§/, $val;

        if (scalar @a) {
          $info->{"customer_".$k} = my_conv($a[0]);
          $info->{"customer_".$k} =~ s/#/\n/g;

          if (scalar @a >= 2) {
            my @custom_fields = split /#/, $a[1];

            for (my $i=0; $i<scalar @custom_fields; $i++) {
              my $idx = $i+1;
              $info->{"customer_custom_field".$idx} = my_conv($custom_fields[$i]);
            }
          }
        }
      }
    }
  }
}

sub decode_doctype_serie {
  my $self = shift;

  my $cfg = $self->config()->load_fixed();
  my $clerk_serie = "";

  if ($self->software_version() < 202101) {
    $clerk_serie = 1;
  }

  my $doc_type_serie = "";

  # server
  if ($self->is_server()) {
    $doc_type_serie = $cfg->{DOC_TYPE_SERIE} || "";
  }
  # station, get info from server config
  else {
    my $server_config = $self->server_config();
    my $server_cfg = $server_config->load_fixed();
    $doc_type_serie = $server_cfg->{DOC_TYPE_SERIE} || "";
  }

  if ($doc_type_serie) {
    my $software_version = $self->software_version() // "";

    if ($software_version ne '' && $software_version >= 202101) {
      $doc_type_serie =~ s/\@YEAR\@//g;
    }
    else {
      my $year = &strftime("%y", localtime);
      $doc_type_serie =~ s/\@YEAR\@/$year/g;
    }

    $doc_type_serie =~ s/\///g;

    return $doc_type_serie.$clerk_serie;
  }
  else {
    return strftime("%y", localtime);
  }
}

# refresh caches in all stations
sub cache_refresh {
  my $s = shift;
  my $file = shift;

  write_file($s->dir_server()."/server/data/tmp/cache_refresh#0.txt", 1);

  my @stations = read_dir($s->dir_server() . "/server/data/registered_stations");

  foreach my $st (@stations) {
    # ignore handies
    if ($st < 80) {
      write_file($s->dir_server()."/server/data/tmp/cache_refresh#$st.txt", 1);
    }
  }
}

1;

