#                                      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.
package Cpanel::Sitejet::Hooks::ChangeDomains;

use cPstrict;
use Cpanel::Imports;

use parent 'Cpanel::Hooks::ChangeDomains';
use constant NAME => 'Sitejet';

=encoding utf-8

=head1 NAME

Cpanel::Sitejet::Hooks::ChangeDomains - Sitejet-specific domain change operations

=head1 DESCRIPTION

This module provides Sitejet-specific domain change operations including website
file migration, metadata updates, and manifest handling. It inherits from
Cpanel::Hooks::ChangeDomains and registers its own hooks independently.

The module handles complex file operations including moving website files,
updating metadata, and ensuring data integrity during domain transitions.

=head1 SYNOPSIS

This module inherits from Cpanel::Hooks::ChangeDomains and registers its own hooks.
It implements the standard service interface:

    my ($result, $message) = Cpanel::Sitejet::Hooks::ChangeDomains->change_domain(
        $user, $old_domain, $new_domain, $context
    );

=head1 STATIC METHODS

=head2 get_instance()

Returns a singleton instance of the Sitejet domain change handler.
This ensures consistent state across multiple hook invocations and provides
service-specific cache isolation.

=over 4

=item B<Returns:>

Singleton instance of Cpanel::Sitejet::Hooks::ChangeDomains with 'sitejet' service identifier

=back

=cut

my $instance = undef;

sub get_instance {
    if ( !$instance ) {
        $instance = __PACKAGE__->new(NAME);
    }
    return $instance;
}

=head2 describe()

Returns an array reference of hook descriptors that define when and how
this module's hooks should be executed by the cPanel hook system.

=over 4

=item B<Parameters:>

None.

=item B<Returns:>

Array reference containing hook configuration hashes for Sitejet domain change operations.

=back

=cut

sub describe {

    # Register Sitejet-specific hooks independently
    return get_instance()->register(NAME);
}

=head2 capture_domain_before_change($context, $data)

Static hook handler that delegates to the singleton instance's parent method.
This is the entry point called by the cPanel hook system.

=cut

sub capture_domain_before_change {
    return get_instance()->SUPER::capture_domain_before_change(@_);
}

=head2 check_for_domain_change($context, $data)

Static hook handler that delegates to the singleton instance's parent method.
This is the entry point called by the cPanel hook system for post-domain-change processing.

=cut

sub check_for_domain_change {
    return get_instance()->SUPER::check_for_domain_change(@_);
}

=head2 uapi_domain_change($context, $data)

Static hook handler that delegates to the singleton instance's parent method.
This is the entry point called by the cPanel hook system for UAPI domain changes.

=cut

sub uapi_domain_change {
    return get_instance()->SUPER::uapi_domain_change(@_);
}

=head1 INSTANCE METHODS

=head2 change_domain($self, $user, $old_domain, $new_domain, $is_whmapi)

Performs Sitejet-specific domain change operations including website migration.

=over 4

=item B<Parameters:>

=over 4

=item * C<$user> - String. The cPanel username.

=item * C<$old_domain> - String. The original domain name.

=item * C<$new_domain> - String. The new domain name.

=item * C<$is_whmapi> - Optional string. When set to 'whmapi', error messages will reference the error log instead of suggesting to contact the hosting provider. Defaults to undef.

=back

=item B<Returns:>

A two-element list: C<($result, $message)>

=over 4

=item * C<$result> - Boolean indicating success (true) or failure (false/undef).

=item * C<$message> - String containing either a success message or error message.

=back

On success, returns the result from the migration operation and a success message.
On failure, returns undef/false and an error message suitable for display to users.

=item B<Side Effects:>

Logs the domain change operation, may switch user context for file operations,
and creates a failure memento YAML file on error for retry operations.

Error message format varies based on the C<$is_whmapi> parameter:
- When C<$is_whmapi> is 'whmapi': Error messages reference the error log for troubleshooting
- When C<$is_whmapi> is undef/other: Error messages suggest contacting the hosting provider

=item B<Examples:>

    # Standard usage (called by framework)
    my ($result, $message) = Cpanel::Sitejet::Hooks::ChangeDomains->change_domain('username', 'old.com', 'new.com');

    # WHM API usage (provides different error messages)
    my ($result, $message) = Cpanel::Sitejet::Hooks::ChangeDomains->change_domain('username', 'old.com', 'new.com', 'whmapi');

=back

=cut

