package Cpanel::UpdateGatherer::modules::Elevate;
#                                      Copyright 2025 WebPros International, LLC
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited.

use strict;
use warnings;

use Cpanel::JSON;

use constant ELEVATE_DIR                 => '/var/cpanel';
use constant ELEVATE_STAGE_FILE          => '/var/cpanel/elevate';
use constant ELEVATE_MANUAL_BLOCKER_FILE => '/var/cpanel/elevate-blockers';
use constant ELEVATE_SECADV_BLOCKER_FILE => '/var/cpanel/elevate-blockers.security-advisor';
use constant ELEVATE_NOC_TOUCH_FILE      => '/var/cpanel/elevate-noc-recommendations';
use constant ELEVATE_ARCHIVE_DIR         => '/var/cpanel/elevate_archive';

# XXX TODO Find a way to poll this from Elevate::OS? How? It is a fatpacked
# script!
my %targets = (
    'CentOS7'     => 'AlmaLinux8',
    'CloudLinux7' => 'CloudLinux8',
    'AlmaLinux8'  => 'AlmaLinux9',
    'CloudLinux8' => 'CloudLinux9',
    'Ubuntu20'    => 'Ubuntu22',
);

# Note to reviewer:
# I tried to avoid "merging" this data, as I think that ultimately would cause
# more problems than that would solve over in BigQuery due to changing the
# schema. Instead, we preserve the operation of current report generation
# while adding the ability to check a similar schema under elevate-archives.
# Ultimately we are keeping the CentOS/CL7 data over in the original schema
# and adding the data for the new elevates over in the added schema.
sub compile {
    my ( $class, $meta ) = @_;

    my $elevate   = _get_elevate_meta_data(ELEVATE_STAGE_FILE) || {};
    my $blockers  = _get_elevate_check_meta_data(
        'blocker' => ELEVATE_MANUAL_BLOCKER_FILE,
        'secadv'  => ELEVATE_SECADV_BLOCKER_FILE,
        'noc'     => ELEVATE_NOC_TOUCH_FILE,
    ) || {};
    my $archives = _get_archives();
    $archives = [] if !$archives || ref $archives ne 'ARRAY';

    my @filtered_archives;
    my @seven_to_eight;
    while( my $elem = shift @$archives ) {
        if( $elem->{'elevate'}{'upgrade_from'} && $elem->{'elevate'}{'upgrade_from'} =~ qr/CentOS7|CloudLinux7/ ) {
            push @seven_to_eight, $elem;
            next;
        }
        push @filtered_archives, $elem;
    }
    push @{$meta->{'elevate-archives'}}, @filtered_archives if @filtered_archives;

    if(@seven_to_eight) {

        $meta->{'elevate-archives'} = $archives if @$archives;

        # Stuff the "current" elevate into the archive
        my %entry;
        if(%$elevate) {
            $elevate->{'upgrade_to'} = $targets{$elevate->{'upgrade_from'}};
            $entry{'elevate'} = $elevate;
        }
        $entry{'elevate-check'} = $blockers if %$blockers;
        push @{$meta->{'elevate-archives'}}, \%entry;

        # Replace with the archival content for ${x}7-to-${x}8.
        # NOTE: It is assumed that elevate succeeded if it is in the archive.
        # Also a key assumption is that you can't do c7->c8 and CL7 -> CL8.
        for (qw{elevate elevate-check}) {
            $meta->{$_} = $seven_to_eight[0]->{$_} if $seven_to_eight[0]->{$_};
        }
    } else {
        $meta->{'elevate'}       = $elevate if %$elevate;
        $meta->{'elevate-check'} = $blockers if %$blockers;
    }

    return 1;
}

sub _get_archives {
    opendir( my $dh, ELEVATE_ARCHIVE_DIR ) or return [];
    my @archives = grep { index( $_, '.' ) != 0 && -d ELEVATE_ARCHIVE_DIR . "/$_" } readdir($dh);
    closedir($dh);
    my @archives_by_action = map {
        my $action = $_;
        my ( $from, $to ) = split( /-to-/, $action );
        my $dir = ELEVATE_ARCHIVE_DIR . "/$action";
        my $data_hr = {};
        my $elevate = _get_elevate_meta_data("$dir/_var_cpanel_elevate") || {};
        $data_hr->{'elevate'} = $elevate if %$elevate;
        $data_hr->{'elevate'}{'upgrade_from'} ||= $from;
        $data_hr->{'elevate'}{'upgrade_to'}     = $to;
        my $blockers = _get_elevate_check_meta_data(
            'blocker' => "$dir/_var_cpanel_elevate-blockers",
            'secadv'  => "$dir/_var_cpanel_elevate-blockers.security-advisor",
            'noc'     => "$dir/_var_cpanel_elevate-noc-recommendations",
        ) || {};
        $data_hr->{'elevate-check'} = $blockers if %$blockers;
        $data_hr;
    } @archives;
    return \@archives_by_action;
}

sub _get_elevate_meta_data {
    my ($stage_file) = @_;

    return {} unless -f $stage_file;
    my $data = eval { Cpanel::JSON::LoadFile($stage_file) } // {};

    return $data unless scalar keys %$data;

    my $elevate_keys = [
        '_elevate_process',
        '_run_once',
        'cloudlinux_database_installed',
        'disabled_cpanel_services',
        'ea4',
        'elevate_version_finish',
        'elevate_version_start',
        'final_notifications',
        'mysql-version',
        'reinstall',
        'stage_number',
        'status',
        'upgrade_from',
        'yum',
    ];
    my $elevate_data = { map { $_ => undef } @$elevate_keys };
    _find_keys_in_data( $elevate_data, $data );

    foreach my $k ( keys %$elevate_data ) {
        delete $elevate_data->{$k} unless defined $elevate_data->{$k};
    }

    return $elevate_data;
}

sub _find_keys_in_data {
    my ( $values, $data ) = @_;

    if ( ref($data) eq 'ARRAY' ) {
        foreach my $a (@$data) {
            _find_keys_in_data( $values, $a );
        }
    }
    elsif ( ref($data) eq 'HASH' ) {
        foreach my $k ( keys %$data ) {
            if ( exists $values->{$k} && !defined $values->{$k} ) {
                $values->{$k} = $data->{$k};
            }
            else {
                _find_keys_in_data( $values, $data->{$k} );
            }
        }
    }

    return;
}

sub _get_elevate_check_meta_data {
    my (%files) = @_;

    my $data         = {};
    my $good_records = 0;
    $data->{'noc-blocker'} = 1 if -f $files{'noc'};
    foreach my $blocker_file ( $files{'blocker'}, $files{'secadv'} ) {
        next unless -e $blocker_file;

        my ( undef, undef, $key ) = File::Spec->splitpath($blocker_file);
        $key =~ s/^elevate-blockers\.?//;
        $key ||= 'manual';

        # Defer validity checking to the consumer of the data:
        $data->{$key}->{'mtime'} = ( stat _ )[9];
        $data->{$key}->{'ctime'} = ( stat _ )[10];

        my $from_json = eval { Cpanel::JSON::LoadFile($blocker_file) } // {};
        $data->{$key}->{'blockers'} = $from_json->{'blockers'} // [];
        $good_records++ unless $@;
        delete $data->{$key} if !$good_records && !$data->{'noc_blocker'};
    }

    return $data;
}

1;
