package App::RoboBot::Config;
$App::RoboBot::Config::VERSION = '4.003';
use v5.20;

use namespace::autoclean;

use Moose;
use MooseX::SetOnce;

use Config::Any::Merge;
use DBD::Pg;
use DBIx::DataStore;
use File::HomeDir;
use Log::Log4perl;
use Log::Log4perl::Appender::Screen;
use Try::Tiny;

use App::RoboBot::NetworkFactory;
use App::RoboBot::Channel;
use App::RoboBot::Nick;

has 'bot' => (
    is       => 'ro',
    isa      => 'App::RoboBot',
    required => 1,
);

has 'config_paths' => (
    is        => 'rw',
    isa       => 'ArrayRef[Str]',
    traits    => [qw( SetOnce )],
    predicate => 'has_config_paths',
);

has 'config' => (
    is        => 'rw',
    isa       => 'HashRef',
    predicate => 'has_config',
);

has 'networks' => (
    is  => 'rw',
    isa => 'HashRef',
);

has 'channels' => (
    is  => 'rw',
    isa => 'ArrayRef[App::RoboBot::Channel]',
);

has 'plugins' => (
    is      => 'rw',
    isa     => 'HashRef',
    default => sub { {} },
);

has 'db' => (
    is => 'rw',
    isa => 'DBIx::DataStore',
    traits => [qw( SetOnce )],
);

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

    my ($logger);

    try {
        unless ($self->has_config) {
            $self->locate_config unless $self->has_config_paths;

            if (my $cfg = Config::Any::Merge->load_files({ files => $self->config_paths, use_ext => 1, override => 1 })) {
                $self->config($cfg);
            } else {
                die "Could not load configuration files: " . join(', ', @{$self->config_paths});
            }
        }

        $self->init_logging;

        $logger = $self->bot->logger('core.config');

        $self->validate_database;
        $logger->debug('Database configuration initialized.');

        $self->validate_globals;
        $logger->debug('Global settings initialized.');

        $self->validate_networks;
        $logger->debug('Network configurations initialized.');

        $self->validate_plugins;
        $logger->debug('Plugin configurations initialized.');

        $self->bot->networks([ values %{$self->networks} ]);
    } catch {
        die "Could not load and validate configuration: $_";
    };

    $logger->debug('All configuration data loaded.');
}

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

    my $home = File::HomeDir->my_home();
    my @exts = qw( conf yml yaml json xml ini );
    my @bases = ("$home/.lispy/lispy.", "$home/.lispy.", "/etc/lispy.");

    my @configs;

    foreach my $base (@bases) {
        foreach my $ext (@exts) {
            push(@configs, $base . $ext);
        }
    }

    my @found;

    CONFIG_FILE:
    foreach my $path (@configs) {
        if (-f $path && -r _) {
            push(@found, $path);
        }
    }

    $self->config_paths([reverse @found]);

    die "Unable to locate a configuration file!" unless $self->has_config_paths;
}

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

    my $log_cfg = $self->config->{'logging'} // {
        'log4j.rootLogger'              => 'INFO, stdout',
        'log4j.appender.stdout'         => 'org.apache.log4j.ConsoleAppender',
        'log4j.appender.stdout.layout'  => 'org.apache.log4j.PatternLayout',
        'log4j.appender.stdout.layout.ConversionPattern' => '%d %5p [%c] %m%n',
    };

    my $config_str = join("\n", map { sprintf('%s=%s', $_, $log_cfg->{$_}) } sort keys %{$log_cfg});

    Log::Log4perl::init( \$config_str );
}

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

    my $logger = $self->bot->logger('core.config.globals');

    my %global = (
        nick => 'lispy',
    );

    $self->config->{'global'} = \%global unless exists $self->config->{'global'};

    foreach my $k (keys %global) {
        $self->config->{'global'}{$k} = $global{$k} unless exists $self->config->{'global'}{$k};
    }

    $logger->debug(sprintf('Global setting %s = %s.', $_, $self->config->{'global'}{$_}))
        for sort keys %{$self->config->{'global'}};

    $self->config->{'global'}{'nick'} = App::RoboBot::Nick->new(
        config => $self,
        name   => $self->config->{'global'}{'nick'}
    );
}

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

    my $logger = $self->bot->logger('core.config.database');

    my %database = (
        name => 'robobot',
    );

    $self->config->{'database'} = \%database unless exists $self->config->{'database'};

    foreach my $k (keys %database) {
        $self->config->{'database'}{$k} = $database{$k} unless exists $self->config->{'database'}{$k};
    }

    if (exists $self->config->{'database'}{'primary'} && ref($self->config->{'database'}{'primary'}) eq 'HASH') {
        $logger->debug('Establishing database connection using explicit configuration hash.');
        $self->db(DBIx::DataStore->new({ config => $self->config->{'database'} })) or die "Could not validate explicit database connection!";
    } else {
        $logger->debug('Establishing database connection using named DataStore definition.');
        $self->db(DBIx::DataStore->new($self->config->{'database'}{'name'})) or die "Could not validate named database connection!";
    }

    $self->bot->migrate_database;
}

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

    my $logger = $self->bot->logger('core.config.networks');

    my @networks;
    my @channels;

    $logger->debug('Creating network factory.');

    my $nfactory = App::RoboBot::NetworkFactory->new(
        bot    => $self->bot,
        config => $self,
        nick   => $self->config->{'global'}{'nick'},
    );

    foreach my $network_name (keys %{$self->config->{'network'}}) {
        $logger->debug(sprintf('Getting configuration data for network %s.', $network_name));
        my $net_cfg = $self->config->{'network'}{$network_name};

        # Do not load (and eventually connect to) the network if the 'enabled'
        # property exists and is set to a falsey value.
        next if exists $self->config->{'network'}{$network_name}{'enabled'}
            && !$self->config->{'network'}{$network_name}{'enabled'};

        $logger->debug(sprintf('Using factory to create network entry for %s.', $network_name));
        push(@networks, $nfactory->create($network_name, $net_cfg));

        my @network_channels;

        # Coerce channel list into an arrayref if only a single channel is
        # listed for this network.
        $net_cfg->{'channel'} = [] unless exists $net_cfg->{'channel'};
        $net_cfg->{'channel'} = [$net_cfg->{'channel'}] if ref($net_cfg->{'channel'}) ne 'ARRAY';

        foreach my $chan_name (@{$net_cfg->{'channel'}}) {
            $logger->debug(sprintf('Adding %s to channel list for network %s.', $chan_name, $network_name));
            push(@network_channels, App::RoboBot::Channel->new( config => $self, network => $networks[-1], name => $chan_name));
            push(@channels, $network_channels[-1]);
        }

        $networks[-1]->channels([@network_channels]);
    }

    $logger->debug('Assigning networks list to bot.');
    $self->networks({ map { $_->name => $_ } @networks });
    $self->channels(\@channels);
}

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

    my $logger = $self->bot->logger('core.config.plugins');

    foreach my $plugin_name (keys %{$self->config->{'plugin'}}) {
        $logger->debug(sprintf('Collecting configuration data for %s plugin.', $plugin_name));
        $self->plugins->{lc($plugin_name)} = $self->config->{'plugin'}{$plugin_name};
    }
}

__PACKAGE__->meta->make_immutable;

1;