sub change_domain( $self, $user, $old_domain, $new_domain, $is_whmapi = undef ) {
    require Cpanel::AccessIds;
    require Cpanel::PwCache;

    # this needs to be set as Sitejet Connector expects
    # these variables to be defined.
    local $Cpanel::user //= $user;
    local $Cpanel::homedir = Cpanel::PwCache::gethomedir($user);
    my $result;
    my $exception;

    eval {
        if ( _is_user_root() ) {
            $result = Cpanel::AccessIds::do_as_user_with_exception(
                $user,
                sub {
                    _run_actions( $user, $old_domain, $new_domain );
                }
            );
        }
        else {
            $result = _run_actions( $user, $old_domain, $new_domain );
        }
    };

    if ( $result && $result < 0 ) {
        my $msg = locale()->maketext( "No Sitejet website migration needed: either “[_1]” has no Sitejet website to migrate, or “[_2]” already has an existing Sitejet website.", $old_domain, $new_domain );
        logger->info($msg);

        # this is not a failure technically, so lets send a positive result
        return ( 1, $msg );

    }

    if ( ( $exception = $@ ) || !$result ) {
        my $user_error = _fetch_user_display_error($exception);

        # Create failure memento for retry script
        my $memento_file;
        if ( _is_user_root() ) {
            $memento_file = Cpanel::AccessIds::do_as_user_with_exception(
                $user,
                sub {
                    # clean it up incase if it exist
                    _cleanup_lock_file($old_domain);
                    if ( _create_failure_memento( $user, $old_domain, $new_domain, $exception ) ) {
                        return _get_memento_file( $user, $old_domain );
                    }
                }
            );
        }
        else {
            # clean it up incase if it exist
            _cleanup_lock_file($old_domain);

            if ( _create_failure_memento( $user, $old_domain, $new_domain, $exception ) ) {
                $memento_file = _get_memento_file( $user, $old_domain );
            }
        }

        # display the known error message to the user if available
        # suppress the unknown error messages
        my $error_msg;
        if ( defined $is_whmapi && $is_whmapi eq 'whmapi' ) {
            $error_msg =
              $user_error
              ? locale()->maketext( "We could not migrate your Sitejet website from “[_2]” to “[_3]”. Error: “[_1]”. For more information, check the error log: “[_4]”.", $user_error, $old_domain, $new_domain, '/usr/local/cpanel/logs/error_log' )
              : locale()->maketext( "We could not migrate your Sitejet website from “[_1]” to “[_2]”. For more information, check the error log: “[_3]”.", $old_domain, $new_domain, '/usr/local/cpanel/logs/error_log' );
        }
        else {
            $error_msg =
              $user_error
              ? locale()->maketext( "We could not migrate your Sitejet website from “[_2]” to “[_3]”. Error: “[_1]”. Contact your hosting provider for assistance.", $user_error, $old_domain, $new_domain )
              : locale()->maketext( "We could not migrate your Sitejet website from “[_1]” to “[_2]”. Contact your hosting provider for assistance.", $old_domain, $new_domain );
        }

        # Log detailed error for server administrator
        logger->error("We could not migrate '$user''s Sitejet website from '$old_domain' to '$new_domain'. Exception: '$exception'.");

        # Log retry instructions with appropriate format based on memento file availability
        if ( $memento_file && -e $memento_file ) {
            logger->error("To try to migrate your Sitejet website again, run: /var/cpanel/plugins/sitejet/scripts/retry_domain_migration.pl --config $memento_file ");
        }
        else {
            logger->error("To try to migrate your Sitejet website again, run: /var/cpanel/plugins/sitejet/scripts/retry_domain_migration.pl --username $user --old_domain $old_domain --new_domain $new_domain");
        }

        return ( $result, $error_msg );
    }

    my $success_msg = locale()->maketext( "Migrated [_1]’s Sitejet website from “[_2]” to “[_3]”.", $user, $old_domain, $new_domain );
    logger->info($success_msg);

    return ( $result, $success_msg );
}

=head1 PRIVATE FUNCTIONS

=head2 _run_actions($user, $old_domain, $new_domain)

Executes the actual website migration actions with file locking for safety.

=over 4

=item B<Parameters:>

=over 4

=item * C<$user> - String. The cPanel username.

=item * C<$old_domain> - String. The original domain name.

=item * C<$new_domain> - String. The new domain name.

=back

=item B<Returns:>

Boolean result from the C<Cpanel::Sitejet::MoveWebsite> operation.

=item B<Side Effects:>

=over 4

=item * Creates and removes file locks to prevent concurrent operations

=item * Ensures the logs directory exists

=item * Executes the website migration via C<Cpanel::Sitejet::MoveWebsite>

=back

=back

=cut

