<?php
if ( ! defined( 'ABSPATH' ) ) exit;

/**
 * EMPR_Notifications
 *
 * Handles all outbound email notifications for the Employee Roster plugin.
 * Every method is a no-op when the notifications_enabled setting is off.
 * Emails are sent as HTML with a plain-text fallback.
 */
class EMPR_Notifications {

    /* ── Guard ─────────────────────────────────────────────────────────────── */

    public static function is_enabled() {
        return EMPR_Settings::get( 'notifications_enabled', '0' ) === '1';
    }

    /* ── Public notification methods ────────────────────────────────────────── */

    /**
     * Welcome email sent when a new employee account is created or reactivated.
     *
     * Without login access: a simplified notification with no credentials.
     * With login access:     the full email with username, temp password, and reset link.
     *
     * @param int    $user_id        Newly created WP user ID.
     * @param string $plain_password Auto-generated plain-text password (ignored unless
     *                               the full welcome variant is in use).
     */
    public static function welcome( $user_id, $plain_password ) {
        if ( ! self::is_enabled() ) return;

        $user = get_userdata( $user_id );
        if ( ! $user ) return;

        // Always the simplified welcome (no credentials in email).
        self::send_free_welcome( $user );
    }

    /**
     * Simplified welcome — no credentials, just a roster notification.
     *
     * @param WP_User $user
     */
    private static function send_free_welcome( $user ) {
        $company   = EMPR_Settings::company_name();
        $name      = $user->display_name ?: $user->user_login;
        /* translators: %s is the company name */
        $subject   = sprintf( __( '[%s] You have been added to the roster', 'employee-roster' ), $company );

        $content = self::greeting( $name )
            . self::paragraph( sprintf(
                /* translators: %s is the company name */
                __( 'You have been added to the staff roster at <strong>%s</strong>. You will receive roster updates and shift notifications from your employer.', 'employee-roster' ),
                esc_html( $company )
            ) );

        /* translators: %s is the recipient's display name */
        $plain = sprintf( __( 'Hi %s,', 'employee-roster' ), $name ) . "\n\n"
            /* translators: %s is the company name */
            . sprintf( __( 'You have been added to the staff roster at %s. You will receive roster updates and shift notifications from your employer.', 'employee-roster' ), $company ) . "\n\n"
            /* translators: %s is the company name */
            . sprintf( __( '-- %s Team', 'employee-roster' ), $company ) . "\n";

        self::send( $user->user_email, $subject, self::wrap( $company, $content ), $plain );
    }

    /**
     * Notification sent when an employee is assigned to a shift.
     *
     * @param int      $user_id  The employee's WP user ID.
     * @param stdClass $shift    Shift row returned by EMPR_Shifts::get().
     */
    public static function shift_assigned( $user_id, $shift ) {
        if ( ! self::is_enabled() ) return;

        $user = get_userdata( $user_id );
        if ( ! $user ) return;

        $company = EMPR_Settings::company_name();
        $name    = $user->display_name ?: $user->user_login;
        $title   = ! empty( $shift->title )          ? $shift->title          : __( 'Shift', 'employee-roster' );
        $dept    = ! empty( $shift->department )      ? $shift->department     : '';
        $start   = self::format_dt( ! empty( $shift->start_datetime ) ? $shift->start_datetime : '' );
        $end     = self::format_dt( ! empty( $shift->end_datetime )   ? $shift->end_datetime   : '' );
        $loc     = ! empty( $shift->location )        ? $shift->location       : '';
        $notes   = ! empty( $shift->notes )           ? $shift->notes          : '';

        /* translators: %s is the company name */
        $subject = sprintf( __( '[%s] New shift scheduled for you', 'employee-roster' ), $company );

        $rows = array( __( 'Shift', 'employee-roster' ) => esc_html( $title ) );
        if ( $dept ) $rows[ __( 'Department', 'employee-roster' ) ] = esc_html( $dept );
        $rows[ __( 'Start', 'employee-roster' ) ] = esc_html( $start );
        $rows[ __( 'End', 'employee-roster' ) ]   = esc_html( $end );
        if ( $loc )   $rows[ __( 'Location', 'employee-roster' ) ] = esc_html( $loc );
        if ( $notes ) $rows[ __( 'Notes', 'employee-roster' ) ]    = esc_html( $notes );

        $content = self::greeting( $name )
            . self::paragraph( __( 'You have been scheduled for the following shift. Please review the details below.', 'employee-roster' ) )
            . self::detail_table( $rows );

        /* translators: %s is the recipient's display name */
        $plain  = sprintf( __( 'Hi %s,', 'employee-roster' ), $name ) . "\n\n"
            /* translators: %s is the company name */
            . sprintf( __( 'You have been assigned to a shift at %s.', 'employee-roster' ), $company ) . "\n\n"
            /* translators: %s is the shift title */
            . sprintf( __( 'Shift: %s', 'employee-roster' ), $title ) . "\n";
        /* translators: %s is the department name */
        if ( $dept )  $plain .= sprintf( __( 'Department: %s', 'employee-roster' ), $dept ) . "\n";
        /* translators: %s is the shift start datetime */
        $plain .= sprintf( __( 'Start: %s', 'employee-roster' ), $start ) . "\n"
            /* translators: %s is the shift end datetime */
            . sprintf( __( 'End: %s', 'employee-roster' ), $end ) . "\n";
        /* translators: %s is the shift location */
        if ( $loc )   $plain .= sprintf( __( 'Location: %s', 'employee-roster' ), $loc ) . "\n";
        /* translators: %s is the shift notes */
        if ( $notes ) $plain .= sprintf( __( 'Notes: %s', 'employee-roster' ), $notes ) . "\n";
        /* translators: %s is the company name */
        $plain .= "\n\n" . sprintf( __( '-- %s Team', 'employee-roster' ), $company ) . "\n";

        self::send( $user->user_email, $subject, self::wrap( $company, $content ), $plain );
    }

