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

package scripts::verify_wptk_userdata;

#                                      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.

=encoding utf-8

=head1 NAME

verify_wptk_userdata - Verify WP Toolkit userdata against WPTK instance data

=head1 SYNOPSIS

    scripts/verify_wptk_userdata [OPTIONS]

    Options:
        --help               Display this help message
        --silent-check       No output, exit code indicates status (mutually exclusive with --repair)
        --repair             Make changes to fix discrepancies (mutually exclusive with --silent-check)
        --no-auto-register   Do not attempt to register missing domains in WPTK during repair
        --allow-remove       Allow removal of wptkid entries for domains missing from WPTK
        --user=USERNAME      Verify only the specified user (can be specified multiple times, mutually exclusive with --skip-user)
        --skip-user=USERNAME Skip the specified user (can be specified multiple times, mutually exclusive with --user)

=head1 DESCRIPTION

This script verifies WP Toolkit data in userdata against the WPTK instance data.

By default, this script reports discrepancies but does not make any changes.
Use the C<--repair> option to apply fixes.

When C<--repair> is used, the script will attempt to auto-register domains that
exist in userdata with a WPTK ID but are missing from WP Toolkit. After
registration, the WPTK instance list is refreshed and the wptkid in userdata is
updated to match the newly registered instance. Use C<--no-auto-register> to
disable this behavior.

By default, wptkid entries for domains that are missing from WP Toolkit are
preserved in userdata. Use C<--allow-remove> to remove these entries as a last
resort after investigating the cause.

Use the C<--silent-check> option for scripted checks - the exit code will be
non-zero if any discrepancies are found.

=head1 EXIT CODES

=over 4

=item 0 - No discrepancies found

=item 1 - Discrepancies were found

=back

=cut

use cPstrict;

use parent qw( Cpanel::HelpfulScript );

use Cpanel::Config::Users              ();
use Cpanel::WPTK::Util::VerifyUserdata ();

use Try::Tiny;

use constant _OPTIONS => (
    'silent-check',
    'repair',
    'no-auto-register',
    'allow-remove',
    'user=s@',
    'skip-user=s@',
);

use constant EXIT_NO_PROBLEMS    => 0;
use constant EXIT_PROBLEMS_FOUND => 1;

if ( !caller ) {
    exit( __PACKAGE__->new(@ARGV)->run() );
}

sub run ($self) {

    $self->ensure_root();

    my $options = $self->_parse_and_validate_options();

    $self->_print_startup_message( $options->{repair} );

    my @users    = $self->_get_users_to_verify($options);
    my $verifier = $self->_create_verifier($options);
    my $result   = $self->_verify_all_users( $verifier, \@users, $options->{silent_check} );

    $self->_print_final_report( $result, $options ) unless $options->{silent_check};

    my $has_problems = keys $result->{users}->%* || scalar $result->{errors}->@*;
    return $has_problems ? EXIT_PROBLEMS_FOUND : EXIT_NO_PROBLEMS;
}

sub _parse_and_validate_options ($self) {
    my $silent_check     = $self->getopt('silent-check');
    my $repair           = $self->getopt('repair');
    my $no_auto_register = $self->getopt('no-auto-register');
    my $allow_remove     = $self->getopt('allow-remove');
    my $user_opt         = $self->getopt('user')      // [];
    my $skip_user_opt    = $self->getopt('skip-user') // [];

    if ( $silent_check && $repair ) {
        die $self->help("--silent-check and --repair are mutually exclusive options.\n");
    }

    if ( $user_opt->@* && $skip_user_opt->@* ) {
        die $self->help("--user and --skip-user are mutually exclusive options.\n");
    }

    return {
        silent_check     => $silent_check,
        repair           => $repair,
        no_auto_register => $no_auto_register,
        allow_remove     => $allow_remove,
        user_opt         => $user_opt,
        skip_user_opt    => $skip_user_opt,
    };
}

sub _print_startup_message ( $self, $repair ) {
    $self->_out('Starting verify_wptk_userdata...');

    if ( !$repair ) {
        $self->_out('Verification mode (read-only). Use --repair to apply fixes.');
    }

    return;
}

sub _get_users_to_verify ( $self, $options ) {
    return $options->{user_opt}->@* if $options->{user_opt}->@*;

    my %skip_users = map       { $_ => 1 } $options->{skip_user_opt}->@*;
    my @users      = sort grep { !$skip_users{$_} } Cpanel::Config::Users::getcpusers();
    return @users;
}

sub _create_verifier ( $self, $options ) {
    return Cpanel::WPTK::Util::VerifyUserdata->new(
        output           => $options->{silent_check}     ? undef : $self->get_output_object(),
        quiet            => $options->{silent_check}     ? 1     : 0,
        repair           => $options->{repair}           ? 1     : 0,
        allow_remove     => $options->{allow_remove}     ? 1     : 0,
        no_auto_register => $options->{no_auto_register} ? 1     : 0,
    );
}

