#!/usr/local/cpanel/3rdparty/bin/perl

#                                      Copyright 2026 WebPros International, LLC
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited.

package scripts::migrate_pal_to_nova_featurelists;

use cPstrict;

=encoding utf-8

=head1 NAME

migrate_pal_to_nova_featurelists - Migrate standalone-pal to standalone-nova in all feature lists

=head1 USAGE

    migrate_pal_to_nova_featurelists [--stage=install|upgrade] [--verbose]

=head1 DESCRIPTION

This script performs comprehensive migration tasks when transitioning from the
PAL plugin to the Nova plugin. It is automatically executed during Nova plugin
installation via the plugin's lifecycle mechanism.

The script performs the following tasks:

=over 4

=item * B<Marker File Migration>: Copies the PAL plugin's C<modify_featurelist> marker
file (C</var/cpanel/version/cpaneldisable_pal_default>) to Nova's marker file
(C</var/cpanel/version/cpaneldisable_nova_default>) to prevent duplicate execution
of the disable logic during Nova installation.

=item * B<Metadata Cleanup>: Removes PAL's feature metadata file
(C</var/cpanel/features-metadata/standalone-pal.json>) since PAL's uninstall
lifecycle may not run when Nova is installed (due to package upgrade ordering).

=item * B<Feature List Migration>: Scans all feature lists and replaces
C<standalone-pal> with C<standalone-nova>, preserving the enabled/disabled state.

=back

For feature lists containing C<standalone-pal>:

=over 4

=item * Removes C<standalone-pal>

=item * Adds C<standalone-nova> (preserving the original value)

=item * Skips migration if C<standalone-nova> already exists

=back

This script uses a run-once mechanism to ensure it only executes one time per lifecycle stage.
The lock file location depends on the C<--stage> parameter:

=over 4

=item * No C<--stage>: C</var/cpanel/version/cpanelmigrate_pal_to_nova_featurelists>

=item * C<--stage=install>: C</var/cpanel/version/cpanelmigrate_pal_to_nova_featurelists_install>

=item * C<--stage=upgrade>: C</var/cpanel/version/cpanelmigrate_pal_to_nova_featurelists_upgrade>

=back

This allows the migration to run once during initial install (when Nova replaces PAL)
and once during upgrade (as a safety net for edge cases).

=head1 OPTIONS

=over 4

=item B<--stage> STAGE

Specify the lifecycle stage (C<install> or C<upgrade>). This controls which lock file
is used, allowing the migration to run once during install and once during upgrade.
If not specified, uses a shared lock file for backward compatibility.

=item B<--verbose>, B<-v>

Enable verbose output showing which feature lists are being migrated.

=back

=head1 EXAMPLES

=over 4

=item Run migration during install phase

    migrate_pal_to_nova_featurelists --stage=install

=item Run migration during upgrade phase

    migrate_pal_to_nova_featurelists --stage=upgrade

=item Run migration with verbose output

    migrate_pal_to_nova_featurelists --stage=install --verbose

=item Run without stage (backward compatible)

    migrate_pal_to_nova_featurelists

=back

=head1 EXIT STATUS

Returns 0 on success, 1 on failure.

=head1 SEE ALSO

L<modify_featurelist>, L<Cpanel::Features::Load>, L<Cpanel::Features::Write>

=cut

use Cpanel::ConfigFiles          ();
use Cpanel::Features::Load       ();
use Cpanel::Features::Write      ();
use Cpanel::FileUtils::TouchFile ();
use Cpanel::Imports;
use Getopt::Long ();

our $VERSION_DIR = "/var/cpanel/version";
our $VERBOSE     = 0;
our $STAGE       = '';

run() if !caller;

sub run {

    # Parse command line options
    Getopt::Long::GetOptions(
        'verbose|v' => \$VERBOSE,
        'help|h'    => sub { print_usage(); exit(0); },
        'stage=s'   => \$STAGE,
    ) or die "Invalid options\n";

    my $result = do_once(
        version => 'migrate_pal_to_nova_featurelists',
        eol     => 'never',
        code    => \&_perform_migration,
    );

    return $result ? 0 : 1;
}

=head2 _perform_migration()

Performs the actual migration by scanning all feature lists and replacing
standalone-pal with standalone-nova where found.

=head3 Returns

Returns 1 on success, 0 on failure.

=cut