    /**
     * Notification sent to all assigned employees when a shift's timing changes.
     *
     * @param int      $user_id       The employee's WP user ID.
     * @param stdClass $updated_shift Updated shift row returned by EMPR_Shifts::get().
     */
    public static function shift_updated( $user_id, $updated_shift ) {
        if ( ! self::is_enabled() ) return;

        $user = get_userdata( $user_id );
        if ( ! $user ) return;

        $company = EMPR_Settings::company_name();
        $name    = $user->display_name ?: $user->user_login;
        $title   = ! empty( $updated_shift->title )          ? $updated_shift->title          : __( 'Shift', 'employee-roster' );
        $dept    = ! empty( $updated_shift->department )      ? $updated_shift->department     : '';
        $start   = self::format_dt( ! empty( $updated_shift->start_datetime ) ? $updated_shift->start_datetime : '' );
        $end     = self::format_dt( ! empty( $updated_shift->end_datetime )   ? $updated_shift->end_datetime   : '' );
        $loc     = ! empty( $updated_shift->location )        ? $updated_shift->location       : '';

        /* translators: %s is the company name */
        $subject = sprintf( __( '[%s] Your shift has been updated', 'employee-roster' ), $company );

        $rows = array( __( 'Shift', 'employee-roster' ) => esc_html( $title ) );
        if ( $dept ) $rows[ __( 'Department', 'employee-roster' ) ] = esc_html( $dept );
        $rows[ __( 'New start', 'employee-roster' ) ] = esc_html( $start );
        $rows[ __( 'New end', 'employee-roster' ) ]   = esc_html( $end );
        if ( $loc ) $rows[ __( 'Location', 'employee-roster' ) ] = esc_html( $loc );

        $alert = '<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 24px;">'
            . '<tr><td style="background:#fffbeb;border:1px solid #fcd34d;border-left:4px solid #f59e0b;border-radius:6px;padding:12px 16px;">'
            . '<p style="margin:0;font-size:13px;color:#92400e;font-weight:500;">&#9888;&nbsp; '
            . __( 'The timing for a shift you are assigned to has changed. Please review the updated details below.', 'employee-roster' )
            . '</p>'
            . '</td></tr></table>';

        $content = self::greeting( $name )
            . $alert
            . self::detail_table( $rows );

        /* translators: %s is the recipient's display name */
        $plain  = sprintf( __( 'Hi %s,', 'employee-roster' ), $name ) . "\n\n"
            /* translators: %s is the company name */
            . sprintf( __( 'A shift you are scheduled for has been updated at %s.', 'employee-roster' ), $company ) . "\n\n"
            /* translators: %s is the shift title */
            . sprintf( __( 'Shift: %s', 'employee-roster' ), $title ) . "\n";
        /* translators: %s is the department name */
        if ( $dept ) $plain .= sprintf( __( 'Department: %s', 'employee-roster' ), $dept ) . "\n";
        /* translators: %s is the new shift start datetime */
        $plain .= sprintf( __( 'New Start: %s', 'employee-roster' ), $start ) . "\n"
            /* translators: %s is the new shift end datetime */
            . sprintf( __( 'New End: %s', 'employee-roster' ), $end ) . "\n";
        /* translators: %s is the shift location */
        if ( $loc ) $plain .= sprintf( __( 'Location: %s', 'employee-roster' ), $loc ) . "\n";
        /* translators: %s is the company name */
        $plain .= "\n\n" . sprintf( __( '-- %s Team', 'employee-roster' ), $company ) . "\n";

        self::send( $user->user_email, $subject, self::wrap( $company, $content ), $plain );
    }