sub _verify_all_users ( $self, $verifier, $users, $silent_check ) {
    my $result = {
        users  => {},
        errors => [],
    };

    for my $username ( $users->@* ) {
        my $user_result = $self->_verify_single_user( $verifier, $username, $result, $silent_check );

        next unless $self->_has_issues($user_result);

        $result->{users}{$username} = $user_result;
        $self->_out('-');
    }

    return $result;
}

sub _verify_single_user ( $self, $verifier, $username, $result, $silent_check ) {
    my $user_result;

    try {
        $user_result = $verifier->verify_wptk_userdata($username);
    }
    catch {
        my $err = $_;
        my $msg = "Error processing user '$username': $err";
        push $result->{errors}->@*, $msg;
        $self->_warn($msg) if !$silent_check;
    };

    return $user_result;
}

sub _has_issues ( $self, $user_result ) {
    return 0 unless defined $user_result;
    return scalar $user_result->@*;
}

sub _print_final_report ( $self, $result, $options ) {
    my %totals = $self->_report_results($result);

    $self->_print_missing_warning( \%totals, $options );

    if ( $options->{repair} ) {
        $self->_out('Repair completed.');
    }
    else {
        $self->_out('Verification (read-only) completed. Use --repair to apply fixes.');
    }

    return;
}

sub _report_results ( $self, $result ) {

    my $users  = $result->{users};
    my $errors = $result->{errors} // [];

    if ( !keys $users->%* && !scalar $errors->@* ) {
        $self->_out('No actionable discrepancies found between WPTK and userdata.');
        return ();
    }

    $self->_print_summary_header();

    my %totals = $self->_report_user_discrepancies($users);
    $self->_report_errors($errors);
    $self->_print_totals( \%totals, $errors );

    return %totals;
}

sub _print_summary_header ($self) {
    $self->_out('');
    $self->_out('====================');
    $self->_out('Summary');
    $self->_out('====================');
    return;
}

sub _report_user_discrepancies ( $self, $users ) {
    my %totals;

    for my $username ( sort keys $users->%* ) {
        my $discrepancies = $users->{$username};

        $self->_out("User: $username");

        my $user_indent = $self->_indent_guard();

        for my $d ( $discrepancies->@* ) {
            $self->_out("[$d->{type}] $d->{message}");
            $totals{ $d->{type} }++;
        }
    }

    return %totals;
}

sub _print_missing_warning ( $self, $totals, $options ) {
    my $missing_count = ( $totals->{ Cpanel::WPTK::Util::VerifyUserdata::TYPE_MISSING() } // 0 ) + ( $totals->{ Cpanel::WPTK::Util::VerifyUserdata::TYPE_BAD_OWNER() } // 0 );
    return unless $missing_count > 0;
    return if $options->{allow_remove};

    $self->_out('');

    if ( !$options->{repair} ) {
        $self->_warn( "WARNING: $missing_count domain(s) exist in userdata with a WPTK ID but are missing " . "from WP Toolkit. Run with --repair to attempt auto-registration of " . "these domains. If auto-registration fails, investigate why the domains " . "are not registered. The --allow-remove option can be used as a last " . "resort to remove these entries from userdata." );
    }
    else {
        $self->_warn( "WARNING: $missing_count domain(s) exist in userdata with a WPTK ID but are still " . "missing from WP Toolkit after auto-registration. Investigate these " . "domains to determine why they cannot be registered. The --allow-remove " . "option can be used as a last resort to remove these entries from userdata." );
    }

    return;
}

sub _report_errors ( $self, $errors ) {
    return unless scalar $errors->@*;

    $self->_out('');
    $self->_out('Errors encountered:');

    my $errors_indent = $self->_indent_guard();
    for my $error ( $errors->@* ) {
        $self->_out($error);
    }

    return;
}

sub _print_totals ( $self, $totals, $errors ) {
    my $total = $self->_calculate_total($totals);

    $self->_out('');
    $self->_out("Total discrepancies: $total");

    {
        my $totals_indent = $self->_indent_guard();

        # Get label/note definitions from the verifier's discrepancy defs
        my @defs = Cpanel::WPTK::Util::VerifyUserdata->_discrepancy_defs();

        for my $def (@defs) {
            my $count = $totals->{ $def->{type} } // 0;
            my $note  = $def->{note} ? " ($def->{note})" : '';
            $self->_out( sprintf( "%-11s%d%s", "$def->{label}:", $count, $note ) );
        }

        $self->_out( sprintf( "%-11s%d", "Errors:", scalar $errors->@* ) );
    }

    $self->_out('====================');
    $self->_out('');

    return;
}

sub _calculate_total ( $self, $totals ) {
    my $total = 0;
    foreach my $value ( values $totals->%* ) {
        $total += $value;
    }
    return $total;
}

sub _out ( $self, $message ) {
    return if $self->getopt('silent-check');
    $self->get_output_object()->out($message);
    return;
}

sub _warn ( $self, $message ) {
    return if $self->getopt('silent-check');
    $self->get_output_object()->warn($message);
    return;
}

sub _indent_guard ($self) {
    return $self->get_output_object()->create_indent_guard();
}

1;
