package pf::ConfigStore;

=head1 NAME

pf::ConfigStore

=cut

=head1 DESCRIPTION

pf::ConfigStore

Is the Base class for accessing pf::IniFiles

=cut

use Moo;
use pfconfig::git_storage;
use namespace::autoclean;
use pf::IniFiles;
use pf::log;
use pf::util qw();
use List::MoreUtils qw(uniq);
use Sort::Naturally qw(nsort);
use pfconfig::manager;
use pf::api::jsonrpcclient;
use pf::config::cluster;
use pf::constants;
use pf::CHI;
use pf::generate_filter qw(filter_with_offset_limit);
use pf::condition_parser qw(parse_condition_string ast_to_object);
our %TOP_OPS = (
    not_and => undef,
    not_or => undef,
    and => undef,
    or => undef,
);

=head1 FIELDS

=head2 cachedConfig

=cut

has cachedConfig =>
  (
   is => 'ro',
   lazy => 1,
   clearer => 1,
   isa => sub {pf::IniFiles->isa($_[0])},
   builder => '_buildCachedConfig'
);

has configFile => ( is => 'ro');

has pfconfigNamespace => ( is => 'ro', default => undef);

has default_section => ( is => 'ro');

has importConfigFile => ( is => 'rw');


=head1 METHODS

=head2 validId

validates id

=cut

sub validId { return defined($_[1]) && length($_[1]) > 0 }

=head2 validParam

validate parameter

=cut

sub validParam { 1; }

=head2 _buildCachedConfig

Build the cached pf::IniFiles object

=cut

sub _buildCachedConfig {
    my ($self) = @_;
    return $self->cache->compute(
        $self->configFile,
        {
            expire_if => sub { $self->expire_if(@_) }
        },
        sub {
            return $self->configIniFile();
        }
    );
}

=head2 configIniFile

Non cached pf::IniFiles object

=cut

sub configIniFile {
    my ($self) = @_;
    my %args = $self->configIniFilesArgs();
    my $file_path = $args{'-file'};
    my $file_exists = -e $file_path;
    if (!$file_exists) {
        delete $args{'-file'};
    }

    my $config = eval {pf::IniFiles->new(%args)};
    if ($@) {
        get_logger->error("Error opening $file_path : $@");
        return undef;
    }

    if (!$file_exists) {
        $config->SetFileName($file_path);
    }

    $config->SetLastModTimestamp;
    return $config;
}

sub configIniFilesArgs {
    my ($self) = @_;
    my %args = (-file => $self->configFile, -allowempty => 1);
    my $default_section = $self->default_section;
    if (defined $default_section) {
        $args{'-default'} = $default_section;
    }

    my $importConfigFile = $self->importConfigFile;
    if (defined $importConfigFile && -e $importConfigFile) {
        $args{'-import'} = pf::IniFiles->new(-file => $importConfigFile, -allowempty => 1);
    }

    return %args;
}

sub cache { pf::CHI->new(namespace => 'configfiles'); }

sub expire_if  {
    my ($self, $cached_obj) = @_;
    my $config = $cached_obj->value;
    return 1 unless $config;
    return $config->HasChanged();
}

=head2 rollback

Rollback changes that were made

=cut

sub rollback {
    my ($self) = @_;
    my $file_path = $self->configFile;
    my $cache = $self->cache;
    if ($cache->l1_cache) {
        $cache->l1_cache->remove($file_path);
    }
    $self->clear_cachedConfig;
}

=head2 rewriteConfig

Save the cached config

=cut

sub rewriteConfig {
    my ($self) = @_;
    my $config = $self->cachedConfig;
    $config->removeDefaultValues();
    my $result = $config->RewriteConfig();
    $config->ReadConfig();
    return $result;
}

=head2 readAllIds

Get all the sections names

=cut

sub readAllIds {
    my ($self) = @_;
    my @sections = $self->_Sections();
    return \@sections;
}

=head2 readAll

Get all the sections as an array of hash refs

=cut

sub readAll {
    my ($self, $idKey) = @_;
    return $self->_readAll($idKey, "read");
}

=head2 _readAll

_readAll

=cut