    /**
     * Notification sent when a leave request is approved or declined.
     *
     * @param int      $user_id     The employee's WP user ID.
     * @param stdClass $leave       Leave row returned by EMPR_Leave::get().
     * @param string   $new_status  'approved' or 'declined'.
     */
    public static function leave_reviewed( $user_id, $leave, $new_status ) {
        if ( ! self::is_enabled() ) return;

        $user = get_userdata( $user_id );
        if ( ! $user ) return;

        $company    = EMPR_Settings::company_name();
        $name       = $user->display_name ?: $user->user_login;
        $approved   = ( $new_status === 'approved' );
        $status_lbl = $approved ? __( 'Approved', 'employee-roster' ) : __( 'Declined', 'employee-roster' );
        $type       = ! empty( $leave->leave_type )
            ? self::leave_type_label( $leave->leave_type )
            : __( 'Leave', 'employee-roster' );
        $start  = ! empty( $leave->start_date ) ? $leave->start_date : '';
        $end    = ! empty( $leave->end_date )   ? $leave->end_date   : '';
        $reason = ! empty( $leave->reason )     ? $leave->reason     : '';

        $subject = sprintf(
            /* translators: 1: company name, 2: leave status ("approved" or "declined") */
            __( '[%1$s] Your leave request has been %2$s', 'employee-roster' ),
            $company,
            strtolower( $status_lbl )
        );

        if ( $approved ) {
            $badge_bg     = '#f0fdf4';
            $badge_border = '#86efac';
            $badge_accent = '#16a34a';
            $badge_text   = '#14532d';
            $badge_icon   = '&#10003;';
        } else {
            $badge_bg     = '#fef2f2';
            $badge_border = '#fca5a5';
            $badge_accent = '#dc2626';
            $badge_text   = '#7f1d1d';
            $badge_icon   = '&#10005;';
        }

        $status_badge = '<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 24px;">'
            . '<tr><td style="background:' . $badge_bg . ';border:1px solid ' . $badge_border . ';border-left:4px solid ' . $badge_accent . ';border-radius:6px;padding:14px 16px;">'
            . '<p style="margin:0;font-size:15px;font-weight:700;color:' . $badge_text . ';">'
            . $badge_icon . '&nbsp; '
            . sprintf(
                /* translators: %s is "Approved" or "Declined" */
                __( 'Leave Request %s', 'employee-roster' ),
                esc_html( $status_lbl )
            )
            . '</p>'
            . '</td></tr></table>';

        $rows = array( __( 'Leave type', 'employee-roster' ) => esc_html( $type ) );
        $rows[ __( 'From', 'employee-roster' ) ]   = esc_html( $start );
        $rows[ __( 'To', 'employee-roster' ) ]     = esc_html( $end );
        $rows[ __( 'Status', 'employee-roster' ) ] = '<span style="display:inline-block;padding:2px 10px;border-radius:20px;font-size:12px;font-weight:600;background:' . $badge_bg . ';color:' . $badge_accent . ';border:1px solid ' . $badge_border . ';">' . esc_html( $status_lbl ) . '</span>';
        if ( $reason ) $rows[ __( 'Reason', 'employee-roster' ) ] = esc_html( $reason );

        $content = self::greeting( $name )
            . $status_badge
            . self::detail_table( $rows );

        /* translators: %s is the recipient's display name */
        $plain  = sprintf( __( 'Hi %s,', 'employee-roster' ), $name ) . "\n\n"
            . sprintf(
                /* translators: 1: leave status ("Approved" or "Declined"), 2: company name */
                __( 'Your leave request has been %1$s at %2$s.', 'employee-roster' ),
                $status_lbl,
                $company
            ) . "\n\n"
            /* translators: %s is the leave type */
            . sprintf( __( 'Type: %s', 'employee-roster' ), $type ) . "\n"
            /* translators: %s is the leave start date */
            . sprintf( __( 'From: %s', 'employee-roster' ), $start ) . "\n"
            /* translators: %s is the leave end date */
            . sprintf( __( 'To: %s', 'employee-roster' ), $end ) . "\n"
            /* translators: %s is the leave status label */
            . sprintf( __( 'Status: %s', 'employee-roster' ), $status_lbl ) . "\n";
        /* translators: %s is the reason/notes */
        if ( $reason ) $plain .= sprintf( __( 'Reason: %s', 'employee-roster' ), $reason ) . "\n";
        /* translators: %s is the company name */
        $plain .= "\n\n" . sprintf( __( '-- %s Team', 'employee-roster' ), $company ) . "\n";

        self::send( $user->user_email, $subject, self::wrap( $company, $content ), $plain );
    }

