package NGCP::Service::Meta 0.01;

use v5.40;

use Exporter qw(import);
use List::Util qw(any none);
use Data::Dumper;
use YAML::XS;
use NGCP::Log::Functions;
use NGCP::Service::IO;

our @EXPORT = qw(
    cluster_meta
    cluster_get_platform
    cluster_get_ngcp_type
    cluster_get_init_system
    cluster_get_process_handler
    cluster_get_process_handler_pathname
    cluster_get_maintenance

    host_meta
    host_get_platform
    host_get_ngcp_type
    host_get_init_system
    host_get_process_handler
    host_get_process_handler_pathname
    host_get_service_manager
    host_get_maintenance
    host_get_roles
    host_has_role
    host_is_active

    SERV_HA_NONE
    SERV_HA_HOST

    serv_get_config_name
    serv_desc_format
    serv_meta
    serv_meta_with_systemd_props
    serv_get_desc_byname
    serv_get_desc_fallback_byname
    serv_get_state
    serv_in_platform
    serv_in_ngcp_type
    serv_in_role
    serv_in_group
    serv_in_status
    serv_is_managed
    serv_is_ha_managed
    serv_get_exp_ha_status
);

# Support testing env.
my $NGCP_SERVICES = $ENV{NGCP_SERVICES} // '/etc/ngcp-service/nsservices.yml';
my $NGCP_SERVICES_DIR = $ENV{NGCP_SERVICES_DIR} // '/etc/ngcp-service/nsservices.d';
my $NGCP_SERVICES_FRAGS = "$NGCP_SERVICES_DIR/*.yml";
my $NGCP_CONFIG = $ENV{NGCP_CONFIG} // '/etc/ngcp-service/nsconfig.yml';

my %CMD_BASE = (
    sysv => '/usr/sbin/service',
    systemd => '/bin/systemctl',
    monit => '/usr/bin/monit',
);

sub _nsconfig
{
    state $conf = YAML::XS::LoadFile($NGCP_CONFIG);

    return $conf;
}


sub cluster_meta
{
    # XXX: Backwards compatibility for upgrades from old releases. Switch
    # to use exclusively the 'cluster' key on the next LTS release (mr27.0).
    #return _nsconfig()->{cluster};
    my $conf = _nsconfig();

    return $conf->{cluster} if exists $conf->{cluster};
    return $conf->{host};
}

sub cluster_get_platform
{
    return cluster_meta()->{platform};
}

sub cluster_get_ngcp_type
{
    return cluster_meta()->{ngcp_type};
}

sub cluster_get_init_system
{
    return cluster_meta()->{init_system};
}

sub cluster_get_process_handler
{
    return cluster_meta()->{process_handler};
}

sub cluster_get_process_handler_pathname
{
    my $manager = shift;

    return $CMD_BASE{$manager};
}

sub cluster_get_maintenance
{
    return cluster_meta()->{maintenance} // 'no';
}


sub host_meta
{
    my $conf = _nsconfig();
    my $hostname = shift // $conf->{host}{name};

    return unless exists $conf->{cluster}{hosts}{$hostname};
    return $conf->{cluster}{hosts}{$hostname};
}

# XXX: Backwards compatibility, remove in the future.
sub host_get_platform
{
    return cluster_get_platform();
}

# XXX: Backwards compatibility, remove in the future.
sub host_get_ngcp_type
{
    return cluster_get_ngcp_type();
}

# XXX: Backwards compatibility, remove in the future.
sub host_get_init_system
{
    return cluster_get_init_system();
}

# XXX: Backwards compatibility, remove in the future.
sub host_get_process_handler
{
    return cluster_get_process_handler();
}

# XXX: Backwards compatibility, remove in the future.
sub host_get_process_handler_pathname
{
    my $manager = shift;

    return cluster_get_process_handler_pathname($manager);
}

sub _host_get_service_manager
{
    my $process_handler = cluster_get_process_handler();

    my $manager;
    if ($process_handler eq 'none') {
        $manager = 'systemd';
    } elsif ($process_handler eq 'monit') {
        # When using monit as process-handler, we always use systemd services,
        # so that we avoid races, and can do actions on services even if monit
        # itself is not currently running.
        $manager = 'systemd';
        debug("process_handler is monit, using $manager");
    } else {
        $manager = $process_handler;
    }

    if ($manager eq 'systemd') {
        my $pathname = get_root_dir() . '/run/systemd/system/';
        if (not -d $pathname) {
            $manager = 'sysv';
            debug("system is not running systemd, falling back to $manager");
        }
    }

    return $manager;
}

sub host_get_service_manager
{
    state $manager = _host_get_service_manager();

    return $manager;
}

# XXX: Backwards compatibility, remove in the future.
sub host_get_maintenance
{
    return cluster_get_maintenance();
}