sub _readAll {
    my ($self, $idKey, $method) = @_;
    my $default_section = $self->default_section;
    if (!defined $default_section) {
        return [ map { $self->$method($_, $idKey) } $self->_Sections() ];
    }

    my @sections;
    foreach my $id ($self->_Sections()) {
        my $section = $self->$method($id, $idKey);
        if ( $id eq $default_section ) {
            unshift @sections, $section;
        } else {
            push @sections,$section;
        }
    }

    return \@sections;
}

=head2 readAllWithoutInherited

readAllWithoutInherited

=cut

sub readAllWithoutInherited {
    my ($self, $idKey) = @_;
    return $self->_readAll($idKey, "readWithoutInherited");
}

=head2 _Section

The sections for the configurations

=cut

sub _Sections {
    my ($self) = @_;
    return $self->cachedConfig->Sections();
}

=head2 hasId

If config has a section

=cut

sub hasId {
    my ($self, $id ) = @_;
    my $config = $self->cachedConfig;
    $id = $self->_formatSectionName($id);
    return $config->SectionExists($id);
}

=head2 _formatSectionName

format the id

=cut

sub _formatSectionName { return $_[1]; }

=head2 _cleanupId

Cleanup the id

=cut

sub _cleanupId { return $_[1]; }

=head2 read

reads a section

=cut

sub read {
    my ($self, $id, $idKey) = @_;
    my $data = $self->readRaw($id, $idKey);
    if ($data) {
        $self->cleanupAfterRead($id, $data);
    }
    return $data;
}

=head2 readRaw

reads a section without doing post-read cleanup

=cut

sub readRaw {
    my ($self, $id, $idKey ) = @_;
    return $self->readFromSection($id, $idKey, $self->cachedConfig);
}

=head2 readFromSection

read all parameters from a section

=cut

sub readFromSection {
    my ($self, $id, $idKey, $config) = @_;
    my $section = $self->_formatSectionName($id);
    if ( !$config->SectionExists($section) ) {
        return undef;
    }

    my $data = {};
    $self->populateItem($config, $data, $section, $config->Parameters($section));
    for my $parent ($self->parentSections($id, $data)) {
        $self->populateItem($config, $data, $parent, grep {!exists $data->{$_}} $config->Parameters($parent))
    }

    $data->{$idKey} = $id if defined $idKey;
    return $data;
}

=head2 parentSections

parentSections

=cut

sub parentSections {
    my ($self, $id, $item) = @_;
    my $default_section = $self->default_section;
    if (defined $default_section && length($default_section) && $default_section ne $id) {
        return $default_section;
    }

    return;
}


=head2 populateItem

populateItem

=cut

sub populateItem {
    my ($self, $config, $item, $id, @params) = @_;
    $config->populate($id, $item, @params);
    return ;
}

=head2 readFromImported

read from the parameters from a section

=cut

sub readFromImported {
    my ($self, $id, $idKey) = @_;
    my $config = $self->cachedConfig();
    if ($config->{imported}) {
        return $self->readFromSection($id, $idKey, $config->{imported});
    }

    return {};
}

=head2 update

Update/edit/modify an existing section

=cut

sub update {
    my ($self, $id, $assignments) = @_;
    my $result;
    if ($id ne 'all') {
        my $config = $self->cachedConfig;
        my $real_id = $self->_formatSectionName($id);
        if ( $result = $config->SectionExists($real_id) ) {
            $self->cleanupBeforeCommit($id, $assignments);
            $self->_update_section($real_id, $assignments);
        }
    }
    return $result;
}

sub _update_section {
    my ($self, $section, $assignments) = @_;
    my $config = $self->cachedConfig;
    my @parentSections = $self->parentSections($section, $assignments);
    my $default_section = $parentSections[0];
    my $imported = $config->{imported} if exists $config->{imported};
    my $use_default = $default_section && $section ne $default_section;
    while ( my ($param, $value) = each %$assignments ) {
        my $param_exists = $config->exists($section, $param);
        my $default_value = $config->val($default_section,$param) if ($use_default);
        my $imported_value = $imported->val($section, $param) if $imported;
        if(defined $value ) { #If value is defined the update or add to section
            if ( $param_exists ) {
                #If value is defined the update or add to section
                #Only set the value if not equal to the default value otherwise delete it
                if ((defined $default_value && $default_value eq $value) &&
                    (!defined $imported_value || $imported_value eq $value)) {
                    $config->delval($section, $param);
                } else {
                    $config->setval($section, $param, $value);
                }
            } else {
                #If the value is the same as the default value then do not add
                next if defined $default_value && $default_value eq $value;
                $config->newval($section, $param, $value);
            }
        } else { #Handle deleting param from section
            if ($param_exists) {
                $config->delval($section, $param);
            }
        }
    }
}


