# Copyright (c) 2001-2023 Hewlett Packard Enterprise Development LP

use strict;
use warnings;
no warnings 'once';
package Mercury::Licensing::Single;
use base qw(Mercury::Base);

use POSIX qw(ceil);
use List::Util qw(max);
use Date::Parse qw(str2time);
use HTML::Entities qw(decode_entities);
use Mercury::DB::Q;
use Mercury::Utility qw(dynamic_use);
use Mercury::Constants;
use Mercury::DB::License;
use Mercury::Utility::MAC;
use Mercury::Utility::File qw(:raw);
use Mercury::Utility::Assert;
use Mercury::Utility::Memoize qw(memoize_function);
use Mercury::Utility::Network;
use Mercury::Logger::AMPEvents;
use Mercury::Utility::DataManip qw(:context :set);
use Mercury::Licensing::Failover;


use constant FAILOVER_LICENSE_ID => 0;

sub ifconfig_period { $C'ONE_HOUR }
sub gpg_homedir { $C'GPG_HOMEDIR }
sub failover_license_filename { dynamic_use('Mercury::Licensing')->failover_license_filename }

sub from_file { # for convenience
  my ($class, $file) = @_;
  return $class->new(
    license => get_raw_file_content($file),
  );
}

sub init {
  my ($self) = @_;

  assert_quickly defined($self->{license}),
    'Must provide raw license as argument.';

  $self->{license} = $self->clean_license($self->{license});
  $self->_parse;
}

sub organization { shift->{_attr}{organization} }
sub product { shift->{_attr}{product} }
sub package { shift->{_attr}{package} }
sub aps { shift->{_attr}{aps} }
sub generated { shift->{_attr}{generated} }
sub expires { shift->{_attr}{expires} }
sub serial { shift->{_attr}{serial} }
sub signature { shift->{_attr}{signature} }
sub ip_address { shift->{_attr}{ip_address} }
sub mac { Mercury::Utility::MAC->any2text(shift->{_attr}{hardware_id}) }

sub is_post_8_0 {
  my ($self) = @_;

  my $package = $self->package;
  return bool(defined($package) && $package =~ $C'LICENSE_POST_8_0_PACKAGE_RX);
}

sub generated_epoch {
  my ($self) = @_;

  my $gen = $self->{_attr}{generated} // '';
  $gen =~ s/by.*//;
  return str2time($gen);
}

sub id {
  my $self = shift;
  $self->{id} = shift if @_;
  return $self->{id};
}

sub _parse {
  my ($self) = @_;

  my $gather;
  my (@body_lines, @sig_lines);
  foreach my $line (split /\n/, $self->{license}) {
    next unless $line =~ /\S/;
    $line =~ s/\r//;

    last if $line =~ /^--- End .*? License Key ---/;

    if ($line =~ /^--- Begin .*? License Key ---/) {
      $gather = \@body_lines;
    } elsif ($line =~ /^--- (Begin License )?Signature ---/) {
      $gather = \@sig_lines;
    } elsif ($gather) {
      push @$gather, $line;
    }
  }

  my %attr;
  foreach my $body_line (@body_lines) {
    my ($key, $value) = ($body_line =~ /(.*?):\s*(.*)/);
    next unless $key;

    $attr{lc($key)} = $value;
  }

  decode_entities($attr{organization}) if $attr{organization};
  $attr{signature} = join('', @sig_lines);
  $attr{signature} =~ s/\s+//g;

  $self->{_attr} = \%attr;
}

sub is_valid_schemeless {
  my ($self) = @_;
  return
    $self->is_verified &&
    $self->is_active &&
    $self->is_valid_machine;
}

sub is_valid {
  my ($self) = @_;
  return $self->is_valid_schemeless && $self->is_license_scheme_ok;
}

sub is_valid_machine {
  my ($self) = @_;
  my $failover = $self->_obtain_failover;
  return $failover->is_valid_ip && $failover->is_valid_mac
    if $failover;

  return $self->is_valid_ip && $self->is_valid_mac;
}

sub is_license_scheme_ok { 1 }

sub _obtain_failover {
  my ($self) = @_;
  return undef if $self->is_failover;

  # avoid invoking M::Licensing->get circularly
  my $existing_failover = Mercury::Licensing::Failover->get_by_id(FAILOVER_LICENSE_ID);
  my $is_failed_over = $existing_failover
    && !dynamic_use('Mercury::Licensing')->_backup_amp
    && $existing_failover->is_verified
    && $existing_failover->is_active;
  return $is_failed_over ? $existing_failover : undef;
}

memoize_function('is_verified');
sub is_verified { return 1 }

sub is_valid_ip {
  my ($self) = @_;
  return bool(!$self->ip_address
    || in($self->ip_address, @{ $self->parsed_ifconfig->{ips} }));
}

sub is_valid_mac {
  my ($self) = @_;
  return bool(!$self->mac
    || in($self->mac, @{ $self->parsed_ifconfig->{macs} }));
}

sub is_time_limited { bool(shift->expires) }

sub is_eval {
  my ($self) = @_;
  return $self->is_post_8_0
    ? 0 # TODO: we don't know what a post 8.0 eval license looks like
    : bool($self->is_time_limited);
}

sub is_failover {
  my ($self) = @_;
  return bool(defined($self->product) && $self->product =~ /Failover$/);
}

sub is_master_console {
  my ($self) = @_;
  return bool(defined($self->product) && $self->product eq 'Master Console');
}

sub has_master_console {
  my ($self) = @_;
  return bool(
    $self->is_master_console || ($self->{_attr}{master_console} // '') eq 'Yes'
  );
}

sub is_active {
  my ($self) = @_;

  my $remaining = $self->days_remaining;
  return bool(!defined($remaining) || $remaining > 0);
}

sub days_remaining {
  my ($self) = @_;

  if (!$self->is_verified) {
    return 0;
  } elsif (defined($self->expires)) {
    return max(0, ceil(($self->expires - $self->now) / $C'ONE_DAY));
  } else {
    return undef;
  }
}

sub verify {
  
  return 1;
}

sub _gpg_verify {
  my ($self, $gpg_lic) = @_;

  my $homedir = $self->gpg_homedir;
  open(my $GPG, "| /usr/bin/gpg --homedir '$homedir' --verify - > /dev/null 2>&1")
    or die $!;

  print $GPG $gpg_lic;
  close($GPG);

  my $err = $?;
  if ($err & 128) { # seg fault
    Mercury::Logger::AMPEvents->log_sys('gpg experienced a crash and dumped its core.');
  }

  return $err == 0;
}

{
  my (@cached_ifconfig, $cached_ifconfig_time);
  
  sub get_ifconfig {
    my ($self) = @_;

    if (!@cached_ifconfig || ($self->now - $cached_ifconfig_time) > $self->ifconfig_period) {
      $cached_ifconfig_time = $self->now;
      @cached_ifconfig = Mercury::Utility::Network::get_ifconfig;
    }

    return wantarray
      ? @cached_ifconfig
      : join('', @cached_ifconfig);
  }
}

sub parsed_ifconfig {
  my ($self) = @_;

  my (@macs, @ips);
  foreach my $line ($self->get_ifconfig) {
    if (my ($mac) = $line =~ /link\/ether\s+(\S+)\s+/) {
      push @macs, $mac;
    } elsif (my ($ipv6) = $line =~ /inet6\s+($C'IPV6_REGEX)\/\d+\s+/){
      push @ips, $ipv6;
    } elsif (my ($ip) = $line =~ /inet\s+($C'IP_REGEX)\/\d+\s+/) {
      push @ips, $ip;
    }
  }
  return {
    macs => \@macs,
    ips => \@ips,
  };
}

sub clean_license {
  my ($self, $lic) = @_;

  $lic =~ s/^.*?(--- Begin .*? License Key ---)/$1/s;
  # Note that allowing for no trailing newline below lets us better
  #  handle copy/pasted licenses.
  $lic =~ s/(--- End .*? License Key ---).*?$/$1\n/s;
  $lic =~ s/\r//g;
  $lic =~ s/\n\n+/\n/g;
  $lic =~ s/ +$//mg;

  return $lic;
}

sub was_saved {
  my ($self) = @_;

  my %existing =
    map { $_->signature => 1 }
    ref($self)->get_all;

  return bool($existing{$self->signature});
}

sub save {
  my ($self) = @_;
  throw(error => 'duplicate_license') if $self->was_saved;
  throw(error => 'license_not_verified') unless $self->verify;
  throw(error => 'license_scheme_not_accepted') unless $self->is_license_scheme_ok;

  if ($self->is_failover) {
    my $current_lic = Mercury::Licensing::Failover->get_by_id(FAILOVER_LICENSE_ID);
    if ($current_lic) {
      my $current_ap_count = $current_lic->aps;
      my $new_ap_count = $self->aps;
      
      if ($current_ap_count && $new_ap_count) {
        my $new_total = $current_ap_count + $new_ap_count;
        $self->{license} =~ s/APs: $current_ap_count/APs: $new_total/;
      }
    }
    write_raw_file($self->{license}, ref($self)->failover_license_filename);
    Mercury::DB::Q->execute('update seas_config set backup_amp = 1');
    $self->id(FAILOVER_LICENSE_ID);
  } else {
    if ($self->is_valid) {
      my $licensing = dynamic_use('Mercury::Licensing')->hard_get;

      # get out of bad grace
      dynamic_use('Mercury::Licensing::Grace')->get->end_grace
          if $licensing->in_bad_grace;
    }

    $self->id(
      Mercury::DB::License->create(
        raw_license => $self->{license},
        generated => $self->generated_epoch,
        (map { $_ => $self->$_ } qw(
          organization product package aps expires serial ip_address mac
        )),
      )->id
    );
  }

  # refresh
  dynamic_use('Mercury::Licensing')->hard_get;

  return $self;
}

sub delete {
  my ($self) = @_;

  my $id = $self->id;

  assert_quickly defined($id),
    'Cannot find license to delete by id.';

  my $licensing = dynamic_use('Mercury::Licensing')->hard_get;

  if ($id == FAILOVER_LICENSE_ID) {
    my $failover_file = ref($self)->failover_license_filename;
    Mercury::DB::Q->execute('update seas_config set backup_amp = 0');
    unlink $failover_file if -f $failover_file;
  } else {
    Mercury::DB::License->delete_by_id($id);
  }

  $self->id(undef);

  # refresh
  $licensing = dynamic_use('Mercury::Licensing')->hard_get;

  return $self;
}

sub get_by_id {
  my ($class, $id) = @_;

  if ($id == FAILOVER_LICENSE_ID) {
    return Mercury::Licensing::Failover->get_by_id($id);
  } else {
    my $lic = Mercury::DB::License->get_by_id($id);

    if ($lic) {
      my $license = $class->new(license => $lic->raw_license);
      $license->id($lic->id);
      return $license;
    }
  }

  return undef;
}

sub get_all {
  my ($class) = @_;

  my @licenses = map {
    my $license = $class->new(license => $_->raw_license);

    $license->id($_->id);
    $license;
  } Mercury::DB::License->get_all;

  my $failover_license = Mercury::Licensing::Failover->get_by_id(FAILOVER_LICENSE_ID);
  unshift @licenses, $failover_license if $failover_license;

  return dedup_by_method 'signature', @licenses;
}

1;