sub host_get_roles
{
    my $hostname = shift;
    my $conf = _nsconfig();

    # XXX: Backwards compatibility for upgrades from old releases. Switch to
    # use exclusively the host_meta() and '$host->{roles}' branch on the next
    # LTS release (mr27.0).

    if (exists $conf->{host}{roles}) {
        if (! defined $hostname || $conf->{host}{name} eq $hostname) {
            return $conf->{host}{roles};
        } else {
            return [];
        }
    } else {
        my $host = host_meta($hostname);
        return [] unless $host;
        return $host->{roles};
    }
}

sub host_has_role
{
    my ($host, $role) = @_;

    return 1 if $role eq 'all';
    return any { $_ eq $role } @{host_get_roles($host)};
}

sub host_is_active
{
    state $rc = execute([ '/usr/sbin/ngcp-check-active' ], verbose => 1);

    return $rc == 0;
}


sub serv_get_config_name
{
    return $NGCP_SERVICES;
}

sub serv_desc_format
{
    my $servdesc = shift;

    local $Data::Dumper::Indent = 0;
    local $Data::Dumper::Sortkeys = 1;
    local $Data::Dumper::Varname = 'service';

    return Dumper($servdesc);
}

sub _load_serv_meta
{
    my $serv = YAML::XS::LoadFile($NGCP_SERVICES);

    foreach my $fragname (glob $NGCP_SERVICES_FRAGS) {
        my $frag = YAML::XS::LoadFile($fragname);
        my $frags = $frag->{services};

        @{$serv->{services}}{keys %{$frags}} = values %{$frags};
    }

    return $serv;
}

sub serv_meta
{
    state $serv = _load_serv_meta();

    return $serv->{services};
}

sub _serv_meta_gen_aliases
{
    my $meta = serv_meta();
    my $aliases;

    foreach my $service (keys %{$meta}) {
        my @aliases;

        if (exists $meta->{$service}->{aliases}) {
            push @aliases, @{$meta->{$service}->{aliases}};
        }

        foreach my $prog (qw(systemd sysv monit)) {
            my $alias = $meta->{$service}->{$prog};
            push @aliases, $alias if defined $alias;
        }
        foreach my $alias (@aliases) {
            $aliases->{$alias} = $meta->{$service};
            $aliases->{$alias}->{name} //= $service;
        }
    }

    return $aliases;
}

sub serv_meta_aliases
{
    state $aliases = _serv_meta_gen_aliases();

    return $aliases;
}

sub serv_meta_with_systemd_props
{
    my (%opts) = @_;

    $opts{skip_unhandled_services} //= 0;

    my %services;
    my @services;

    # Filter the services we need to act on.
    foreach my $service (keys %{serv_meta()}) {
        my $servdesc = serv_get_desc_byname($service);
        my $unit_name = $servdesc->{systemd} // $servdesc->{sysv};

        debug("considering service: " . serv_desc_format($servdesc));

        # We cannot handle services w/o a unit name.
        next unless defined $unit_name;

        # Skip any service that is not currently handled on this node.
        if ($opts{skip_unhandled_services}) {
            next unless $servdesc->{enable} eq 'yes';
            next unless serv_in_platform($servdesc);
            next unless serv_in_ngcp_type($servdesc);
            next unless serv_in_role($servdesc);
        }

        $services{$unit_name} = $servdesc;
    }

    # Do a single query to systemd to get the service properties.
    my @prop_opts = '-p' . join ',', qw(Id Type ActiveState);
    my @cmd = ('systemctl', 'show', @prop_opts, sort keys %services);
    my (undef, $stdout, $stderr) = execute(\@cmd);

    foreach my $para (split /\n\n+/, join '', @{$stdout}) {
        my %props = $para =~ m/^([^=]+)=(.*)$/mg;

        next unless defined $props{Id};

        my $servdesc = $services{$props{Id}};

        $props{Type} //= '';
        $props{ActiveState} //= 'unknown';

        $servdesc->{props}{systemd} = { %props };

        push @services, $servdesc;
    }

    return \@services;
}

# Get service definitions with default properties added.
sub serv_get_desc_byname
{
    my ($service) = shift;

    my $servdesc = serv_meta()->{$service} // serv_meta_aliases()->{$service};

    return unless defined $servdesc;

    $servdesc->{name} //= $service;
    $servdesc->{platform} //= 'all';
    $servdesc->{group} //= [ ];
    $servdesc->{node} //= 'all';
    $servdesc->{role} //= [ 'all' ];
    $servdesc->{enable} //= 'yes';
    $servdesc->{ngcp_type} //= [ 'spce', 'sppro', 'carrier' ];

    return $servdesc;
}