=head2 create

To create new section

=cut

sub create {
    my ($self, $id, $assignments) = @_;
    my $config = $self->cachedConfig;
    my $result;
    if ($self->validId($id)) {
        my $real_id = $self->_formatSectionName($id);
        if($result = !$config->SectionExists($id) ) {
            $self->cleanupBeforeCommit($id, $assignments);
            $config->AddSection($real_id);
            $self->_update_section($real_id, $assignments);
        }
    }
    return $result;
}

=head2 update_or_create

=cut

sub update_or_create {
    my ($self, $id, $assignments) = @_;
    if ( $self->hasId($id) ) {
        return $self->update($id, $assignments);
    } else {
        return $self->create($id, $assignments);
    }
}


=head2 remove

Removes an existing item

=cut

sub remove {
    my ($self, $id) = @_;
    my ($msg, $deletable) = $self->canDelete($id);
    if (!$deletable) {
        return $msg, $deletable;
    }

    $self->cleanupBeforeDelete($id);
    my $section = $self->_formatSectionName($id);
    my $deleted = $self->cachedConfig->DeleteSection($section);
    return "Removed", $deleted;
}


=head2 cleanupBeforeDelete

cleanup Before Delete

=cut

sub cleanupBeforeDelete { }


=head2 remove_always

remove_always

=cut

sub remove_always {
    my ($self, $id) = @_;
    my $realSectionName = $self->_formatSectionName($id);
    if (!$self->cachedConfig->SectionExists($realSectionName)) {
        return $FALSE;
    }

    return $self->cachedConfig->DeleteSection($realSectionName);
}


=head2 canDelete

canDelete

=cut

sub canDelete {
    my ($self, $id) = @_;
    my $realSectionName = $self->_formatSectionName($id);
    my $default_section = $self->default_section;
    if ($default_section && $default_section eq $realSectionName) {
        return "Cannot delete a standard item", $FALSE;
    }

    my $import = $self->cachedConfig->{imported};
    if ($import && $import->SectionExists($realSectionName)) {
        return "Cannot delete a standard item", $FALSE;
    }

    return "", $TRUE;
}

=head2 Copy

Copies a section

=cut

sub copy {
    my ($self,$from,$to) = @_;
    my $result;
    if ($self->validId($to)) {
        $result = $self->cachedConfig->CopySection($self->_formatSectionName($from),$self->_formatSectionName($to));
    }
    return $result;
}

=head2 renameItem

=cut

sub renameItem {
    my ( $self, $old, $new ) = @_;
    my $result;
    if ($self->validId($new)) {
        $result = $self->cachedConfig->RenameSection($self->_formatSectionName($old),$self->_formatSectionName($new));
    }
    return $result;
}

=head2 sortItems

Sorting the items

=cut

sub sortItems {
    my ( $self, $sections ) = @_;
    my $default_section = $self->default_section;
    if ($default_section) {
        @$sections = grep { $_ ne $default_section } @$sections;
    }

    return $self->cachedConfig->ResortSections(map { $self->_formatSectionName($_) } @$sections);
}

=head2 cleanupAfterRead

=cut

sub cleanupAfterRead { }

=head2 cleanupBeforeCommit

=cut

sub cleanupBeforeCommit { }

=head2 expand_list

=cut

sub expand_list {
    my ($self, $object, @columns) = @_;
    foreach my $column (@columns) {
        if (exists $object->{$column}) {
            $object->{$column} = [ $self->split_list($object->{$column}) ];
        }
    }
}

sub split_list {
    my ($self, $list) = @_;
    return split(/\s*,\s*/,$list);
}

sub join_list {
    my ($self, @list) = @_;
    return join(',',@list);
}

=head2 flatten_list

=cut

sub flatten_list {
    my ($self, $object, @columns) = @_;
    foreach my $column (@columns) {
        next unless exists $object->{$column};
        my $val = $object->{$column};
        if (ref($val) eq 'ARRAY') {
            $object->{$column} = $self->join_list(@$val);
        }
    }
}

=head2 commit

=cut