sub _run_actions( $user, $old_domain, $new_domain ) {
    no warnings 'once';
    require Cpanel::PwCache;
    local $Cpanel::homedir = Cpanel::PwCache::gethomedir($user);
    _verify_and_create_dir_if_not_exist();

    my $lock_file = _get_lock_file_path($old_domain);
    require Cpanel::Plugins::FileLock;
    my $lock_obj = Cpanel::Plugins::FileLock->new($lock_file);
    my $lock;
    if ( !( $lock = $lock_obj->create() ) ) {
        my $error_msg = "Failed to acquire lock for domain migration from $old_domain to $new_domain. Another migration may be in progress. Try again later or contact your hosting provider for assistance.";
        logger->error($error_msg);
        return 0;
    }

    require Cpanel::Sitejet::MoveWebsite;
    my $move_website_obj = Cpanel::Sitejet::MoveWebsite->new( $old_domain, $new_domain );

    my $results;
    $results = $move_website_obj->run();

    # Always clean up the lock
    $lock_obj->remove($lock);

    return $results;
}

=head2 _verify_and_create_dir_if_not_exist()

Ensures that the logs directory exists in the user's home directory.

=over 4

=item B<Parameters:>

None.

=item B<Returns:>

None.

=item B<Side Effects:>

Creates C<$Cpanel::homedir/logs> directory with mode 0710 if it doesn't exist.

Creates C<$Cpanel::homedir/tmp/locks> directory with mode 0700 if it doesn't exist.

=back

=cut

sub _verify_and_create_dir_if_not_exist {
    my $lock_dir = "$Cpanel::homedir/tmp/locks";
    require Cpanel::SafeDir::MK;
    Cpanel::SafeDir::MK::safemkdir_or_die( $lock_dir, '0700' ) if !-d $lock_dir;
    return;
}

=head2 _get_lock_file_path($domain)

Generates the lock file path for a given domain.

=over 4

=item B<Parameters:>

=over 4

=item * C<$domain> - String. The domain name to create a lock file path for.

=back

=item B<Returns:>

String. The full path to the lock file.

=item B<Example:>

    my $lock_file = _get_lock_file_path('example.com');
    # Returns: /home/user/tmp/locks/sitejet_example.com

=back

=cut

sub _get_lock_file_path($domain) {
    return "$Cpanel::homedir/tmp/locks/sitejet_$domain";
}

=head2 _is_user_root()

Checks if the current effective user ID is root (0).

=over 4

=item B<Parameters:>

None.

=item B<Returns:>

Boolean. True if the effective user ID is 0 (root), false otherwise. Even if the process was once root, and dropped privileges down, this will report the current effective status.

=back

=cut

sub _is_user_root() {
    return $> == 0 ? 1 : 0;
}

=head2 _fetch_user_display_error($exception)

Extracts user-friendly error messages from exception strings.

=over 4

=item B<Parameters:>

=over 4

=item * C<$exception> - String. The exception message to parse for user-friendly errors.

=back

=item B<Returns:>

String. A user-friendly error message if a known pattern is found, empty string otherwise.

=item B<Example:>

    my $user_error = _fetch_user_display_error($@);
    # Returns: "Another migration is already in progress. " for lock conflicts
    # Returns: "Insufficient disk space. " for disk space issues
    # Returns: "Permission error occurred. " for permission issues
    # Returns: "" for unknown exceptions

=back

=cut

sub _fetch_user_display_error($exception) {

    # Extract user-friendly error from $exception if available
    my $user_error = '';
    if ($exception) {
        if ( $exception =~ /Failed to acquire lock/ ) {
            $user_error = locale->maketext("The site is being updated by another process. Try again later.");
        }
        elsif ( $exception =~ /No space left on device/ ) {
            $user_error = locale->maketext("Insufficient disk space. You may remove files to free up disk space.");
        }
        elsif ( $exception =~ /Disk quota exceeded/ ) {
            $user_error = locale->maketext("Disk quota exceeded. Remove files to free up disk space.");
        }
        elsif ( $exception =~ /Permission denied/ ) {
            $user_error = locale->maketext("Permission denied.");
        }
    }
    return $user_error;
}

=head2 _create_failure_memento($user, $old_domain, $new_domain, $exception)

Creates or overwrites a failure memento YAML file for retry operations when domain migration fails.
using the retry script (/scripts/retry_domain_migration.pl). If a memento file already exists,
it will be completely overwritten with the new failure information.

=over 4

=item B<Parameters:>

=over 4

=item * C<$user> - String. The cPanel username.

=item * C<$old_domain> - String. The original domain name.

=item * C<$new_domain> - String. The target domain name.

=item * C<$exception> - String. Exception details or error message.

=back

=item B<Returns:>

Boolean indicating success of memento creation (1 on success, 0 on failure).

