package Cpanel::Sitejet::MoveWebsite;

# cpanel - Cpanel/Sitejet/MoveWebsite.pm           Copyright 2025 cPanel, L.L.C.
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

use cPstrict;
use Cpanel::Autodie;
use Cpanel::FileUtils::Write;

use Cpanel::Imports;    # includes logger, locale, and maketext

=encoding utf-8

=head1 NAME

Cpanel::Sitejet::MoveWebsite

=head1 DESCRIPTION

This module handles transfer of website files from one domain to
another. It includes transfer of sitejet metadata, sitejet manifest
& all website related files in the document root.

=cut

=head1 METHODS

=over

=item * new -- Create MoveWebsite object.

    ARGUMENTS
        None

    RETURNS
        Cpanel::Sitejet::MoveWebsite object

    ERRORS
        None

    EXAMPLE
        my $mov_obj = Cpanel::Sitejet::MoveWebsite::new();

=back

=cut

sub new {
    my $class = shift;

    my ( $old_domain, $new_domain ) = @_;

    require Cpanel::Sitejet::Connector;
    my $connector = Cpanel::Sitejet::Connector->new();
    my $self      = {
        old_domain => $old_domain,
        new_domain => $new_domain,
        connector  => $connector,
    };

    return bless $self, $class;
}

=head1 METHODS

=over

=item * run -- Run the move website process.

    This method runs the move website process. It validates if the sitejet metadata exists just for old domains,
    moves the sitejet manifest data, moves the sitejet metadata, updates the new domain metadata
    and replaces the old domain with the new domain in all relevant files.
    This is the main method that is called to perform the move website process.

    ARGUMENTS
        none

    RETURNS:
        1 on success (migration completed successfully)
        0 on failure (migration encountered errors during execution)
        -1 when no migration is needed (old domain has no Sitejet metadata OR new domain already has Sitejet metadata)

    ERRORS
        None.

    EXAMPLE
        my $move_website_obj = new Cpanel::Sitejet::MoveWebsite( 'old-domain.com', 'new-domain.com' );
        my $result = $move_website_obj->run();
        if ($result == 1) {
            # Migration completed successfully
        } elsif ($result == 0) {
            # Migration failed with errors
        } elsif ($result == -1) {
            # No migration needed - nothing to migrate or target already exists
        }

=back

=cut

sub run($self) {

    if ( !$self->validate_if_sitejet_metadata_exist() ) {
        return -1;
    }

    my $old_domain      = $self->{old_domain};
    my $new_domain      = $self->{new_domain};
    my $overall_success = 1;

    # Update website assets in sitejet CMS if they contain old domain references
    # should continue even if it fails
    my $ret = $self->_do_action(
        sub {
            $self->update_website_assets_domain();
        },
        "Update website assets domain references from `$old_domain` to `$new_domain`."
    );

    $ret = $self->_do_action(
        sub {
            $self->move_metadata();
        },
        "Move metadata from `$old_domain` to `$new_domain`."
    );

    return $ret if !$ret;

    # Move manifest data
    $ret = $self->_do_action(
        sub {
            $self->move_manifest_data();
        },
        "Moving Manifest data from `$old_domain` to `$new_domain`."
    );

    return $ret if !$ret;

    # Replace old domain in files
    $ret = $self->_do_action(
        sub {
            $self->replace_old_domain_in_files();
        },
        "Replace old domain: `$old_domain` references in sitejet website with new domain: `$new_domain`."
    );

    # clean up memento file if it exist and overall process is a success
    $self->_clean_up_failure_memento() if $ret;

    return $ret;

}

=head1 METHODS

=over

=item * validate_if_sitejet_metadata_exist -- Validate if sitejet metadata exists for the old domain.

    Validates if the sitejet metadata exists for the old domain and does not exist for the new domain.
    We will proceed with the migration only if the old domain has sitejet metadata and the new domain does not.
    This is necessary to ensure that we do not overwrite any existing sitejet metadata, manifest data or files
    that may already exist for the new domain.

    ARGUMENTS
        none

    RETURNS:
        1 on success, 0 on failure.

    ERRORS
        None.

    EXAMPLE
        my $move_website_obj = new Cpanel::Sitejet::MoveWebsite( 'old-domain.com', 'new-domain.com' );
        $move_website_obj->validate_if_sitejet_metadata_exist();

=back

=cut

sub validate_if_sitejet_metadata_exist($self) {

    # To account for re-tries, look at the memento_file so we can continue with
    # rest of the process
    my $memento_file = $self->_get_memento_file();
    return 1 if -e $memento_file;

    my @warnings;

    local $SIG{'__WARN__'} = sub { push @warnings, $_[0] };
    my $new_domain_metadata = $self->{connector}->cp_load_sitejet_metadata( $self->{new_domain} );

    my $old_domain_metadata = $self->{connector}->cp_load_sitejet_metadata( $self->{old_domain} );
    if (@warnings) {
        die "Cannot load Sitejet metadata: " . join( "\n", @warnings );
    }

    if ( exists $new_domain_metadata->{domain} && $new_domain_metadata->{domain} ) {
        logger->error( sprintf( "The new domain “%s” already has Sitejet metadata.", $self->{new_domain} ) );
        return 0;
    }
    if ( !exists $old_domain_metadata->{domain} || !$old_domain_metadata->{domain} ) {
        logger->error( sprintf( "The old domain “%s” does not include Sitejet metadata. Nothing to migrate.", $self->{old_domain} ) );
        return 0;
    }

    return 1;

}