    /**
     * Notify managers when an employee submits a leave request.
     *
     * @param int $leave_id
     * @param int $employee_id
     */
    public static function leave_requested( $leave_id, $employee_id ) {
        if ( ! self::is_enabled() ) return;

        global $wpdb;
        $leave = $wpdb->get_row( $wpdb->prepare(
            "SELECT * FROM {$wpdb->prefix}emr_leave WHERE id = %d",
            $leave_id
        ) );
        if ( ! $leave ) return;

        $employee   = get_userdata( $employee_id );
        $recipients = self::get_manager_recipients( $employee_id );
        if ( empty( $recipients ) ) return;

        $company    = EMPR_Settings::company_name();
        $admin_url  = admin_url( 'admin.php?page=employee-roster-leave' );
        $emp_name   = $employee->display_name ?: $employee->user_login;
        $leave_type = self::leave_type_label( $leave->leave_type );
        $start      = date_i18n( get_option( 'date_format' ), strtotime( $leave->start_date ) );
        $end        = date_i18n( get_option( 'date_format' ), strtotime( $leave->end_date ) );
        $reason     = ! empty( $leave->reason ) ? $leave->reason : __( 'No reason provided', 'employee-roster' );

        $subject = sprintf(
            /* translators: 1: employee name, 2: company name */
            __( '[%2$s] Leave request from %1$s', 'employee-roster' ),
            $emp_name,
            $company
        );

        $rows = array(
            __( 'Employee',   'employee-roster' ) => esc_html( $emp_name ),
            __( 'Leave type', 'employee-roster' ) => esc_html( $leave_type ),
            __( 'From',       'employee-roster' ) => esc_html( $start ),
            __( 'To',         'employee-roster' ) => esc_html( $end ),
            __( 'Reason',     'employee-roster' ) => esc_html( $reason ),
        );

        $intro      = self::paragraph( sprintf(
            /* translators: %s is the employee's display name */
            __( '<strong>%s</strong> has submitted a leave request that requires your review.', 'employee-roster' ),
            esc_html( $emp_name )
        ) );
        $body_html  = $intro . self::detail_table( $rows )
            . self::btn( $admin_url, __( 'Review Leave Requests', 'employee-roster' ) );

        /* translators: %s is the employee's display name */
        $plain_body = sprintf( __( 'Employee: %s', 'employee-roster' ), $emp_name ) . "\n"
            /* translators: %s is the leave type */
            . sprintf( __( 'Leave Type: %s', 'employee-roster' ), $leave_type ) . "\n"
            /* translators: %s is the leave start date */
            . sprintf( __( 'From: %s', 'employee-roster' ), $start ) . "\n"
            /* translators: %s is the leave end date */
            . sprintf( __( 'To: %s', 'employee-roster' ), $end ) . "\n"
            /* translators: %s is the reason/notes */
            . sprintf( __( 'Reason: %s', 'employee-roster' ), $reason ) . "\n\n"
            /* translators: %s is the admin review URL */
            . sprintf( __( 'Review: %s', 'employee-roster' ), $admin_url ) . "\n\n"
            /* translators: %s is the company name */
            . sprintf( __( '— %s', 'employee-roster' ), $company ) . "\n";

        foreach ( $recipients as $email ) {
            $mgr      = get_user_by( 'email', $email );
            $mgr_name = ( $mgr && $mgr->display_name ) ? $mgr->display_name : 'there';
            $content  = self::greeting( $mgr_name ) . $body_html;
            /* translators: %s is the manager's display name */
            $plain    = sprintf( __( 'Hi %s,', 'employee-roster' ), $mgr_name ) . "\n\n"
                /* translators: %s is the employee's display name */
                . sprintf( __( '%s has submitted a leave request.', 'employee-roster' ), $emp_name ) . "\n\n"
                . $plain_body;
            self::send( $email, $subject, self::wrap( $company, $content ), $plain );
        }
    }

