<?php
/**
 * Email Controls block WP defaults.
 *
 * @package NimblyMail\SMTP
 *
 * @since 1.0.0
 */

namespace NimblyMail\SMTP\Controls;

defined( 'ABSPATH' ) || exit;

use NimblyMail\SMTP\Options;

/**
 * Class EmailControls
 *
 * Maps the toggleable "Email Controls" settings to the WordPress core hooks
 * that must be filtered to suppress each notification.
 *
 * @since 1.0.0
 * @since 1.0.4 Five controls were pointed at hook names that did not exist,
 *              so toggling them off had no effect and the notification kept
 *              sending. Repointed each to the real hook that actually
 *              controls it, and added two more suppression techniques for
 *              notifications that have no simple on/off switch to filter:
 *              removing the WordPress callback that sends the email
 *              outright where that's possible, and — for the one remaining
 *              case where it isn't — blanking the recipient address so the
 *              send has nowhere to go.
 * @since 1.0.5 Eight controls filtered a message body or an email-parts
 *              array on a hook WordPress core doesn't actually gate the
 *              send on, so "disabling" them either delivered a blank email
 *              (dis_admin_email_attempt, dis_admin_email_network_attempt,
 *              dis_new_user_created_network, dis_new_site_activated_to_admin)
 *              or dereferenced an emptied array with no isset() guard,
 *              producing PHP warnings and a false delivery-failure alert
 *              (dis_new_user_to_admin, dis_new_user_to_user,
 *              dis_user_password_reset, dis_user_email_change_attempt).
 *              Repointed the first four at removing the real WordPress
 *              callback that sends the mail, two of which are registered on
 *              two differently-named action hooks and needed a new
 *              multi-hook removal mechanism (see disable_email()). Repointed
 *              two of the second four at genuine WP 6.1+ boolean gate
 *              filters, and the remaining two — dis_user_password_reset and
 *              dis_user_email_change_attempt — at removing their real
 *              sending callback: wp_password_change_notification() on
 *              after_password_reset, and
 *              send_confirmation_on_profile_email() on
 *              personal_options_update (wp-includes/user.php), the actual
 *              core mechanism that emails the proposed new address when a
 *              user changes their email on their own profile screen.
 */
class EmailControls {