=head1 METHODS

=over

=item * move_manifest_data -- Move the sitejet manifest data from the old domain to the new domain.

    Moves the sitejet manifest file from the old domain to the new domain.
    The manifest file is located at '$Cpanel::homedir/.cpanel/sitejet/<domain>-files'.

    ARGUMENTS
        none

    RETURNS:
        1 on success, 0 on failure.

    ERRORS
        None.

    EXAMPLE
        my $move_website_obj = new Cpanel::Sitejet::MoveWebsite( 'old-domain.com', 'new-domain.com' );
        $move_website_obj->move_manifest_data();

=back

=cut

sub move_manifest_data($self) {

    my $old_domain      = $self->{old_domain};
    my $new_domain      = $self->{new_domain};
    my $source_manifest = "$Cpanel::homedir/.cpanel/sitejet/$old_domain-files";

    # nothing to do if source domain does not have a manifest
    return 1 if !-e $source_manifest;
    my $dest_manifest = "$Cpanel::homedir/.cpanel/sitejet/$new_domain-files";
    return _move_file( $source_manifest, $dest_manifest );
}

=head1 METHODS

=over

=item * move_metadata -- Move the sitejet metadata from the old domain to the new domain.

    Moves the sitejet metadata file from the old domain to the new domain and updates the metadata fields.
    The metadata file is located at '$Cpanel::homedir/.cpanel/sitejet/<domain>'.
    After successfully moving the metadata file, this method also calls update_new_metadata() to update
    the domain, company, and cPanelDomainGUID fields in the new domain's metadata to match the new domain name.

    ARGUMENTS
        none

    RETURNS:
        1 on success, 0 on failure.

    ERRORS
        None.

    EXAMPLE
        my $move_website_obj = new Cpanel::Sitejet::MoveWebsite( 'old-domain.com', 'new-domain.com' );
        $move_website_obj->move_metadata();

=back

=cut

sub move_metadata($self) {
    my $old_domain      = $self->{old_domain};
    my $new_domain      = $self->{new_domain};
    my $source_metadata = "$Cpanel::homedir/.cpanel/sitejet/$old_domain";
    my $dest_metadata   = "$Cpanel::homedir/.cpanel/sitejet/$new_domain";

    # nothing to do if source domain does not have a metadata
    return 1 if !-e $source_metadata;
    if ( _move_file( $source_metadata, $dest_metadata ) ) {

        # now update metadata
        return $self->update_new_metadata();
    }
    return 0;
}

=head1 METHODS

=over

=item * update_new_metadata -- Update selected metadata fields for the new domain.

    Updates the sitejet metadata file for the new domain.
    The domain, company & cPanelDomainGUID fields are updated in the metadata.
    This is necessary to ensure that the new domain has the correct metadata.
    The metadata file is located at '$Cpanel::homedir/.cpanel/sitejet/<domain>'.

    ARGUMENTS
        none

    RETURNS:
        1 on success, 0 on failure.

    ERRORS
        None.

    EXAMPLE
        my $move_website_obj = new Cpanel::Sitejet::MoveWebsite( 'old-domain.com', 'new-domain.com' );
        $move_website_obj->update_new_metadata();

=back

=cut

sub update_new_metadata($self) {
    my $new_domain          = $self->{new_domain};
    my $new_domain_metadata = $self->{connector}->cp_load_sitejet_metadata( $self->{new_domain} );
    $new_domain_metadata->{domain}           = $new_domain;
    $new_domain_metadata->{cpanelDomainGUID} = $new_domain;
    $new_domain_metadata->{company}          = $new_domain;

    $self->{connector}->cp_update_sitejet_metadata( $new_domain, $new_domain_metadata );
    return 1;

}

=head1 METHODS

=over

=item * update_website_assets_domain -- Update website assets domain references if they match the original domain.

    Updates website asset details through the Sitejet API if the assets contain references
    to the original domain. This ensures that when domains are changed, asset details are
    updated only if they haven't been customized by the user (i.e., they still contain
    the old domain references).

    Fields are updated if they contain the old domain.

    ARGUMENTS
        none

    RETURNS:
        1 on success, 0 on failure.

    ERRORS
        None.

    EXAMPLE
        my $move_website_obj = new Cpanel::Sitejet::MoveWebsite( 'old-domain.com', 'new-domain.com' );
        $move_website_obj->update_website_assets_domain();

=back

=cut