    /**
     * Notify managers when an employee submits a shift swap request.
     *
     * @param int $swap_id
     * @param int $employee_id
     */
    public static function swap_requested( $swap_id, $employee_id ) {
        if ( ! self::is_enabled() ) return;

        global $wpdb;
        $swap = $wpdb->get_row( $wpdb->prepare(
            "SELECT sr.*, s.title AS shift_title, s.start_datetime, s.end_datetime
             FROM {$wpdb->prefix}emr_swap_requests sr
             LEFT JOIN {$wpdb->prefix}emr_shifts s ON s.id = sr.requester_shift_id
             WHERE sr.id = %d",
            $swap_id
        ) );
        if ( ! $swap ) return;

        $employee   = get_userdata( $employee_id );
        $recipients = self::get_manager_recipients( $employee_id );
        if ( empty( $recipients ) ) return;

        $company    = EMPR_Settings::company_name();
        $admin_url  = admin_url( 'admin.php?page=employee-roster-leave' );
        $emp_name   = $employee->display_name ?: $employee->user_login;
        $shift_date = date_i18n( get_option( 'date_format' ), strtotime( $swap->start_datetime ) );
        $shift_time = date_i18n( get_option( 'time_format' ), strtotime( $swap->start_datetime ) )
            . ' – '
            . date_i18n( get_option( 'time_format' ), strtotime( $swap->end_datetime ) );
        $reason     = ! empty( $swap->reason ) ? $swap->reason : __( 'No reason provided', 'employee-roster' );

        $target_name = __( 'Any eligible employee', 'employee-roster' );
        if ( ! empty( $swap->target_user_id ) ) {
            $target = get_userdata( $swap->target_user_id );
            if ( $target ) $target_name = $target->display_name;
        }

        $subject = sprintf(
            /* translators: 1: employee name, 2: company name */
            __( '[%2$s] Shift swap request from %1$s', 'employee-roster' ),
            $emp_name,
            $company
        );

        $rows = array(
            __( 'Employee',  'employee-roster' ) => esc_html( $emp_name ),
            __( 'Shift',     'employee-roster' ) => esc_html( $swap->shift_title ?: __( 'Shift', 'employee-roster' ) ),
            __( 'Date',      'employee-roster' ) => esc_html( $shift_date ),
            __( 'Time',      'employee-roster' ) => esc_html( $shift_time ),
            __( 'Swap with', 'employee-roster' ) => esc_html( $target_name ),
            __( 'Reason',    'employee-roster' ) => esc_html( $reason ),
        );

        $intro     = self::paragraph( sprintf(
            /* translators: %s is the employee's display name */
            __( '<strong>%s</strong> has submitted a shift swap request that requires your review.', 'employee-roster' ),
            esc_html( $emp_name )
        ) );
        $body_html = $intro . self::detail_table( $rows )
            . self::btn( $admin_url, __( 'Review Swap Requests', 'employee-roster' ) );

        /* translators: %s is the employee's display name */
        $plain_body = sprintf( __( 'Employee: %s', 'employee-roster' ), $emp_name ) . "\n"
            /* translators: %s is the shift title */
            . sprintf( __( 'Shift: %s', 'employee-roster' ), $swap->shift_title ) . "\n"
            /* translators: %s is the shift date */
            . sprintf( __( 'Date: %s', 'employee-roster' ), $shift_date ) . "\n"
            /* translators: %s is the shift time range */
            . sprintf( __( 'Time: %s', 'employee-roster' ), $shift_time ) . "\n"
            /* translators: %s is the target employee's name */
            . sprintf( __( 'Swap with: %s', 'employee-roster' ), $target_name ) . "\n"
            /* translators: %s is the reason/notes */
            . sprintf( __( 'Reason: %s', 'employee-roster' ), $reason ) . "\n\n"
            /* translators: %s is the admin review URL */
            . sprintf( __( 'Review: %s', 'employee-roster' ), $admin_url ) . "\n\n"
            /* translators: %s is the company name */
            . sprintf( __( '— %s', 'employee-roster' ), $company ) . "\n";

        foreach ( $recipients as $email ) {
            $mgr      = get_user_by( 'email', $email );
            $mgr_name = ( $mgr && $mgr->display_name ) ? $mgr->display_name : 'there';
            $content  = self::greeting( $mgr_name ) . $body_html;
            /* translators: %s is the manager's display name */
            $plain    = sprintf( __( 'Hi %s,', 'employee-roster' ), $mgr_name ) . "\n\n"
                /* translators: %s is the employee's display name */
                . sprintf( __( '%s has submitted a shift swap request.', 'employee-roster' ), $emp_name ) . "\n\n"
                . $plain_body;
            self::send( $email, $subject, self::wrap( $company, $content ), $plain );
        }
    }