=item B<File Created:>

C<$Cpanel::homedir/tmp/sitejet_migration_$old_domain.failed.yaml>

    ---
    old_domain: olddomain.com
    new_domain: newdomain.com
    username: testuser
    timestamp: '20250122_143000'
    exception: 'Test exception'
    created_by: Cpanel::Sitejet::Hooks::ChangeDomains

=back

=cut

sub _create_failure_memento( $user, $old_domain, $new_domain, $exception ) {
    require Common::YAML;
    require POSIX;

    my $timestamp    = POSIX::strftime( '%Y%m%d_%H%M%S', localtime() );
    my $failure_file = _get_memento_file( $user, $old_domain );

    # Build memento data
    my $memento = {
        old_domain => $old_domain,
        new_domain => $new_domain,
        username   => $user,
        timestamp  => $timestamp,
        exception  => $exception || '',
        created_by => 'Cpanel::Sitejet::Hooks::ChangeDomains',
    };

    eval {
        Common::YAML::write( $failure_file, $memento );
        logger->info("Created/Updated failure memento: $failure_file");
    };

    if ($@) {
        logger->warn("Failed to create failure memento: '$failure_file': $@");
        return 0;
    }

    return 1;
}

=head2 _get_memento_file($user, $old_domain)

Helper function to generate the standardized path for failure memento files.

=over 4

=item B<Parameters:>

=over 4

=item * C<$user> - String. The cPanel username.

=item * C<$old_domain> - String. The original domain name.

=back

=item B<Returns:>

String containing the full path to the memento file.

=back

=cut

sub _get_memento_file( $user, $old_domain ) {
    require Cpanel::PwCache;
    my $homedir = Cpanel::PwCache::gethomedir($user);
    return "$homedir/tmp/sitejet_migration_$old_domain.failed.yaml";
}

=head2 _cleanup_lock_file($old_domain)

Removes the lock file for a domain migration operation.

This function cleans up the lock file that was created during domain migration
to prevent concurrent migrations of the same domain. It should be called after
a migration completes (successfully or with failure) or when cleaning up after
an error to ensure the lock doesn't persist and block future migrations.

=over 4

=item B<Parameters:>

=over 4

=item * C<$old_domain> - String. The domain name for which to remove the lock file.

=back

=item B<Returns:>

Boolean. Returns the result of the file removal operation:
- True if the lock file was successfully removed or didn't exist
- False if the removal failed

=item B<Side Effects:>

Removes the lock file from the filesystem at the path:
C<$Cpanel::homedir/tmp/locks/sitejet_$old_domain>

=item B<Example:>

    # Clean up lock file after migration
    _cleanup_lock_file('example.com');

    # Clean up in error handling
    eval {
        # migration operations...
    };
    if ($@) {
        _cleanup_lock_file($old_domain);  # Ensure lock is released
        die $@;
    }

=back

=cut

sub _cleanup_lock_file($old_domain) {
    my $lock_file = _get_lock_file_path($old_domain);
    require Cpanel::Plugins::FileLock;
    my $lock_obj = Cpanel::Plugins::FileLock->new($lock_file);
    return $lock_obj->_remove_file();
}

=head1 DEPENDENCIES

=over 4

=item * L<Cpanel::AccessIds> - For user privilege switching

=item * L<Cpanel::PwCache> - For user home directory lookup

=item * L<Cpanel::Plugins::FileLock> - For file locking operations

=item * L<Cpanel::Sitejet::MoveWebsite> - For website migration operations

=item * L<Cpanel::SafeDir::MK> - For safe directory creation

=item * L<Common::YAML> - For failure memento file creation

=back

=head1 ERROR HANDLING

The module includes comprehensive error handling:

=over 4

=item * File locking prevents concurrent operations on the same domain

=item * Failed migrations create memento files for retry operations

=item * User-friendly error messages are extracted from technical exceptions

=item * Detailed logging for server administrators

=back

=head1 LOGGING

The module uses the cPanel logging system to record:

=over 4

=item * Domain migration start and completion

=item * Lock conflicts and warnings

=item * Migration success and failure messages

=item * Retry instructions for failed operations

=back

=head1 SEE ALSO

=over 4

=item * L<Cpanel::Hooks::ChangeDomains> - The generic domain change framework

=item * L<Cpanel::Sitejet::MoveWebsite> - The core website migration module

=item * L<Cpanel::Plugins::FileLock> - File locking functionality

=item * L<Cpanel::AccessIds> - User privilege management

=back

=head1 AUTHOR

WebPros International, LLC

=head1 COPYRIGHT

Copyright 2025 WebPros International, LLC. All rights reserved.

=cut

1;