sub update_website_assets_domain($self) {
    my $old_domain = $self->{old_domain};
    my $new_domain = $self->{new_domain};

    # since this is the first action, retry script might throw
    # exceptions if metadata is moved already, but it will not fail the process
    eval {
        # Get current website assets from Sitejet API
        # Only update assets if they contain old domain references that haven't been customized
        # Special handling for customer.company field to match metadata update behavior
        my $assets = $self->{connector}->getWebsiteAssets($old_domain);

        if ( !$assets ) {
            logger->info("No assets found for domain: $old_domain");
            return 1;
        }

        my $needs_update   = 0;
        my $updated_assets = {};

        # Check if any asset fields contain the old domain and update them
        # The assets structure is a hash with customer.* fields directly at the top level
        for my $key ( keys %$assets ) {
            my $value = $assets->{$key};

            # Skip non-string values
            next unless defined $value && !ref $value;

            # Check if the value contains the old domain
            if ( $value =~ /\Q$old_domain\E/ ) {
                my $new_value = $value;
                $new_value =~ s/\Q$old_domain\E/$new_domain/g;
                $updated_assets->{$key} = $new_value;
                $needs_update++;
                logger->info("Updated asset field '$key' from '$value' to '$new_value'");
            }
        }

        # Update assets via API if any changes were made
        if ($needs_update) {
            $self->{connector}->updateWebsiteAssets( $old_domain, $updated_assets );
            logger->info("Successfully updated website assets for domain: $old_domain -> $new_domain");
        }
        else {
            logger->info("No asset updates needed for domain: $old_domain");
        }
    };

    # Don't fail the entire migration for asset update failures
    logger->warn("Failed to update website assets for domain $old_domain: $@") if $@;

    return 1;
}

=head1 METHODS

=over

=item * replace_old_domain_in_files -- Replace old domain with new domain in all relevant files.

    Replaces the old domain with the new domain in all relevant files.
    Look through the sitejet manifest files and replace the old domain with the new domain.
    This is necessary to skip any non-sitejet files that may have the old domain in them.
    This will improve efficiency and avoid unnecessary changes to files that are not related to sitejet.

    ARGUMENTS
        none

    RETURNS:
        1 on success, 0 on failure.

    ERRORS
        None.

    EXAMPLE
        my $move_website_obj = new Cpanel::Sitejet::MoveWebsite( 'old-domain.com', 'new-domain.com' );
        $move_website_obj->replace_old_domain_in_files();

=back

=cut

sub replace_old_domain_in_files($self) {
    my $old_domain = $self->{old_domain};
    my $new_domain = $self->{new_domain};
    require File::Find::Rule;
    my $rule           = File::Find::Rule->new;
    my @manifest_files = $self->{connector}->list_manifest_files($new_domain);
    $rule->or(
        File::Find::Rule->binary->prune->discard(),
        File::Find::Rule->exec(
            sub {
                # $_[2] is the complete filename
                Cpanel::Autodie::open( my $fh, '<', "$_[2]" );
                my $data;
                my $match_found = 0;
                while ( my $line = <$fh> ) {
                    if ( $line =~ /$old_domain/ ) {
                        $line =~ s/$old_domain/$new_domain/g;
                        $match_found = 1;
                    }
                    $data .= $line;
                }
                close $fh;
                if ($match_found) {

                    Cpanel::FileUtils::Write::overwrite( "$_[2]", $data, 0644 );
                    return 1;
                }
                else {
                    return;
                }

            },
            [ sub { 1 } ]
        ),

    );

    my @modified_files = $rule->file()->in(@manifest_files);
    logger->info( sprintf( "Following files are modified: %s", join( "\n", @modified_files ) ) );

    return 1;

}

sub _move_file( $src, $dest ) {
    require Cpanel::FileUtils::Move;
    return Cpanel::FileUtils::Move::safemv( $src, $dest );
}

sub _do_action( $self, $action, $desc ) {
    my $ret;
    if ( ref $action eq 'CODE' ) {
        $ret = $action->();
    }

    logger->info( sprintf( "action: “%s”, status: “%s”", $desc, $ret ) );
    return $ret;
}

# TODO: This is repeated, will move it to common space
sub _get_memento_file($self) {
    return "$Cpanel::homedir/tmp/sitejet_migration_$self->{old_domain}.failed.yaml";
}

=head2 _clean_up_failure_memento

Removes the failure memento file after a successful domain migration operation.
This cleanup prevents stale memento files from accumulating in the user's tmp directory
and ensures that only current failure states are preserved for retry operations.

The memento file is used to track failed migration attempts and contains information
needed for retry operations. Once a migration completes successfully, the memento
file is no longer needed and should be removed.

=over 4

=item B<Arguments:>

None.

=item B<Returns:>

None. The method uses C<Cpanel::Autodie::unlink_if_exists> which safely removes
the file if it exists, or does nothing if the file doesn't exist.

=item B<Side Effects:>

Removes the memento file from the user's tmp directory at:
C<$Cpanel::homedir/tmp/sitejet_migration_$old_domain.failed.yaml>

=back

=cut

sub _clean_up_failure_memento($self) {
    require Cpanel::Autodie;
    eval { Cpanel::Autodie::unlink_if_exists( $self->_get_memento_file() ); };

    # we don't die here as it is best effort cleanup.
    logger->error("Failed to remove memento file: '$@'") if $@;
    return;
}

1;