    /**
     * Notify managers when an employee claims an open shift.
     *
     * @param int $shift_id
     * @param int $employee_id
     */
    public static function shift_claimed( $shift_id, $employee_id ) {
        if ( ! self::is_enabled() ) return;

        global $wpdb;
        $shift = $wpdb->get_row( $wpdb->prepare(
            "SELECT * FROM {$wpdb->prefix}emr_shifts WHERE id = %d",
            $shift_id
        ) );
        if ( ! $shift ) return;

        $employee   = get_userdata( $employee_id );
        $recipients = self::get_manager_recipients( $employee_id );
        if ( empty( $recipients ) ) return;

        $company    = EMPR_Settings::company_name();
        $admin_url  = admin_url( 'admin.php?page=employee-roster' );
        $emp_name   = $employee->display_name ?: $employee->user_login;
        $shift_date = date_i18n( get_option( 'date_format' ), strtotime( $shift->start_datetime ) );
        $shift_time = date_i18n( get_option( 'time_format' ), strtotime( $shift->start_datetime ) )
            . ' – '
            . date_i18n( get_option( 'time_format' ), strtotime( $shift->end_datetime ) );
        $location   = ! empty( $shift->location ) ? $shift->location : __( 'Not specified', 'employee-roster' );
        $needs_review = isset( $shift->assignment_type ) && $shift->assignment_type === 'open_approval';

        $subject = sprintf(
            /* translators: 1: employee name, 2: company name */
            __( '[%2$s] Open shift claimed by %1$s', 'employee-roster' ),
            $emp_name,
            $company
        );

        $status_label = $needs_review
            ? __( 'Pending your approval', 'employee-roster' )
            : __( 'Auto-confirmed', 'employee-roster' );

        $status_html = $needs_review
            ? '<span style="display:inline-block;padding:2px 10px;border-radius:20px;font-size:12px;font-weight:600;background:#fffbeb;color:#b45309;border:1px solid #fcd34d;">'
                . esc_html( $status_label ) . '</span>'
            : '<span style="display:inline-block;padding:2px 10px;border-radius:20px;font-size:12px;font-weight:600;background:#f0fdf4;color:#16a34a;border:1px solid #86efac;">'
                . esc_html( $status_label ) . '</span>';

        $rows = array(
            __( 'Employee', 'employee-roster' ) => esc_html( $emp_name ),
            __( 'Shift',    'employee-roster' ) => esc_html( $shift->title ?: __( 'Shift', 'employee-roster' ) ),
            __( 'Date',     'employee-roster' ) => esc_html( $shift_date ),
            __( 'Time',     'employee-roster' ) => esc_html( $shift_time ),
            __( 'Location', 'employee-roster' ) => esc_html( $location ),
            __( 'Status',   'employee-roster' ) => $status_html,
        );

        $intro_text = $needs_review
            ? sprintf(
                /* translators: %s is the employee's display name */
                __( '<strong>%s</strong> has requested to join an open shift and is awaiting your approval.', 'employee-roster' ),
                esc_html( $emp_name )
              )
            : sprintf(
                /* translators: %s is the employee's display name */
                __( '<strong>%s</strong> has joined an open shift. No action is required.', 'employee-roster' ),
                esc_html( $emp_name )
              );

        $btn_label = $needs_review
            ? __( 'Review Open Shift Claim', 'employee-roster' )
            : __( 'View Roster', 'employee-roster' );

        $body_html = self::paragraph( $intro_text )
            . self::detail_table( $rows )
            . self::btn( $admin_url, $btn_label );

        /* translators: %s is the employee's display name */
        $plain_body = sprintf( __( 'Employee: %s', 'employee-roster' ), $emp_name ) . "\n"
            /* translators: %s is the shift title */
            . sprintf( __( 'Shift: %s', 'employee-roster' ), $shift->title ) . "\n"
            /* translators: %s is the shift date */
            . sprintf( __( 'Date: %s', 'employee-roster' ), $shift_date ) . "\n"
            /* translators: %s is the shift time range */
            . sprintf( __( 'Time: %s', 'employee-roster' ), $shift_time ) . "\n"
            /* translators: %s is the shift location */
            . sprintf( __( 'Location: %s', 'employee-roster' ), $location ) . "\n"
            /* translators: %s is the claim status label */
            . sprintf( __( 'Status: %s', 'employee-roster' ), $status_label ) . "\n\n"
            /* translators: %s is the admin review URL */
            . sprintf( __( 'Review: %s', 'employee-roster' ), $admin_url ) . "\n\n"
            /* translators: %s is the company name */
            . sprintf( __( '— %s', 'employee-roster' ), $company ) . "\n";

        foreach ( $recipients as $email ) {
            $mgr      = get_user_by( 'email', $email );
            $mgr_name = ( $mgr && $mgr->display_name ) ? $mgr->display_name : 'there';
            $content  = self::greeting( $mgr_name ) . $body_html;
            /* translators: %s is the manager's display name */
            $plain    = sprintf( __( 'Hi %s,', 'employee-roster' ), $mgr_name ) . "\n\n"
                . ( $needs_review
                    /* translators: %s is the employee's display name */
                    ? sprintf( __( '%s has requested to join an open shift.', 'employee-roster' ), $emp_name )
                    /* translators: %s is the employee's display name */
                    : sprintf( __( '%s has joined an open shift.', 'employee-roster' ), $emp_name )
                  ) . "\n\n"
                . $plain_body;
            self::send( $email, $subject, self::wrap( $company, $content ), $plain );
        }
    }

