#!/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)
        --only-add-update    Only add and update wptkids, do not remove
        --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 using the C<--only-add-update> option, the script will only add missing
WPTK IDs and update mismatched IDs, but will not remove any WPTK IDs from
userdata.

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',
    'only-add-update',
    '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 $silent_check    = $self->getopt('silent-check');
    my $repair          = $self->getopt('repair');
    my $only_add_update = $self->getopt('only-add-update');
    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");
    }

    $self->_out('Starting verify_wptk_userdata...');

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

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

    my $verifier = Cpanel::WPTK::Util::VerifyUserdata->new(
        output          => $silent_check    ? undef : $self->get_output_object(),
        quiet           => $silent_check    ? 1     : 0,
        repair          => $repair          ? 1     : 0,
        only_add_update => $only_add_update ? 1     : 0,
    );

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

    for my $username (@users) {
        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;
        };

        next if !defined $user_result;
        my $has_issues = grep { ref $_ eq 'HASH' && keys $_->%* } values $user_result->%*;

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

    }

    my $has_problems = keys $result->{users}->%* || scalar $result->{errors}->@*;

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

    return $has_problems ? EXIT_PROBLEMS_FOUND : EXIT_NO_PROBLEMS;
}

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->_out('');
    $self->_out('====================');
    $self->_out('Summary');
    $self->_out('====================');

    my %totals = (
        updated   => 0,
        added     => 0,
        removed   => 0,
        bad_owner => 0,
        stale     => 0,
    );

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

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

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

        for my $domain ( sort keys $user_data->{updated}->%* ) {
            my $info = $user_data->{updated}{$domain};
            $self->_out("[UPDATE] Mismatched in userdata (id: $info->{old}) and in WPTK (id: $info->{new}): $domain");
            $totals{updated}++;
        }

        for my $domain ( sort keys $user_data->{added}->%* ) {
            my $id = $user_data->{added}{$domain};
            $self->_out("[ADD] Missing in userdata and exists in WPTK (id: $id): $domain");
            $totals{added}++;
        }

        for my $domain ( sort keys $user_data->{removed}->%* ) {
            my $id = $user_data->{removed}{$domain};
            $self->_out("[REMOVE] Exists in userdata (id: $id) and missing in WPTK: $domain");
            $totals{removed}++;
        }

        for my $domain ( sort keys $user_data->{stale}->%* ) {
            my $id = $user_data->{stale}{$domain};
            $self->_out("[MANUAL FIX] Exists in userdata and WPTK (id: $id) without domain userdata, requires manual cleanup: $domain");
            $totals{stale}++;
        }

        for my $domain ( sort keys $user_data->{bad_owner}->%* ) {
            my $info = $user_data->{bad_owner}{$domain};
            $self->_out("[REMOVE] Exists in userdata (id: $info->{id}) and exists in WPTK for a different owner ($info->{owner}): $domain");
            $totals{bad_owner}++;
        }
    }

    if ( scalar $errors->@* ) {
        $self->_out('');
        $self->_out('Errors encountered:');
        my $errors_indent = $self->_indent_guard();
        for my $error ( $errors->@* ) {
            $self->_out($error);
        }
    }

    my $total = 0;
    foreach my $value ( values %totals ) {
        $total += $value;
    }

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

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

        $self->_out("Update:    $totals{updated}");
        $self->_out("Add:       $totals{added}");
        $self->_out("Remove:    $totals{removed}");
        $self->_out("Bad owner: $totals{bad_owner}");
        $self->_out("Stale:     $totals{stale} (manual fix needed)");
        $self->_out( "Errors:    " . scalar $errors->@* );
    }

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

    return;
}

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;