    /**
     * Get the full controls configuration.
     *
     * Returns the built-in WordPress notification toggles, then applies the
     * nimblymail_email_controls filter so Pro add-ons can append additional
     * email control groups (e.g. WooCommerce notifications).
     *
     * @since 1.0.0 Added nimblymail_email_controls filter for Pro add-ons.
     * @since 1.0.4 Five of the admin-email-change and personal-data-request
     *              controls were pointed at hook names that don't exist in
     *              WordPress core, so disabling them had no effect. Repointed
     *              all five at their real hook.
     * @since 1.0.5 Repointed eight controls (dis_admin_email_attempt,
     *              dis_admin_email_network_attempt,
     *              dis_new_user_created_network,
     *              dis_new_site_activated_to_admin, dis_new_user_to_admin,
     *              dis_new_user_to_user, dis_user_password_reset,
     *              dis_user_email_change_attempt) whose hook_type filtered a
     *              value core sends unconditionally regardless of it, so
     *              disabling them either delivered a blank email or produced
     *              PHP warnings and a false failure alert. See the class
     *              docblock for the full breakdown.
     *
     * @return array
     */
    public static function get_controls(): array {
        $controls = array(
            'comments'      => array(
                'title'  => __( 'Comments', 'nimblymail-smtp' ),
                'emails' => array(
                    'dis_comments_awaiting_moderation' => array(
                        'label'       => __( 'Awaiting Moderation', 'nimblymail-smtp' ),
                        'description' => __( 'Comment is awaiting moderation. Sent to site admin and post author.', 'nimblymail-smtp' ),
                        'hook'        => 'notify_moderator',
                        'hook_type'   => 'filter',
                    ),
                    'dis_comments_published'           => array(
                        'label'       => __( 'Published', 'nimblymail-smtp' ),
                        'description' => __( 'Comment has been published. Sent to the post author.', 'nimblymail-smtp' ),
                        'hook'        => 'notify_post_author',
                        'hook_type'   => 'filter',
                    ),
                ),
            ),

            'admin_email'   => array(
                'title'  => __( 'Change of Admin Email', 'nimblymail-smtp' ),
                'emails' => array(
                    'dis_admin_email_attempt'         => array(
                        'label'               => __( 'Site Admin Email Change Attempt', 'nimblymail-smtp' ),
                        'description'         => __( 'Change of site admin email attempted. Sent to proposed new email.', 'nimblymail-smtp' ),
                        'hook'                => 'update_option_new_admin_email',
                        'hook_type'           => 'action_removal',
                        'callback'            => 'update_option_new_admin_email',
                        'priority'            => 10,
                        'additional_removals' => array(
                            array(
                                'hook'     => 'add_option_new_admin_email',
                                'callback' => 'update_option_new_admin_email',
                                'priority' => 10,
                            ),
                        ),
                    ),
                    'dis_admin_email_changed'         => array(
                        'label'       => __( 'Site Admin Email Changed', 'nimblymail-smtp' ),
                        'description' => __( 'Site admin email changed. Sent to old admin email.', 'nimblymail-smtp' ),
                        'hook'        => 'send_site_admin_email_change_email',
                        'hook_type'   => 'filter',
                    ),
                    'dis_admin_email_network_attempt' => array(
                        'label'               => __( 'Network Admin Email Change Attempt', 'nimblymail-smtp' ),
                        'description'         => __( 'Network admin email change attempted. Sent to proposed email.', 'nimblymail-smtp' ),
                        'hook'                => 'update_site_option_new_admin_email',
                        'hook_type'           => 'action_removal',
                        'callback'            => 'update_network_option_new_admin_email',
                        'priority'            => 10,
                        'multisite'           => true,
                        'additional_removals' => array(
                            array(
                                'hook'     => 'add_site_option_new_admin_email',
                                'callback' => 'update_network_option_new_admin_email',
                                'priority' => 10,
                            ),
                        ),
                    ),
                    'dis_admin_email_network_changed' => array(
                        'label'       => __( 'Network Admin Email Changed', 'nimblymail-smtp' ),
                        'description' => __( 'Network admin email changed. Sent to old network admin email.', 'nimblymail-smtp' ),
                        'hook'        => 'send_network_admin_email_change_email',
                        'hook_type'   => 'filter',
                        'multisite'   => true,
                    ),
                ),
            ),

            'user_details'  => array(
                'title'  => __( 'Change of User Email or Password', 'nimblymail-smtp' ),
                'emails' => array(
                    'dis_user_password_reset_request' => array(
                        'label'       => __( 'Reset Password Request', 'nimblymail-smtp' ),
                        'description' => __( 'User requested password reset via "Lost your password?". Sent to user.', 'nimblymail-smtp' ),
                        'hook'        => 'retrieve_password_message',
                        'hook_type'   => 'content_filter',
                    ),
                    'dis_user_password_reset'         => array(
                        'label'       => __( 'Password Reset Successfully', 'nimblymail-smtp' ),
                        'description' => __( 'User reset their password. Sent to site admin.', 'nimblymail-smtp' ),
                        'hook'        => 'after_password_reset',
                        'hook_type'   => 'action_removal',
                        'callback'    => 'wp_password_change_notification',
                        'priority'    => 10,
                    ),
                    'dis_user_password_changed'       => array(
                        'label'       => __( 'Password Changed', 'nimblymail-smtp' ),
                        'description' => __( 'User changed their password. Sent to the user.', 'nimblymail-smtp' ),
                        'hook'        => 'send_password_change_email',
                        'hook_type'   => 'filter',
                    ),

                    'dis_user_email_change_attempt'   => array(
                        'label'       => __( 'Email Change Attempt', 'nimblymail-smtp' ),
                        'description' => __( 'User attempted to change their email. Sent to proposed new email.', 'nimblymail-smtp' ),
                        'hook'        => 'personal_options_update',
                        'hook_type'   => 'action_removal',
                        'callback'    => 'send_confirmation_on_profile_email',
                        'priority'    => 10,
                    ),
                    'dis_user_email_changed'          => array(
                        'label'       => __( 'Email Changed', 'nimblymail-smtp' ),
                        'description' => __( 'User changed their email. Sent to the user.', 'nimblymail-smtp' ),
                        'hook'        => 'send_email_change_email',
                        'hook_type'   => 'filter',
                    ),
                ),
            ),

            'personal_data' => array(
                'title'  => __( 'Personal Data Requests', 'nimblymail-smtp' ),
                'emails' => array(
                    'dis_personal_data_confirmed'   => array(
                        'label'       => __( 'User Confirmed Export / Erasure Request', 'nimblymail-smtp' ),
                        'description' => __( 'User confirmed personal data request. Sent to admin.', 'nimblymail-smtp' ),
                        'hook'        => 'user_request_action_confirmed',
                        'hook_type'   => 'action_removal',
                        'callback'    => '_wp_privacy_send_request_confirmation_notification',
                        'priority'    => 12,
                    ),
                    'dis_personal_data_erased'      => array(
                        'label'       => __( 'Admin Erased Data', 'nimblymail-smtp' ),
                        'description' => __( 'Site admin erased personal data. Sent to requester.', 'nimblymail-smtp' ),
                        'hook'        => 'wp_privacy_personal_data_erased',
                        'hook_type'   => 'action_removal',
                        'callback'    => '_wp_privacy_send_erasure_fulfillment_notification',
                        'priority'    => 10,
                    ),
                    'dis_personal_data_export_link' => array(
                        'label'       => __( 'Admin Sent Link to Export Data', 'nimblymail-smtp' ),
                        'description' => __( 'Admin sent data export link. Sent to requester.', 'nimblymail-smtp' ),
                        'hook'        => 'wp_privacy_personal_data_email_to',
                        'hook_type'   => 'recipient_filter',
                        'warning'     => true,
                    ),
                ),
            ),

            'auto_updates'  => array(
                'title'  => __( 'Automatic Updates', 'nimblymail-smtp' ),
                'emails' => array(
                    'dis_auto_plugin_update_status' => array(
                        'label'        => __( 'Plugin Status', 'nimblymail-smtp' ),
                        'description'  => __( 'Background automatic plugin update result. Sent to admin.', 'nimblymail-smtp' ),
                        'hook'         => 'auto_plugin_update_send_email',
                        'hook_type'    => 'filter',
                        'network_only' => true,
                    ),
                    'dis_auto_theme_update_status'  => array(
                        'label'        => __( 'Theme Status', 'nimblymail-smtp' ),
                        'description'  => __( 'Background automatic theme update result. Sent to admin.', 'nimblymail-smtp' ),
                        'hook'         => 'auto_theme_update_send_email',
                        'hook_type'    => 'filter',
                        'network_only' => true,
                    ),
                    'dis_auto_core_update_status'   => array(
                        'label'        => __( 'WP Core Status', 'nimblymail-smtp' ),
                        'description'  => __( 'Background automatic core update result. Sent to admin.', 'nimblymail-smtp' ),
                        'hook'         => 'auto_core_update_send_email',
                        'hook_type'    => 'filter',
                        'network_only' => true,
                    ),
                    'dis_auto_updates_full_log'     => array(
                        'label'        => __( 'Full Log', 'nimblymail-smtp' ),
                        'description'  => __( 'Full background update results log. Only on dev versions.', 'nimblymail-smtp' ),
                        'hook'         => 'automatic_updates_send_debug_email',
                        'hook_type'    => 'filter',
                        'network_only' => true,
                    ),
                ),
            ),

            'new_user'      => array(
                'title'  => __( 'New User', 'nimblymail-smtp' ),
                'emails' => array(
                    'dis_new_user_to_admin'          => array(
                        'label'       => __( 'Created (Admin)', 'nimblymail-smtp' ),
                        'description' => __( 'A new user was created. Sent to site admin.', 'nimblymail-smtp' ),
                        'hook'        => 'wp_send_new_user_notification_to_admin',
                        'hook_type'   => 'filter',
                    ),
                    'dis_new_user_to_user'           => array(
                        'label'       => __( 'Created (User)', 'nimblymail-smtp' ),
                        'description' => __( 'A new user was created. Sent to the new user.', 'nimblymail-smtp' ),
                        'hook'        => 'wp_send_new_user_notification_to_user',
                        'hook_type'   => 'filter',
                    ),
                    'dis_new_user_invited_network'   => array(
                        'label'       => __( 'Invited To Site', 'nimblymail-smtp' ),
                        'description' => __( 'A new user was invited. Sent to invited user.', 'nimblymail-smtp' ),
                        'hook'        => 'wpmu_signup_user_notification',
                        'hook_type'   => 'filter',
                        'multisite'   => true,
                    ),
                    'dis_new_user_created_network'   => array(
                        'label'       => __( 'Created On Site', 'nimblymail-smtp' ),
                        'description' => __( 'New user account created. Sent to Network Admin.', 'nimblymail-smtp' ),
                        'hook'        => 'wpmu_new_user',
                        'hook_type'   => 'action_removal',
                        'callback'    => 'newuser_notify_siteadmin',
                        'priority'    => 10,
                        'multisite'   => true,
                    ),
                    'dis_new_user_activated_network' => array(
                        'label'       => __( 'Added / Activated on Site', 'nimblymail-smtp' ),
                        'description' => __( 'A user has been added or activated. Sent to the user.', 'nimblymail-smtp' ),
                        'hook'        => 'wpmu_welcome_user_notification',
                        'hook_type'   => 'filter',
                        'multisite'   => true,
                    ),
                ),
            ),

            'new_site'      => array(
                'title'  => __( 'New Site', 'nimblymail-smtp' ),
                'emails' => array(
                    'dis_new_site_registered'         => array(
                        'label'       => __( 'User Created Site', 'nimblymail-smtp' ),
                        'description' => __( 'User registered for a new site. Sent to site admin.', 'nimblymail-smtp' ),
                        'hook'        => 'wpmu_signup_blog_notification',
                        'hook_type'   => 'filter',
                        'multisite'   => true,
                    ),
                    'dis_new_site_activated_to_admin' => array(
                        'label'       => __( 'Network Admin: User Activated / Added Site', 'nimblymail-smtp' ),
                        'description' => __( 'User activated their site or site was added. Sent to Network Admin.', 'nimblymail-smtp' ),
                        'hook'        => 'wp_initialize_site',
                        'hook_type'   => 'action_removal',
                        'callback'    => 'newblog_notify_siteadmin',
                        'priority'    => 100,
                        'multisite'   => true,
                    ),
                    'dis_new_site_activated_to_site'  => array(
                        'label'       => __( 'Site Admin: Activated / Added Site', 'nimblymail-smtp' ),
                        'description' => __( 'User activated their site or site was added. Sent to Site Admin.', 'nimblymail-smtp' ),
                        'hook'        => 'wpmu_welcome_notification',
                        'hook_type'   => 'filter',
                        'multisite'   => true,
                    ),
                ),
            ),
        );

        /**
         * Filters the full email controls configuration.
         *
         * Allows Pro add-ons to append additional email control groups (e.g.
         * WooCommerce transactional emails) to the existing WordPress notification
         * toggles. Each group must follow the same shape as the built-in groups:
         * an associative array with a 'title' string and an 'emails' array of
         * control definitions.
         *
         * @since 1.0.0
         *
         * @param array $controls The full controls map keyed by group slug.
         *
         * @return array
         */
        return apply_filters( 'nimblymail_email_controls', $controls );
    }