sub serv_get_desc_fallback_byname
{
    my $service = shift;
    my $manager = host_get_service_manager();
    my $enabled = 'no';

    if ($manager eq 'systemd') {
        my ($rc, $stdout, undef) = execute([ 'systemctl', 'is-enabled', $service ]);
        my $state = $stdout->[0];
        chomp $state if defined $state;

        if ($rc == 0) {
            $enabled = 'yes';
        } elsif (length $state) {
            $enabled = 'no';
        } else {
            # Service is unknown.
            return;
        }
    } elsif ($manager eq 'sysv') {
        # Is the service known?
        return if not -e "/etc/init.d/$service";

        my $stdout = (execute([ 'runlevel' ]))[1][0];
        chomp $stdout;
        my $runlevel = (split ' ', $stdout)[1];
        my $rclink = glob "/etc/rc${runlevel}.d/S*$service";
        chomp $rclink if defined $rclink;
        $enabled = length $rclink ? 'yes' : 'no';
    } else {
        # Unhandled service manager.
        return;
    }

    my $servdesc = {
        name => $service,
        platform => 'all',
        group => [ ],
        node => 'all',
        role => [ 'all' ],
        enable => $enabled,
        ngcp_type => [ 'spce', 'sppro', 'carrier' ],
        $manager => $service,
    };

    return $servdesc;
}

sub serv_get_state
{
    my ($servdesc, $process_handler, $action) = @_;

    my $servstate = {
        desc => $servdesc,
        unmonitor => 0,
        monitor => 0,
        manager => host_get_service_manager(),
    };

    if ($process_handler eq 'monit') {
        # When using monit as process-handler (but not when acting on monit
        # itself), we need to replicate its internal unmonitor/monitor logic,
        # as we do not call it directly anymore, otherwise monit will generate
        # alerts if the service state changes under its feet. So on
        # start/restart/reload we need to unmonitor then monitor it again, and
        # on stop we just need to unmonitor it.
        if ($servdesc->{name} ne 'monit' and exists $servdesc->{monit}) {
            if (none { $action eq $_ } qw(reset-failed status stop)) {
                $servstate->{monitor} = 1
            }
            if (none { $action eq $_ } qw(reset-failed status)) {
                $servstate->{unmonitor} = 1
            }
        }
    }

    return $servstate;
}

sub serv_in_platform
{
    my $servdesc = shift;

    return 1 if $servdesc->{platform} eq 'all';
    return $servdesc->{platform} eq cluster_get_platform();
}

sub serv_in_ngcp_type
{
    my $servdesc = shift;
    my $host_type = cluster_get_ngcp_type();

    return any { $_ eq $host_type } @{$servdesc->{ngcp_type}};
}

sub serv_in_role
{
    my ($servdesc, $host) = @_;

    return 1 if any { $_ eq 'all' } @{$servdesc->{role}};

    foreach my $role (@{$servdesc->{role}}) {
        return 1 if host_has_role($host, $role);
    }

    return 0;
}

sub serv_in_group
{
    my ($servdesc, $match_group) = @_;

    return 1 unless defined $match_group;

    foreach my $group (@{$servdesc->{group}}) {
        return 1 if $group eq $match_group;
    }

    return 0;
}

sub serv_in_status
{
    my $servdesc = shift;

    my $host_is_active = host_is_active();
    my $status = $host_is_active ? 'active' : 'not active';

    debug("node status: $status");

    return 1 if $servdesc->{node} eq 'all';

    my $serv_on_active = $servdesc->{node} eq 'active';

    return $host_is_active == $serv_on_active;
}

sub serv_is_managed
{
    my $servdesc = shift;

    # If we are on a CE, the service is never managed.
    return 0 if cluster_get_ngcp_type() eq 'spce';

    # If the service should only be run on the active node, it means that
    # HA is managing it, and monit only monitors it.
    return 1 if exists $servdesc->{node} and $servdesc->{node} eq 'active';
    return 0;
}

use constant {
    SERV_HA_NONE => 0,
    SERV_HA_HOST => 1,
};

sub serv_is_ha_managed
{
    my $servdesc = shift;

    # If we are on a CE, the service is never managed.
    return SERV_HA_NONE if cluster_get_ngcp_type() eq 'spce';

    # If the service should only be run on the active HA node,
    # it means that HA is managing it, and monit only monitors it.
    if (exists $servdesc->{node}) {
        return SERV_HA_HOST if $servdesc->{node} eq 'active';
    }
    return SERV_HA_NONE;
}

sub serv_get_exp_ha_status
{
    my ($servdesc, $host_is_active) = @_;
    my $ha_managed = serv_is_ha_managed($servdesc);

    my $exp_status = 'inactive';
    if ($ha_managed == SERV_HA_HOST) {
        $exp_status = $host_is_active ? 'active' : 'inactive';
    } else {
        $exp_status = 'active';
    }

    return $exp_status;
}

1;