    /* ── HTML template helpers ──────────────────────────────────────────────── */

    /**
     * Wraps $content in the full email shell: doctype, outer table, header, body, footer.
     */
    private static function wrap( $company, $content ) {
        $brand = EMPR_Settings::primary_color() ?: '#2563eb';
        $year  = gmdate( 'Y' );
        $co    = esc_html( $company );

        return '<!DOCTYPE html>'
            . '<html lang="en"><head>'
            . '<meta charset="UTF-8">'
            . '<meta name="viewport" content="width=device-width,initial-scale=1">'
            . '<meta http-equiv="X-UA-Compatible" content="IE=edge">'
            . '<title>' . $co . '</title>'
            . '</head>'
            . '<body style="margin:0;padding:0;background-color:#f1f5f9;-webkit-text-size-adjust:100%;mso-line-height-rule:exactly;">'
            . '<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0"><tr><td><![endif]-->'
            /* outer wrapper */
            . '<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="border-collapse:collapse;background-color:#f1f5f9;">'
            . '<tr><td align="center" style="padding:32px 16px;">'
            /* card */
            . '<table width="600" cellpadding="0" cellspacing="0" border="0" role="presentation" style="border-collapse:collapse;max-width:600px;width:100%;background-color:#ffffff;border-radius:10px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,.08);">'
            /* header */
            . '<tr><td style="background-color:' . $brand . ';padding:28px 36px;">'
            . '<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="border-collapse:collapse;">'
            . '<tr>'
            . '<td style="font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Helvetica,Arial,sans-serif;font-size:20px;font-weight:700;color:#ffffff;letter-spacing:-.2px;">' . $co . '</td>'
            . '<td align="right" style="font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Helvetica,Arial,sans-serif;font-size:12px;color:rgba(255,255,255,.7);">' . __( 'EmpRoster', 'employee-roster' ) . '</td>'
            . '</tr>'
            . '</table>'
            . '</td></tr>'
            /* body */
            . '<tr><td style="padding:36px 36px 28px;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Helvetica,Arial,sans-serif;">'
            . $content
            . '</td></tr>'
            /* footer */
            . '<tr><td style="background-color:#f8fafc;border-top:1px solid #e2e8f0;padding:20px 36px;">'
            . '<p style="margin:0;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Helvetica,Arial,sans-serif;font-size:12px;color:#94a3b8;text-align:center;line-height:1.5;">'
            . '&copy; ' . $year . ' ' . $co . '&ensp;&middot;&ensp;'
            . __( 'This is an automated notification &mdash; please do not reply to this email.', 'employee-roster' )
            . '</p>'
            . '</td></tr>'
            . '</table>'
            /* /card */
            . '</td></tr></table>'
            /* /outer */
            . '<!--[if mso]></td></tr></table><![endif]-->'
            . '</body></html>';
    }

    /** "Hi Name," paragraph. */
    private static function greeting( $name ) {
        return '<p style="margin:0 0 16px;font-size:17px;font-weight:600;color:#1e293b;line-height:1.4;">'
            . sprintf(
                /* translators: %s is the recipient's display name */
                __( 'Hi %s,', 'employee-roster' ),
                esc_html( $name )
            )
            . '</p>';
    }

    /** Generic paragraph. */
    private static function paragraph( $text, $extra_style = '' ) {
        $style = 'margin:0 0 24px;font-size:15px;line-height:1.65;color:#475569;' . $extra_style;
        return '<p style="' . $style . '">' . $text . '</p>';
    }