    /**
     * Initialize Email Controls filters.
     *
     * Defers the actual filter registration to the WordPress `init` action
     * because get_controls() builds its config array using __() calls. WP 6.7+
     * raises an _load_textdomain_just_in_time notice when any plugin
     * translation function fires before `init`, and on WP_DEBUG sites that
     * notice emits HTML output before the response headers are sent, which
     * then cascades into "headers already sent" warnings and blocks login
     * cookies. Hooking on `init` runs us comfortably after the WP textdomain
     * machinery is ready and before any wp_mail() the filters care about.
     *
     * Also runs again on `admin_init`. WordPress doesn't register every
     * notification callback at the same point in the request: the ones this
     * class removes outright are added while the admin is still bootstrapping,
     * which for some of them happens after `init` has already fired. Running
     * register_filters() a second time once that bootstrapping has finished
     * catches those. Re-running it is safe — re-adding the same filter
     * callback, or trying to remove one that's already gone, is a no-op both
     * times.
     *
     * @since 1.0.0
     * @since 1.0.4 Also runs on admin_init, so a control that removes a
     *              WordPress callback registered during admin bootstrapping
     *              — after init already fired — still takes effect.
     */
    public function init(): void {
        add_action( 'init', array( $this, 'register_filters' ) );
        add_action( 'admin_init', array( $this, 'register_filters' ) );
    }