sub _perform_migration {
    _verbose("Starting feature list migration from standalone-pal to standalone-nova");

    # Do not run disable_featurelist code if it was run already for PAL
    _migrate_disable_flag();

    # system update packages installs it automatically so install nova-plugin
    # might run before the install/MigratePalToNova, so lets clean up features metadata
    _cleanup_features_metadata();

    my @feature_lists = _get_featurelists();

    if ( !@feature_lists ) {
        _verbose("No feature lists found");
        return 1;
    }

    _verbose( "Found " . scalar(@feature_lists) . " feature lists to check" );

    my $migrated_count = 0;

    foreach my $feature_list (@feature_lists) {
        my $features = eval { Cpanel::Features::Load::load_featurelist($feature_list) };

        if ( my $error = $@ ) {
            warn "Unable to load feature list '$feature_list': $error\n";
            next;
        }

        # Skip if this feature list doesn't have standalone-pal
        next unless $features && exists $features->{'standalone-pal'};

        my $value = $features->{'standalone-pal'};
        _verbose("Migrating feature list: $feature_list");

        # Perform the migration
        # Remove standalone-pal and add standalone-nova with the same value(preserving enabled/disabled state)
        # Only add standalone-nova if it doesn't already exist to avoid overwriting manual changes
        delete $features->{'standalone-pal'};
        $features->{'standalone-nova'} = $value if !exists $features->{'standalone-nova'};

        eval { Cpanel::Features::Write::write_featurelist( $feature_list, $features ); };

        if ( my $error = $@ ) {
            warn "Failed to write feature list '$feature_list': $error\n";
            next;
        }

        $migrated_count++;
        _verbose("  ✓ Removed standalone-pal, added standalone-nova");
    }

    my $message = "Migration complete. Updated $migrated_count feature list(s).";
    say $message if $VERBOSE;
    _verbose($message);

    return 1;
}

=head2 _get_featurelists()

Returns a list of all feature lists on the system, excluding the 'disabled' list.

=head3 Returns

Array of feature list names.

=cut

sub _get_featurelists {
    opendir( my $dh, $Cpanel::ConfigFiles::FEATURES_DIR ) || do {
        warn "Cannot open directory: $Cpanel::ConfigFiles::FEATURES_DIR: $!";
        return;
    };

    my @feature_lists = grep { !/^(\.\.?|disabled)$/n } readdir($dh);
    closedir($dh) or die "Failed to close features directory: $!";

    return @feature_lists;
}

=head2 do_once(%opts)

Creates a touch file to track whether a task has been done.
The task is executed and as long as the touch file exists,
it will not do it again.

This follows the same pattern used by L<modify_featurelist>.

=head3 Parameters

=over 4

=item version - string - The version identifier used to create the lock file name

=item eol - string - End of life marker (typically 'never' for migrations)

=item code - coderef - The subroutine to execute once

=back

=head3 Returns

The return value from the executed code, or undef if already executed or on error.

=cut

sub do_once (%opts) {
    return unless $opts{version} && $opts{code} && ref $opts{code} eq 'CODE';

    my $lock = _lock_name(%opts);

    if ( -e $lock ) {
        _verbose("Migration already completed (lock file exists: $lock)");
        return 1;
    }

    my $ret = eval { $opts{code}->(); };

    if ( my $error = $@ ) {
        warn("Error during migration: $error");
        return;
    }

    # If the code executed successfully, create the marker file to prevent future runs
    _mark_did_once(%opts);

    return $ret;
}

sub _mark_did_once (%opts) {
    my $lock = _lock_name(%opts);

    if ( !Cpanel::FileUtils::TouchFile::touchfile($lock) ) {
        warn("Failed to create migration lock file: $lock");
    }

    _verbose("Created migration lock file: $lock");

    return 1;
}

sub _lock_name (%opts) {
    my $suffix = $STAGE ? "_$STAGE" : '';
    return $VERSION_DIR . '/cpanel' . $opts{version} . $suffix;
}

sub _verbose ($message) {
    return unless $VERBOSE;
    say "[VERBOSE] $message";
}

sub print_usage {
    say "Usage: $0 [--stage=install|upgrade] [--verbose]";
    say "";
    say "Options:";
    say "  --stage         Lifecycle stage to run (install or upgrade)";
    say "  --verbose, -v   Enable verbose output";
    say "  --help, -h      Show this help message";
    return;
}

sub _cleanup_features_metadata {

    # Clean up PAL's feature metadata since PAL's uninstall lifecycle might not run
    # (cpanel-plugin-common gets upgraded before PAL is removed)
    my $pal_metadata_file = '/var/cpanel/features-metadata/standalone-pal.json';
    if ( -e $pal_metadata_file ) {
        require Cpanel::Autodie;
        _verbose("Removing PAL feature metadata file: $pal_metadata_file");
        eval { Cpanel::Autodie::unlink_if_exists($pal_metadata_file); };
        if ( my $err = $@ ) {
            warn("Failed to remove $pal_metadata_file: $@");
        }

        # Clear the metadata cache
        require Cpanel::Features::MetadataList;
        Cpanel::Features::MetadataList::clear_cache();
    }
    return;
}

sub _migrate_disable_flag {
    my $pal_marker_file  = '/var/cpanel/version/cpaneldisable_pal_default';
    my $nova_marker_file = '/var/cpanel/version/cpaneldisable_nova_default';

    # If PAL's modify_featurelist already ran, touch Nova's marker file to prevent
    # it from running again during Nova installation (since the feature list was
    # already modified when PAL was installed)
    if ( -e $pal_marker_file && !-e $nova_marker_file ) {
        require Cpanel::FileUtils::TouchFile;
        Cpanel::FileUtils::TouchFile::touchfile($nova_marker_file);
        _verbose("Created Nova marker file ($nova_marker_file) to skip duplicate modify_featurelist execution.");
    }
}

1;