    /** Bordered detail table from an associative array of label => value. */
    private static function detail_table( $rows ) {
        $html = '<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" '
            . 'style="border-collapse:collapse;margin:0 0 28px;border:1px solid #e2e8f0;border-radius:8px;overflow:hidden;">';

        $i = 0;
        foreach ( $rows as $label => $value ) {
            $row_bg = ( $i % 2 === 0 ) ? '#ffffff' : '#f8fafc';
            $border = ( $i > 0 ) ? 'border-top:1px solid #e2e8f0;' : '';
            $html .= '<tr style="background-color:' . $row_bg . ';">'
                . '<td style="' . $border . 'padding:11px 16px;width:130px;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Helvetica,Arial,sans-serif;font-size:11px;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:.6px;white-space:nowrap;">'
                . esc_html( $label )
                . '</td>'
                . '<td style="' . $border . 'padding:11px 16px;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Helvetica,Arial,sans-serif;font-size:14px;color:#1e293b;line-height:1.5;">'
                . $value
                . '</td>'
                . '</tr>';
            $i++;
        }

        return $html . '</table>';
    }

    /** CTA button. */
    private static function btn( $url, $label ) {
        $brand = EMPR_Settings::primary_color() ?: '#2563eb';
        return '<table cellpadding="0" cellspacing="0" border="0" role="presentation" style="border-collapse:collapse;margin:0 0 28px;">'
            . '<tr><td style="background-color:' . $brand . ';border-radius:6px;">'
            . '<a href="' . esc_url( $url ) . '" '
            . 'style="display:inline-block;padding:13px 26px;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Helvetica,Arial,sans-serif;font-size:14px;font-weight:600;color:#ffffff;text-decoration:none;letter-spacing:.1px;">'
            . esc_html( $label )
            . '</a>'
            . '</td></tr></table>';
    }

    /* ── Private helpers ────────────────────────────────────────────────────── */

    /** Maps a leave_type DB slug to its translated display label. */
    private static function leave_type_label( $slug ) {
        $labels = array(
            'annual' => __( 'Annual', 'employee-roster' ),
            'sick'   => __( 'Sick', 'employee-roster' ),
        );
        return isset( $labels[ $slug ] )
            ? $labels[ $slug ]
            : ucwords( str_replace( '_', ' ', $slug ) );
    }

    /* ── Private transport helpers ──────────────────────────────────────────── */

    /**
     * Returns email addresses for the administrators who should receive
     * roster-directed notifications.
     *
     * @param int $exclude_user_id User ID to exclude (the acting employee).
     * @return string[]
     */
    private static function get_manager_recipients( $exclude_user_id = 0 ) {
        $recipients = array();

        $managers = get_users( array(
            'role'   => 'emr_manager',
            'fields' => array( 'ID', 'user_email' ),
        ) );

        foreach ( $managers as $manager ) {
            if ( (int) $manager->ID === (int) $exclude_user_id ) {
                continue;
            }
            if ( get_user_meta( $manager->ID, 'empr_deactivated', true ) ) {
                continue;
            }
            $recipients[] = $manager->user_email;
        }

        // Include opted-in administrators as notification recipients.
        if ( EMPR_Settings::get( 'admin_include_all', '0' ) === '1' ) {
            $opted_in_admins = get_users( array(
                'role'   => 'administrator',
                'fields' => array( 'ID', 'user_email' ),
            ) );
        } else {
            $opted_in_ids    = EMPR_Settings::admin_opted_in_ids();
            $opted_in_admins = empty( $opted_in_ids ) ? array() : get_users( array(
                'include' => $opted_in_ids,
                'fields'  => array( 'ID', 'user_email' ),
            ) );
        }
        foreach ( $opted_in_admins as $admin ) {
            if ( (int) $admin->ID === (int) $exclude_user_id ) {
                continue;
            }
            if ( in_array( $admin->user_email, $recipients, true ) ) {
                continue;
            }
            $recipients[] = $admin->user_email;
        }

        return array_filter( $recipients );
    }

    private static function send( $to, $subject, $html, $plain ) {
        // Send HTML version. WordPress's wp_mail wraps the HTML with its own
        // MIME structure when the content-type header is set to text/html.
        $headers = array( 'Content-Type: text/html; charset=UTF-8' );
        wp_mail( $to, $subject, $html, $headers );
    }

    private static function format_dt( $datetime ) {
        if ( ! $datetime ) return '—';
        $ts = strtotime( $datetime );
        return $ts
            ? date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $ts )
            : $datetime;
    }
}