sub commit {
    my ($self) = @_;
    my $result;
    my $error;
    eval {
        $result = $self->rewriteConfig();
    };

    if ($@) {
        $error = $@;
        get_logger->error("rewriteConfig: $error");
    }

    if ($result){
        if (pf::config::cluster::increment_config_version()) {
            ($result,$error) = $self->commitPfconfig;
        } else {
            $result = $FALSE;
            $error = "Can't increment configuration version.";
        }
    } else {
        $error //= "Unable to commit changes to file please run '/usr/local/pf/bin/pfcmd fixpermissions' and try again";
        $self->rollback();
    }

    if ($error) {
        get_logger->error($error);
    }

    return ($result, $error);
}

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

    if(defined($self->pfconfigNamespace)){
        if($cluster_enabled){
            eval {
                $self->commitCluster();
            };
            if($@){
                return (undef, "Could not synchronize cluster ($@).");
            }
        }
        else {
            my $manager = pfconfig::manager->new;
            $manager->expire($self->pfconfigNamespace);
            if(pfconfig::git_storage->is_enabled) {
                return $self->commitGitStorage(); 
            }
        }
    }
    else{
        get_logger->warn("Can't expire pfconfig in ".ref($self)." because the pfconfig namespace is not defined.");
    }
    return (1, undef);
}

sub gitStorageConfigFile {
    my ($self) = @_;
    # Strip the prefix of the directory
    my $conf_directory = pfconfig::git_storage->conf_directory;
    my $file = $self->configFile;
    $file =~ s|^$conf_directory||;
    $file =~ s|^/||;
    return "conf/$file";
}

sub commitGitStorage {
    my ($self) = @_;
    my ($res, $msg) = pfconfig::git_storage->commit_file($self->configFile, $self->gitStorageConfigFile);
    if(!$res) {
        return ($res, $msg);
    }
    
    return pfconfig::git_storage->deploy(namespace => $self->pfconfigNamespace, light => 1);
}

sub commitCluster {
    my ($self) = @_;
    my $apiclient = pf::api::jsonrpcclient->new();
    my %data = (
        namespace => $self->pfconfigNamespace,
        conf_file => $self->configFile,
    );
    my $result = $apiclient->call('expire_cluster', %data );
    unless($result){
        get_logger->error("Couldn't contact API to expire the configuration.");
    }
}

=head2 search

=cut

sub search {
    my ($self, $field, $value, $idKey) = @_;
    return unless defined $field && defined $value;
    return $self->filter(
        sub {
            my $i = shift;
            return exists $i->{$field} && defined $i->{$field} && $i->{$field} eq $value;
        },
        $idKey
    );
}

=head2 search_like

search_like

=cut

sub search_like {
    my ($self, $field, $re, $idKey) = @_;
    return unless defined $field && defined $re;
    return $self->filter(
        sub {
            return exists $_[0]->{$field} && defined $_[0]->{$field} && $_[0]->{$field} =~ $re;
        },
        $idKey
    );
}

=head2 filter

$self->filter($method, $idKey = undef)

=cut

sub filter {
    my ($self, $filter, $idKey) = @_;
    return unless defined $filter;
    return grep {my $i = $_; $filter->($i) } @{$self->readAll($idKey)};
}

=head2 filter_offset_limit

$self->filter_offset_limit($method, $offset, $limit, $idKey = undef);

=cut

sub filter_offset_limit {
    my ($self, $filter, $offset, $limit, $idKey) = @_;
    return unless defined $filter;
    return filter_with_offset_limit($filter, $offset, $limit, $self->readAll($idKey));
}

=head2 is_section_in_import

Check if section is in the imported Config::IniFiles

=cut

sub is_section_in_import {
    my ($self, $section) = @_;
    my $ini = $self->cachedConfig;
    my $imported = $ini->{imported};
    return $FALSE unless $imported;
    return $imported->SectionExists($section) ? $TRUE : $FALSE;
}

=head2 readInherited

readInherited

=cut

sub readInherited {
    my ($self, $id, $idKey) = @_;
    my $item = $self->readInheritedRaw($id, $idKey);
    if (!defined $item) {
        return undef;
    }

    $self->cleanupAfterRead($id, $item);
    return $item;
}

=head2 readInheritedRaw

readInheritedRaw

=cut