    /**
     * Register the actual notification-blocking filters.
     *
     * Runs on the `init` action so __() calls inside get_controls() are
     * issued after the WordPress translation pipeline is ready.
     *
     * @since 1.0.0
     *
     * @return void
     */
    public function register_filters(): void {
        $options = Options::init();

        foreach ( self::get_controls() as $category ) {
            foreach ( $category['emails'] as $key => $email ) {
                $multisite_only = ! empty( $email['multisite'] );
                $network_only   = $multisite_only || ! empty( $email['network_only'] );

                // Multisite controls only apply to multisite installs.
                if ( $multisite_only && ! is_multisite() ) {
                    continue;
                }

                if ( $network_only && is_multisite() ) {
                    // Network-only control on multisite: driven solely by the
                    // network-level value, so a per-site value can never enable
                    // or disable it.
                    $disabled = (bool) $options->get_network_metadata( 'controls', $key )['value'];
                } else {
                    $disabled = (bool) $options->get( 'controls', $key );
                }

                if ( $disabled ) {
                    $this->disable_email( $email );
                }
            }
        }
    }

    /**
     * Apply the correct technique to suppress the email.
     *
     * Most notifications have a real boolean or content filter that can just
     * be short-circuited. A few don't: WordPress sends them unconditionally
     * and only lets a plugin change who receives it, not whether it's sent
     * at all. For those, removing the WordPress callback that sends the
     * email is used wherever that callback is a normal action hook, since
     * that stops the send outright rather than causing it to fail. Where the
     * email is instead sent from a direct function call with nothing to
     * remove, blanking the recipient is the only lever available. That's a
     * real send failure, not true silence: it's recorded as a failed send
     * elsewhere in the plugin (a stored log entry, a delivery-error notice),
     * and for the one control still using this — the personal data export
     * link — WordPress itself also surfaces an error on the admin's Export
     * Personal Data screen and leaves that request marked incomplete, since
     * it only considers the request done once the email genuinely sends.
     *
     * @since 1.0.0
     * @since 1.0.4 Added removing the sending callback outright as a
     *              suppression technique, and a fallback for blanking the
     *              recipient of a notification that has no callback to
     *              remove and no boolean gate to filter.
     * @since 1.0.5 action_removal now also honours an 'additional_removals'
     *              array on the control, each entry the same
     *              hook/callback/priority shape as the control's own, for
     *              the two admin-email-change controls whose sending
     *              callback is registered on two differently-named action
     *              hooks (an add_option_* hook that only ever fires the
     *              first time the option is created, and the update_option_*
     *              hook that fires on every later change) rather than one.
     *
     * @param array $email Email configuration from controls array.
     *
     * @return void
     */
    private function disable_email( array $email ): void {
        switch ( $email['hook_type'] ) {
            case 'filter':
                add_filter( $email['hook'], '__return_false' );
                break;
            case 'content_filter':
                add_filter( $email['hook'], '__return_empty_string' );
                break;
            case 'email_filter':
                add_filter( $email['hook'], '__return_empty_array' );
                break;
            case 'recipient_filter':
                add_filter( $email['hook'], '__return_empty_string' );
                break;
            case 'action_removal':
                remove_action( $email['hook'], $email['callback'], $email['priority'] );

                foreach ( $email['additional_removals'] ?? array() as $removal ) {
                    remove_action( $removal['hook'], $removal['callback'], $removal['priority'] );
                }
                break;
        }
    }
}
