# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.

package Bugzilla::Extension::Push::Config;

use 5.10.1;
use strict;
use warnings;

use Bugzilla::Logging;
use Bugzilla::Constants;
use Bugzilla::Extension::Webhooks::Webhook;
use Bugzilla::Extension::Push::Option;
use Crypt::CBC;

sub new {
  my ($class, $name, @options) = @_;
  my $self = {_name => $name};
  bless($self, $class);

  $self->{_options} = [@options];
  unshift @{$self->{_options}},
    {
    name    => 'enabled',
    label   => 'Status',
    help    => '',
    type    => 'select',
    values  => ['Enabled', 'Disabled'],
    default => 'Disabled',
    };

  return $self;
}

sub options {
  my ($self) = @_;
  return @{$self->{_options}};
}

sub option {
  my ($self, $name) = @_;
  foreach my $option ($self->options) {
    return $option if $option->{name} eq $name;
  }
  return undef;
}

sub load {
  my ($self) = @_;
  my $config = {};

  # prime $config with defaults
  foreach my $rh ($self->options) {
    $config->{$rh->{name}} = $rh->{default};
  }

  # override defaults with values from database
  my $options
    = Bugzilla::Extension::Push::Option->match({connector => $self->{_name},});
  foreach my $option (@$options) {
    my $option_config = $self->option($option->name) || next;
    if ($option_config->{type} eq 'password') {
      $config->{$option->name} = $self->_decrypt($option->value);
    }
    else {
      $config->{$option->name} = $option->value;
    }
  }

  # validate when running from the daemon
  if (Bugzilla->push_ext->is_daemon) {
    $self->_validate_config($config);
  }

  # done, update self
  foreach my $name (keys %$config) {
    my $value
      = $self->option($name)->{type} eq 'password' ? '********' : $config->{$name};
    TRACE(sprintf "%s: set %s=%s\n", $self->{_name}, $name, $value || '');
    $self->{$name} = $config->{$name};
  }
}

sub validate {
  my ($self, $config) = @_;
  $self->_validate_mandatory($config);
  $self->_validate_config($config);
}

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

  my @valid_options = map { $_->{name} } $self->options;

  my %options;
  my $options_list
    = Bugzilla::Extension::Push::Option->match({connector => $self->{_name},});
  foreach my $option (@$options_list) {
    $options{$option->name} = $option;
  }

  # delete options which are no longer valid
  foreach my $name (keys %options) {
    if (!grep { $_ eq $name } @valid_options) {
      $options{$name}->remove_from_db();
      delete $options{$name};
    }
  }

  # update options
  foreach my $name (keys %options) {
    my $option = $options{$name};
    if ($self->option($name)->{type} eq 'password') {
      $option->set_value($self->_encrypt($self->{$name}));
    }
    else {
      $option->set_value($self->{$name});
    }
    $option->update();
  }

  # add missing options
  foreach my $name (@valid_options) {
    next if exists $options{$name};
    Bugzilla::Extension::Push::Option->create({
      connector    => $self->{_name},
      option_name  => $name,
      option_value => $self->{$name},
    });
  }
}

sub _remove_invalid_options {
  my ($self, $config) = @_;
  my @names;
  foreach my $rh ($self->options) {
    push @names, $rh->{name};
  }
  foreach my $name (keys %$config) {
    if ($name =~ /^_/ || !grep { $_ eq $name } @names) {
      delete $config->{$name};
    }
  }
}

sub _validate_mandatory {
  my ($self, $config) = @_;
  $self->_remove_invalid_options($config);

  my @missing;
  foreach my $option ($self->options) {
    next unless $option->{required};
    my $name = $option->{name};
    if ( !exists $config->{$name}
      || !defined($config->{$name})
      || $config->{$name} eq '')
    {
      push @missing, $option;
    }
  }
  if (@missing) {
    my $connector = $self->{_name};
    @missing = map { $_->{label} } @missing;
    if (scalar @missing == 1) {
      die "The option '$missing[0]' for the connector '$connector' is mandatory\n";
    }
    else {
      die "The following options for the connector '$connector' are mandatory:\n  "
        . join("\n  ", @missing) . "\n";
    }
  }
}

sub _validate_config {
  my ($self, $config) = @_;
  $self->_remove_invalid_options($config);

  my @errors;
  foreach my $option ($self->options) {
    my $name = $option->{name};
    next unless exists $config->{$name} && exists $option->{validate};
    eval { $option->{validate}->($config->{$name}, $config); };
    push @errors, $@ if $@;
  }
  die join("\n", @errors) if @errors;

  if ($self->{_name} ne 'global') {
    my $name = $self->{_name} =~ /\QWebhook\E/ ? 'Webhook' : $self->{_name};
    my $class = 'Bugzilla::Extension::Push::Connector::' . $name;
    $class->options_validate($config);
  }
}

sub _cipher {
  my ($self) = @_;
  $self->{_cipher} ||= Crypt::CBC->new(
    -key    => Bugzilla->localconfig->site_wide_secret,
    -cipher => 'DES_EDE3'
  );
  return $self->{_cipher};
}

sub _decrypt {
  my ($self, $value) = @_;
  my $result;
  eval { $result = $self->_cipher->decrypt_hex($value) };
  return $@ ? '' : $result;
}

sub _encrypt {
  my ($self, $value) = @_;
  return $self->_cipher->encrypt_hex($value);
}

1;