sub readInheritedRaw {
    my ($self, $id, $idKey) = @_;
    my $item = $self->readWithoutInherited($id, $idKey);
    if (!defined $item) {
        return undef;
    }

    my @parentSections = $self->parentSections($id, $item);
    my $config = $self->cachedConfig;
    my $import = $config->{imported};
    if (@parentSections == 0 && !$import) {
        return undef;
    }

    my $inherited = {};
    for my $parent (@parentSections) {
        $self->populateItem($config, $inherited, $parent, grep {!exists $inherited->{$_}} $config->Parameters($parent));
    }

    if ($import) {
        my $section = $self->_formatSectionName($id);
        $self->populateItem($import, $inherited, $section, grep { !exists $inherited->{$_}} $import->Parameters($section));
    }

    return $inherited;
}

sub flattenCondition {
    my ($self, $item, $key) = @_;
    my $condition = exists $item->{$key} ? $item->{$key} : undef;
    if (!defined $condition) {
        return;
    }

    my $top_op = $condition->{op};
    if ($top_op eq 'and' || $top_op eq 'or' || $top_op eq 'not_and' || $top_op eq 'not_or') {
        if (@{$condition->{values} // []} == 1) {
            $item->{top_op} = $top_op;
        }
    } else {
        $item->{top_op} = undef;
    }

    $item->{$key} = pf::condition_parser::object_to_str($condition);
}


sub expandCondition {
    my ($self, $item, $key) = @_;
    my $condition = exists $item->{$key} ? $item->{$key} : undef;
    if (!defined $condition) {
        return;
    }

    my ($ast, $err) = parse_condition_string($condition);
    $condition = ast_to_object($ast);
    my $top_op = delete $item->{top_op};
    my $op = $condition->{op};
    if ($top_op) {
        if ($top_op eq 'not_and' || $top_op eq 'not_or') {
            if ($op eq 'not' ) {
                $condition->{op} = $top_op;
            }
        } else {
            $condition = { op => $top_op, values => [$condition]};
        }
    } elsif (!exists $TOP_OPS{$op}) {
        if ($op eq 'not') {
            $condition = { op => 'not_and', values => $condition->{values}};
        } else {
            $condition = { op => 'and', values => [$condition]};
        }
    }

    $item->{$key} = $condition;
    return;
}

sub readWithoutInheritedRaw {
    my ($self, $id, $idKey) = @_;
    my $section = $self->_formatSectionName($id);
    my $config = $self->cachedConfig;
    if ( !$config->SectionExists($section) ) {
        return undef;
    }

    my $item = {};
    $self->populateItem($config, $item, $section, $config->MyParameters($section));
    $item->{$idKey} = $id if defined $idKey;
    return $item;
}

sub readWithoutInherited {
    my ($self, $id, $idKey) = @_;
    my $item = $self->readWithoutInheritedRaw($id, $idKey);
    if (!defined $item) {
        return undef;
    }

    $self->cleanupAfterRead($id, $item);
    return $item;
}

sub readDefaults {
    my ($self) = @_;
    my $default_section = $self->default_section; 
    if (!defined $default_section) {
        return undef;
    }
    
    return $self->read($default_section, 'id');
}

sub flatten_to_ordered_array {
    my ($self, $item, $items_key, $item_key) = @_;
    my $items = delete $item->{$items_key} // [];
    my $i = 0;
    for my $j (@$items) {
        $item->{"${item_key}.$i"} = $j;
        $i++;
    }
}

sub expand_ordered_array {
    my ($self, $item, $items_key, $item_key) = @_;
    return pf::util::expand_ordered_array($item, $items_key, $item_key);
}

sub join_list_cr {
    my ($self, @list) = @_;
    return join("\n",@list);
}

=head2 flatten_list_cr

=cut

sub flatten_list_cr {
    my ($self, $object, @columns) = @_;
    foreach my $column (@columns) {
        next unless exists $object->{$column};
        my $val = $object->{$column};
        if (ref($val) eq 'ARRAY') {
            $object->{$column} = $self->join_list_cr(@$val);
        }
    }
}

__PACKAGE__->meta->make_immutable unless $ENV{"PF_SKIP_MAKE_IMMUTABLE"};

=head1 COPYRIGHT

Copyright (C) 2005-2023 Inverse inc.

=head1 LICENSE

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
USA.

=cut

1;
