<?php
// Plugin Check / PHPCS: category-level exclusions with reasons.
// Line-level findings in this file are handled individually below.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- PaintOps keeps its records in its own custom tables (built by paintops_table()); $wpdb is the supported API for plugin-owned tables, there is no core CRUD alternative, and results are request-scoped.
// phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date -- date() here is deliberately paired with current_time() timestamps: crews are scheduled on the site's local calendar day, and gmdate() would shift jobs across midnight UTC.
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only GET routing and filter parameters (which screen/record to show); every state-changing handler verifies a nonce before writing.
/**
 * Included File: Customers
 *
 * A searchable directory over the cs_customers table (see
 * paintops_find_or_create_customer() in paintops.php), which until now
 * only ever got written to silently when a job/quote was saved, there
 * was no page to actually look anyone up. This adds:
 *
 *   - A list view with a simple name/email/phone/address search.
 *   - A detail view per customer: contact info (editable), every job
 *     and quote linked via jobs.customer_id, and a lifetime revenue
 *     total pulled from the same job-financials data the Profits page
 *     already uses.
 *   - A basic "add a customer by hand" form, for anyone who was never
 *     attached to a job (a referral you want to note down, etc.).
 *
 * Same staff-only gate as the rest of the app (paintops_user_can_manage())
 *, this is operational business data, never shown to a customer.
 */

if (!defined('ABSPATH')) exit;

/**
 * Display-format a customer's phone: US 10-digit numbers become
 * (111) 222-3333 via the shared paintops_format_phone_number() helper; anything
 * else (blank, or an international/unusual number) is shown as-entered
 * so we never mangle a non-US number or print an error. Returns
 * display-ready, escaped HTML.
 */
if (!function_exists('paintops_customer_phone_display')) {
    function paintops_customer_phone_display($phone): string {
        $phone = trim((string) $phone);
        if ($phone === '') {
            return '';
        }
        $digits = preg_replace('/\D+/', '', $phone);
        if (strlen($digits) === 10 && function_exists('paintops_format_phone_number')) {
            // paintops_format_phone_number(..., 0) returns the plain "(111) 222-3333"
            // string (safe characters only); no extra escaping needed.
            return paintops_format_phone_number($phone, 0);
        }
        // International / unusual length, show exactly what was entered.
        return esc_html($phone);
    }
}

/**
 * Assemble a customer's service address into one readable line:
 * "Street, City, ST 12345" for US, gracefully dropping any piece that
 * isn't present so international addresses (often no US-style state, and
 * a different postal format) still read cleanly, e.g. "12 King St,
 * London SW1A 1AA" or "Street, City". Returns a plain string, the
 * caller escapes it on output.
 */
if (!function_exists('paintops_customer_address_line')) {
    function paintops_customer_address_line($customer): string {
        $street = trim((string) ($customer->service_address ?? ''));
        $city   = trim((string) ($customer->service_city ?? ''));
        $state  = trim((string) ($customer->service_state ?? ''));
        $zip    = trim((string) ($customer->service_zip ?? ''));

        // City, State, Zip built US-style, but each part is optional so a
        // missing state/zip (common internationally) never leaves a
        // stray comma or double space behind.
        $cityStateZip = $city;
        if ($state !== '') {
            $cityStateZip = ($cityStateZip !== '') ? $cityStateZip . ', ' . $state : $state;
        }
        if ($zip !== '') {
            $cityStateZip = trim($cityStateZip . ' ' . $zip);
        }

        $segments = array_filter([$street, $cityStateZip], static function ($s) {
            return $s !== '';
        });

        return implode(', ', $segments);
    }
}

// The Jobs page used to embed a "Find a Customer" widget here, a
// single search box that just redirected to ?customers=1&custsearch=...
// with the term pre-filled. It duplicated the Customers page's own
// search field exactly (same custsearch param, same placeholder) and
// added no capability of its own, so it was removed in favor of just
// linking to the Customers page (reachable from the More menu).

/**
 * Total revenue recognized across every real Job (not Quote) linked to
 * this customer. Reuses paintops_job_financial_summary(), the same
 * already-fixed, labor-cost-aware function the Profits page and job
 * reports use, so this figure is never a second, drifting copy of
 * the profit math.
 */
function paintops_customer_lifetime_value(array $jobs): float {
    if (!function_exists('paintops_job_financial_summary')) {
        return 0.0;
    }

    $total = 0.0;
    foreach ($jobs as $job) {
        if ((int) $job->jobType === 2) {
            $summary = paintops_job_financial_summary((int) $job->id);
            $total += (float) ($summary['gross'] ?? 0);
        }
    }

    return $total;
}

/**
 * Handles the add/edit POST actions. Runs before any HTML so redirects
 * and validation messages can be produced cleanly.
 */
/**
 * Permanently delete a customer and, as a full wipe, every job/quote
 * linked to them and all of that data, via paintops_delete_job_deep()
 * (which handles the child tables and media files). Used by the Delete
 * Customer control on the customer detail view. The caller enforces the
 * administrator-only gate + nonce + acknowledgment; this just does the
 * work. Returns the number of job rows removed (for the summary notice).
 */
function paintops_delete_customer_deep(int $customer_id): int {
    global $wpdb;

    if ($customer_id <= 0) {
        return 0;
    }

    $jobs_t = paintops_table('jobs');
    $job_ids = array_map('intval', (array) $wpdb->get_col($wpdb->prepare(
        "SELECT id FROM %i WHERE customer_id = %d", $jobs_t, $customer_id)));

    $jobs_removed = 0;
    if (function_exists('paintops_delete_job_deep')) {
        foreach ($job_ids as $jid) {
            // Idempotent: a follow-up already removed as a child of an
            // earlier job in this list simply deletes 0 rows the second time.
            $jobs_removed += paintops_delete_job_deep($jid);
        }
    }

    $wpdb->delete(paintops_table('customers'), ['id' => $customer_id], ['%d']);

    return $jobs_removed;
}

function paintops_handle_customers_post(): string {
    global $wpdb;
    $table = paintops_table('customers');
    $notice = '';

    // Permanent customer wipe. Administrators only (stricter than the
    // edit_posts gate on the rest of this page), nonce-checked, and it
    // requires the acknowledgment checkbox so a stray click can't erase a
    // customer and all their job history. See paintops_delete_customer_deep().
    if (isset($_POST['paintops_delete_customer']) && current_user_can('manage_options') && check_admin_referer('paintops_delete_customer_action')) {
        $delete_customer_id = (int) ($_POST['customer_id'] ?? 0); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- value is cast to a number, sanitization not applicable
        $acknowledged = !empty($_POST['paintops_delete_customer_ack']);

        if ($delete_customer_id <= 0) {
            $notice = '<div class="notice notice-error" style="margin:12px 0;padding:8px 12px;background:#fdecea;border-radius:4px;">No customer was specified to delete.</div>';
        } elseif (!$acknowledged) {
            // Keep them on the detail view so they can tick the box.
            $_GET['viewCustomer'] = $delete_customer_id;
            $notice = '<div class="notice notice-error" style="margin:12px 0;padding:8px 12px;background:#fdecea;border-radius:4px;">Tick the confirmation box to permanently delete this customer.</div>';
        } else {
            $jobs_removed = paintops_delete_customer_deep($delete_customer_id);
            // The customer no longer exists: drop any view/edit routing so the
            // page falls through to the list instead of "Customer not found".
            unset($_GET['viewCustomer'], $_GET['editCustomer']);
            $tail = $jobs_removed > 0
                ? ', along with ' . (int) $jobs_removed . ' linked job/quote record' . ($jobs_removed === 1 ? '' : 's') . ' and everything attached to them (estimates, financials, photos, receipts, payments).'
                : '.';
            $notice = '<div class="notice notice-success" style="margin:12px 0;padding:8px 12px;background:#d7f5d7;border-radius:4px;">Customer permanently deleted' . $tail . '</div>';
        }
    }

    // Inline primary-address save from the customer LIST rows (Mike
    // 2026-07-23): the primary address doubles as the mailing/invoice
    // address, but the only way to change it was the full Edit Customer
    // form buried behind the detail page. This branch updates ONLY the
    // four address columns, deliberately its own branch instead of
    // reusing paintops_save_customer below, which reads EVERY field
    // from POST and would blank the name/email/phone a row form doesn't
    // carry. No viewCustomer redirect either: the save came from the
    // list, so the user stays on the list (search filter and all, the
    // row form posts back to the current URL).
    if (isset($_POST['paintops_save_customer_address']) && paintops_user_can_manage() && check_admin_referer('paintops_save_customer_address_action')) {
        $addr_customer_id = (int) ($_POST['customer_id'] ?? 0); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- value is cast to a number, sanitization not applicable
        if ($addr_customer_id > 0) {
            $wpdb->update($table, [
                'service_address' => sanitize_text_field(wp_unslash($_POST['service_address'] ?? '')),
                'service_city'    => sanitize_text_field(wp_unslash($_POST['service_city'] ?? '')),
                'service_state'   => strtoupper(sanitize_text_field(wp_unslash($_POST['service_state'] ?? ''))),
                'service_zip'     => sanitize_text_field(wp_unslash($_POST['service_zip'] ?? '')),
            ], ['id' => $addr_customer_id], ['%s', '%s', '%s', '%s'], ['%d']);
            $notice = '<div class="notice notice-success" style="margin:12px 0;padding:8px 12px;background:#d7f5d7;border-radius:4px;">Primary address updated.</div>';
        }
    }

    if (isset($_POST['paintops_save_customer']) && paintops_user_can_manage() && check_admin_referer('paintops_save_customer_action')) {
        $customer_id = (int) ($_POST['customer_id'] ?? 0); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- value is cast to a number, sanitization not applicable

        $data = [
            'first_name'      => sanitize_text_field(wp_unslash($_POST['first_name'] ?? '')),
            'last_name'       => sanitize_text_field(wp_unslash($_POST['last_name'] ?? '')),
            'email'           => sanitize_email(wp_unslash($_POST['email'] ?? '')),
            'phone'           => sanitize_text_field(wp_unslash($_POST['phone'] ?? '')),
            'service_address' => sanitize_text_field(wp_unslash($_POST['service_address'] ?? '')),
            'service_city'    => sanitize_text_field(wp_unslash($_POST['service_city'] ?? '')),
            'service_state'   => strtoupper(sanitize_text_field(wp_unslash($_POST['service_state'] ?? ''))),
            'service_zip'     => sanitize_text_field(wp_unslash($_POST['service_zip'] ?? '')),
            'notes'           => sanitize_textarea_field(wp_unslash($_POST['notes'] ?? '')),
        ];
        $formats = ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s'];

        if ($customer_id > 0) {
            $wpdb->update($table, $data, ['id' => $customer_id], $formats, ['%d']);
            $notice = '<div class="notice notice-success" style="margin:12px 0;padding:8px 12px;background:#d7f5d7;border-radius:4px;">Customer updated.</div>';
        } else {
            $wpdb->insert($table, $data, $formats);
            $customer_id = (int) $wpdb->insert_id;
            $notice = '<div class="notice notice-success" style="margin:12px 0;padding:8px 12px;background:#d7f5d7;border-radius:4px;">Customer added.</div>';
        }

        // Reflect the save immediately on whichever view sent it.
        $_GET['viewCustomer'] = $customer_id;
    }

    return $notice;
}

/**
 * The searchable list view.
 */
function paintops_render_customers_list(): string {
    global $wpdb;
    $table = paintops_table('customers');

    // Standardized screen title (2026-07-19 consistency pass); the list
    // is the Customers "page", detail/edit render their own titles.
    $title_html = function_exists('paintops_screen_title') ? paintops_screen_title('Customers') : '';

    // Deliberately NOT named "s", that's WordPress's own reserved search
    // query var, and requesting ?customers=1&s=... triggers WP's native
    // search (is_search()) before this shortcode content ever renders,
    // showing the theme's "nothing found" search-results page instead.
    // Read-only display filter (a search box on the staff customer list,
    // like any WP search form): it changes what is SHOWN, never any state,
    // and the whole screen is already behind the app's capability gate. A
    // nonce here would only break bookmarked/shared search URLs.
    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    $search = isset($_GET['custsearch']) ? sanitize_text_field(wp_unslash($_GET['custsearch'])) : '';

    if ($search !== '') {
        $like = '%' . $wpdb->esc_like($search) . '%';
        // Matching first_name/last_name separately misses a query like
        // "George Smith", neither column alone contains that whole
        // string. CONCAT'ing the two together catches full-name searches
        // too, on top of the existing single-field matches.
        $customers = $wpdb->get_results($wpdb->prepare(
            "SELECT * FROM %i
             WHERE first_name LIKE %s OR last_name LIKE %s OR CONCAT(first_name, ' ', last_name) LIKE %s
                OR email LIKE %s OR phone LIKE %s OR service_address LIKE %s
             ORDER BY last_name ASC, first_name ASC
             LIMIT 200", $table, $like, $like, $like, $like, $like, $like));
    } else {
        $customers = $wpdb->get_results( $wpdb->prepare("SELECT * FROM %i ORDER BY last_name ASC, first_name ASC LIMIT 200", $table) );
    }

    ob_start();
    echo wp_kses($title_html, paintops_kses_allowed_html(), paintops_kses_protocols());
    paintops_enqueue_style('customers-list');
    ?>

    <form method="get" class="paintops-customers-search">
        <input type="hidden" name="customers" value="1">
        <input type="text" name="custsearch" value="<?php echo esc_attr($search); ?>" placeholder="Search by name, email, phone, or address">
        <button type="submit" class="paintops-btn paintops-btn-secondary">Search</button>
        <?php if ($search !== '') : ?>
            <a href="<?php echo esc_url(paintops_admin_url(['customers' => 1])); ?>" class="paintops-btn paintops-btn-secondary">Clear</a>
        <?php endif; ?>
        <a href="<?php echo esc_url(paintops_admin_url(['customers' => 1, 'newCustomer' => 1])); ?>" class="paintops-btn paintops-btn-primary" style="margin-left:auto;">+ New Customer</a>
    </form>

    <?php if (empty($customers)) : ?>
        <p style="color:#777;"><?php echo esc_attr($search !== '' ? 'No customers match that search.' : 'No customers yet, they show up here automatically once you add an email to a job or quote, or you can add one directly.'); ?></p>
    <?php else : ?>
        <table class="paintops-customers-table">
            <thead>
                <tr><th>Name</th><th>Email</th><th>Phone</th><th>Primary Address</th><th>Actions</th></tr>
            </thead>
            <tbody>
                <?php
                // Country-driven State/Zip labels, same source as the full
                // Edit Customer form below.
                $custAddrFmt = function_exists('paintops_address_format') ? paintops_address_format() : ['state_label' => 'State', 'state_maxlength' => 2, 'state_uppercase' => true, 'zip_label' => 'Zip'];
                $custStateMaxlength = ((int) $custAddrFmt['state_maxlength'] > 0) ? ' maxlength="' . (int) $custAddrFmt['state_maxlength'] . '"' : '';
                $custStateUppercase = !empty($custAddrFmt['state_uppercase']) ? ' style="text-transform:uppercase;"' : '';
                ?>
                <?php foreach ($customers as $customer) :
                    $name = trim($customer->first_name . ' ' . $customer->last_name);
                    $address = paintops_customer_address_line($customer);
                ?>
                    <tr>
                        <td data-label="Name"><a href="<?php echo esc_url(paintops_admin_url(['customers' => 1, 'viewCustomer' => (int) $customer->id])); ?>"><?php echo esc_html($name !== '' ? $name : 'Customer #' . $customer->id); ?></a></td>
                        <td data-label="Email"><?php echo esc_html($customer->email); ?></td>
                        <td data-label="Phone"><?php echo wp_kses(paintops_customer_phone_display($customer->phone), paintops_kses_allowed_html(), paintops_kses_protocols()); ?></td>
                        <td data-label="Address">
                            <div class="po-cust-addr">
                                <span class="po-cust-addr-line"><?php echo wp_kses($address !== '' ? esc_html($address) : '<span style="color:var(--po-text-muted,#999);">No address on file</span>', paintops_kses_allowed_html(), paintops_kses_protocols()); ?></span>
                                <button type="button" class="po-cust-addr-edit" title="Edit primary address" aria-label="Edit primary address"><span class="dashicons dashicons-edit" aria-hidden="true"></span></button>
                                <form method="post" class="po-cust-addr-form">
                                    <?php wp_nonce_field('paintops_save_customer_address_action'); ?>
                                    <input type="hidden" name="paintops_save_customer_address" value="1">
                                    <input type="hidden" name="customer_id" value="<?php echo (int) $customer->id; ?>">
                                    <input type="text" class="po-ca-street" name="service_address" placeholder="Street" autocapitalize="words" value="<?php echo esc_attr($customer->service_address ?? ''); ?>">
                                    <input type="text" class="po-ca-city" name="service_city" placeholder="City" autocapitalize="words" value="<?php echo esc_attr($customer->service_city ?? ''); ?>">
                                    <input type="text" class="po-ca-state" name="service_state" placeholder="<?php echo esc_attr($custAddrFmt['state_label']); ?>" autocapitalize="characters"<?php echo wp_kses($custStateMaxlength, paintops_kses_allowed_html(), paintops_kses_protocols()); ?> value="<?php echo esc_attr($customer->service_state ?? ''); ?>"<?php echo wp_kses($custStateUppercase, paintops_kses_allowed_html(), paintops_kses_protocols()); ?>>
                                    <input type="text" class="po-ca-zip" name="service_zip" inputmode="numeric" placeholder="<?php echo esc_attr($custAddrFmt['zip_label']); ?>" value="<?php echo esc_attr($customer->service_zip ?? ''); ?>">
                                    <button type="submit" class="paintops-btn paintops-btn-primary">Save</button>
                                    <button type="button" class="paintops-btn paintops-btn-secondary po-cust-addr-cancel">Cancel</button>
                                </form>
                            </div>
                        </td>
                        <td data-label="Actions"><a href="<?php echo esc_url(paintops_admin_url(['newJob' => 1, 'customerId' => (int) $customer->id])); ?>" class="paintops-btn paintops-btn-secondary">New Request</a></td>
                    </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
        <?php paintops_enqueue_script('customers-address-edit'); ?>
    <?php endif; ?>
    <?php
    return ob_get_clean();
}

/**
 * The "add a new customer by hand" form.
 */
function paintops_render_customer_form(?object $customer = null): string {
    $id = $customer->id ?? 0;

    ob_start();
    ?>
    <h2 class="po-screen-title"><?php echo esc_attr($id ? 'Edit Customer' : 'New Customer'); ?></h2>

    <?php if (function_exists('paintops_maps_api_key') && paintops_maps_api_key() !== '') :
        // The autocomplete behavior lives in assets/js/customers-address-autocomplete.js
        // (it defines the initCustomerAddressAutocomplete callback). It is a
        // dependency of the Maps loader below, so the callback global is
        // guaranteed to exist before the async Maps API can call it.
        paintops_enqueue_script('customers-address-autocomplete');
        wp_enqueue_script(
            'paintops-google-maps-customers',
            'https://maps.googleapis.com/maps/api/js?key=' . paintops_maps_api_key() . '&loading=async&libraries=places&callback=initCustomerAddressAutocomplete',
            ['paintops-customers-address-autocomplete'],
            null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Google serves the current Maps JS; a version pin is not applicable
            ['strategy' => 'async', 'in_footer' => true]
        );
    else : ?>
    <p style="color:#b00;"><em>Address autocomplete is disabled: no Google Maps API key is set. <a href="<?php echo esc_url(paintops_settings_open_url('admin.php?page=paintops-settings&po_open=setup-health&po_entry=google-maps-key')); ?>">Add one in PaintOps &gt; Settings</a>.</em></p>
    <?php endif; ?>

    <div class="job-form">
    <form method="post">
        <?php wp_nonce_field('paintops_save_customer_action'); ?>
        <input type="hidden" name="customer_id" value="<?php echo esc_attr((string) $id); ?>">

        <label>First Name</label>
        <input type="text" name="first_name" autocomplete="given-name" autocapitalize="words" value="<?php echo esc_attr($customer->first_name ?? ''); ?>">

        <label>Last Name</label>
        <input type="text" name="last_name" autocomplete="family-name" autocapitalize="words" value="<?php echo esc_attr($customer->last_name ?? ''); ?>">

        <label>Email</label>
        <input type="email" name="email" inputmode="email" autocomplete="email" autocapitalize="none" spellcheck="false" value="<?php echo esc_attr($customer->email ?? ''); ?>">

        <label>Phone</label>
        <input type="tel" name="phone" inputmode="tel" autocomplete="tel" value="<?php echo esc_attr($customer->phone ?? ''); ?>">

        <label>Primary Address</label>
        <input type="text" id="service_address" name="service_address" autocomplete="street-address" autocapitalize="words" placeholder="i.e. 1234 Easy St" value="<?php echo esc_attr($customer->service_address ?? ''); ?>">

        <?php
        // Country (Business Settings > Company Profile) drives the
        // State/Zip labels here too, same as the New Request job form.
        $paintopsAddrFmt = function_exists('paintops_address_format') ? paintops_address_format() : ['state_label' => 'State', 'state_maxlength' => 2, 'state_uppercase' => true, 'zip_label' => 'Zip'];
        $stateMaxlengthAttr = ((int) $paintopsAddrFmt['state_maxlength'] > 0) ? ' maxlength="' . (int) $paintopsAddrFmt['state_maxlength'] . '"' : '';
        $stateUppercaseStyle = !empty($paintopsAddrFmt['state_uppercase']) ? ' style="text-transform:uppercase;"' : '';
        ?>
        <div class="addr-row" style="grid-template-columns: 2fr 1fr 1fr;">
            <div>
                <label>City</label>
                <input type="text" id="service_city" name="service_city" autocomplete="address-level2" autocapitalize="words" value="<?php echo esc_attr($customer->service_city ?? ''); ?>">
            </div>
            <div>
                <label><?php echo esc_html($paintopsAddrFmt['state_label']); ?></label>
                <input type="text" id="service_state" name="service_state" autocomplete="address-level1" autocapitalize="characters"<?php echo wp_kses($stateMaxlengthAttr, paintops_kses_allowed_html(), paintops_kses_protocols()); ?> value="<?php echo esc_attr($customer->service_state ?? ''); ?>"<?php echo wp_kses($stateUppercaseStyle, paintops_kses_allowed_html(), paintops_kses_protocols()); ?>>
            </div>
            <div>
                <label><?php echo esc_html($paintopsAddrFmt['zip_label']); ?></label>
                <input type="text" id="service_zip" name="service_zip" inputmode="numeric" autocomplete="postal-code" value="<?php echo esc_attr($customer->service_zip ?? ''); ?>">
            </div>
        </div>

        <label>Notes</label>
        <textarea name="notes" rows="4"><?php echo esc_textarea($customer->notes ?? ''); ?></textarea>

        <div class="paintops-sticky-save">
            <button type="submit" name="paintops_save_customer" value="1" class="button button-primary">Save Customer</button>
        </div>
    </form>
    </div>
    <?php
    return ob_get_clean();
}

/**
 * The detail view: contact info, an edit toggle, and every job/quote
 * linked to this customer with a running lifetime value.
 */
function paintops_render_customer_detail(int $customer_id): string {
    global $wpdb;

    $customer = $wpdb->get_row($wpdb->prepare(
        "SELECT * FROM %i WHERE id = %d LIMIT 1", paintops_table('customers'), $customer_id));

    if (!$customer) {
        return '<p>Customer not found.</p><a href="' . esc_url(paintops_admin_url(['customers' => 1])) . '" class="paintops-back-link"><span class="dashicons dashicons-arrow-left-alt2" aria-hidden="true"></span>Back to Customers</a>';
    }

    if (isset($_GET['editCustomer'])) {
        return '<a href="' . esc_url(paintops_admin_url(['customers' => 1, 'viewCustomer' => $customer_id])) . '" class="paintops-back-link"><span class="dashicons dashicons-arrow-left-alt2" aria-hidden="true"></span>Back to Customer</a>' . paintops_render_customer_form($customer);
    }

    $jobs = $wpdb->get_results($wpdb->prepare(
        "SELECT * FROM %i WHERE customer_id = %d ORDER BY startYear DESC, startMonth DESC, startDay DESC", paintops_table('jobs'), $customer_id));

    $lifetime_value = paintops_customer_lifetime_value($jobs);
    $name = trim($customer->first_name . ' ' . $customer->last_name);
    $address = paintops_customer_address_line($customer);

    ob_start();
    paintops_enqueue_style('customers-detail');
    ?>

    <a href="<?php echo esc_url(paintops_admin_url(['customers' => 1])); ?>" class="paintops-back-link"><span class="dashicons dashicons-arrow-left-alt2" aria-hidden="true"></span>Back to Customers</a>

    <div class="paintops-customer-card">
        <h2 class="po-screen-title" style="margin-top:0;"><?php echo esc_html($name !== '' ? $name : 'Customer #' . $customer->id); ?></h2>
        <?php if (!empty($customer->email)) : ?><p>Email: <?php echo esc_html($customer->email); ?></p><?php endif; ?>
        <?php if (!empty($customer->phone)) : ?><p>Phone: <?php echo wp_kses(paintops_customer_phone_display($customer->phone), paintops_kses_allowed_html(), paintops_kses_protocols()); ?></p><?php endif; ?>
        <?php if ($address !== '') : ?><p>Primary Address: <?php echo esc_html($address); ?></p><?php endif; ?>
        <?php
        // Known addresses (2026-07-19, Mike's multi-address ask, derived
        // design): every distinct service address across this customer's
        // jobs, with usage counts. Grows automatically as jobs are added,
        // nothing to maintain. Only shown when there's more than the
        // primary, a single-address customer sees no extra noise.
        global $wpdb;
        $known_addresses = $wpdb->get_results($wpdb->prepare(
            "SELECT custServiceAddress, custServiceCity, custServiceState, custServiceZip, COUNT(*) AS uses
             FROM %i
             WHERE customer_id = %d AND custServiceAddress != ''
             GROUP BY custServiceAddress, custServiceCity, custServiceState, custServiceZip
             ORDER BY uses DESC", paintops_table('jobs'), $customer_id));
        // Merge rows that are really the SAME street (Mike 2026-07-23:
        // a punch-list visit saved before the state-field copy fix in
        // job-followups.php stored state='', so the SQL GROUP BY above
        // yielded "Venice FL 34285" and "Venice 34285" as two rows for
        // one address). Keyed on the normalized street; when variants
        // collide, their job counts add up and the variant with the
        // most complete city/state/zip wins the display. The primary is
        // still excluded, it's already shown above.
        $ka_norm = function ($s) {
            return strtolower(preg_replace('/\s+/', ' ', trim(stripslashes((string) $s))));
        };
        $ka_primary_norm = $ka_norm($customer->service_address);
        $ka_by_street = [];
        foreach ((array) $known_addresses as $ka) {
            $ka_street = $ka_norm($ka->custServiceAddress);
            if ($ka_street === '' || $ka_street === $ka_primary_norm) {
                continue;
            }
            if (isset($ka_by_street[$ka_street])) {
                $ka_prev = $ka_by_street[$ka_street];
                $ka_merged_uses = (int) $ka_prev->uses + (int) $ka->uses;
                $ka_prev_detail = strlen(trim((string) $ka_prev->custServiceCity . (string) $ka_prev->custServiceState . (string) $ka_prev->custServiceZip));
                $ka_new_detail  = strlen(trim((string) $ka->custServiceCity . (string) $ka->custServiceState . (string) $ka->custServiceZip));
                if ($ka_new_detail > $ka_prev_detail) {
                    $ka_by_street[$ka_street] = $ka;
                }
                $ka_by_street[$ka_street]->uses = $ka_merged_uses;
            } else {
                $ka_by_street[$ka_street] = $ka;
            }
        }
        $extra_addresses = array_values($ka_by_street);
        if ($extra_addresses) : ?>
        <p style="margin-bottom:4px;"><strong>Other addresses on file:</strong></p>
        <?php foreach ($extra_addresses as $ka) :
            $ka_line = trim($ka->custServiceAddress . ', ' . $ka->custServiceCity . ' ' . (string) ($ka->custServiceState ?? '') . ' ' . $ka->custServiceZip);
            $ka_line = preg_replace('/\s+/', ' ', $ka_line);
        ?>
        <p style="margin:2px 0;font-size:14px;">&bull; <?php echo esc_html(trim($ka_line, ', ')); ?> <span style="color:var(--po-text-muted,#667581);">(<?php echo (int) $ka->uses; ?> job<?php echo (int) $ka->uses === 1 ? '' : 's'; ?>)</span></p>
        <?php endforeach; ?>
        <?php endif; ?>
        <?php if (!empty($customer->notes)) : ?><p>Notes: <?php echo esc_html($customer->notes); ?></p><?php endif; ?>
        <?php if (function_exists('paintops_user_can_view_financials') && paintops_user_can_view_financials()) : ?>
        <p style="margin-top:12px;font-size:18px;"><strong>Lifetime Value: <?php echo esc_html(paintops_currency_symbol() . number_format($lifetime_value, 2)); ?></strong></p>
        <?php endif; ?>
        <p>
            <a href="<?php echo esc_url(paintops_admin_url(['customers' => 1, 'viewCustomer' => $customer_id, 'editCustomer' => 1])); ?>" class="paintops-btn paintops-btn-secondary">Edit Customer</a>
            <a href="<?php echo esc_url(paintops_admin_url(['newJob' => 1, 'customerId' => $customer_id])); ?>" class="paintops-btn paintops-btn-primary">New Request</a>
        </p>
    </div>

    <div class="job-form-section">Jobs &amp; Quotes</div>
    <?php if (empty($jobs)) : ?>
        <p style="color:#777;">No jobs or quotes linked to this customer yet.</p>
    <?php else : ?>
        <table class="paintops-customer-jobs-table">
            <thead>
                <tr><th>Type</th><th>Job</th><th>Property</th><th>Date</th><th>Status</th></tr>
            </thead>
            <tbody>
                <?php foreach ($jobs as $job) :
                    $category = function_exists('paintops_get_category_details') ? (string) paintops_get_category_details((int) $job->jobCat, 0) : '';
                    $label = ((int) $job->jobType === 2) ? 'Job' : 'Quote';
                    $date = ((int) $job->startYear > 0)
                        ? date('M j, Y', mktime(0, 0, 0, (int) $job->startMonth, (int) $job->startDay, (int) $job->startYear))
                        : '-';
                    $status = ((int) $job->completed === 1) ? 'Completed' : 'Active';
                    // Each job stores its own service address independently
                    // of the customer record's address above, so a
                    // customer with multiple properties still shows the
                    // right one per job here, even though the Customers
                    // profile itself only keeps a single primary address.
                    $job_address = trim($job->custServiceAddress . ', ' . $job->custServiceCity, ' ,');
                ?>
                    <tr>
                        <td data-label="Type"><?php echo esc_html($label); ?></td>
                        <td data-label="Job"><a href="<?php echo esc_url(paintops_admin_url(['viewEid' => (int) $job->id])); ?>"><?php echo esc_html(trim($category . ' ' . $job->jobTitle)); ?></a></td>
                        <td data-label="Property"><?php echo esc_html($job_address !== '' ? $job_address : '-'); ?></td>
                        <td data-label="Date"><?php echo wp_kses_post($date); ?></td>
                        <td data-label="Status"><?php echo esc_html($status); ?></td>
                    </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
    <?php endif; ?>

    <?php
    // Delete Customer, administrators only. A full, permanent wipe (chosen
    // by Mike): the customer plus every linked job/quote and all attached
    // data. Gated behind a details toggle + an acknowledgment checkbox + a
    // JS confirm so it can't happen by accident. For a test entry or to
    // honor a data deletion (GDPR-style) request.
    if (current_user_can('manage_options')) :
        $job_count = count($jobs);
    ?>
        <details class="paintops-danger">
            <summary>Delete this customer</summary>
            <div class="paintops-danger-body">
                <p><strong>Permanently deletes this customer</strong> and everything attached to them<?php
                    echo esc_html($job_count > 0
                        ? ': all ' . (int) $job_count . ' linked job' . ($job_count === 1 ? '' : 's') . ' or quote' . ($job_count === 1 ? '' : 's') . ', plus their estimates, financials, photos, receipts, and payments'
                        : ' (no linked jobs or quotes yet)');
                ?>. This cannot be undone.</p>
                <p>Use this to remove a test entry, or to honor a customer data deletion request.</p>
                <form method="post" onsubmit="return confirm('Permanently delete this customer and all linked jobs, quotes, photos, and records? This cannot be undone.');">
                    <?php wp_nonce_field('paintops_delete_customer_action'); ?>
                    <input type="hidden" name="customer_id" value="<?php echo esc_attr((string) $customer_id); ?>">
                    <label><input type="checkbox" name="paintops_delete_customer_ack" value="1"> I understand this permanently deletes this customer and all attached records.</label>
                    <button type="submit" name="paintops_delete_customer" value="1" class="paintops-btn paintops-danger-btn">Permanently Delete Customer</button>
                </form>
            </div>
        </details>
    <?php endif; ?>
    <?php
    return ob_get_clean();
}

/**
 * Front controller for the whole ?customers=1 area.
 */
function paintops_render_customers_page(): string {
    if (!paintops_user_can_manage()) {
        return '<p>You do not have permission to view customers.</p>';
    }

    $notice = paintops_handle_customers_post();

    if (isset($_GET['viewCustomer'])) {
        return $notice . paintops_render_customer_detail((int) $_GET['viewCustomer']);
    }

    if (isset($_GET['newCustomer'])) {
        return $notice
            . '<a href="' . esc_url(paintops_admin_url(['customers' => 1])) . '" class="paintops-back-link"><span class="dashicons dashicons-arrow-left-alt2" aria-hidden="true"></span>Back to Customers</a>'
            . paintops_render_customer_form();
    }

    return $notice . paintops_render_customers_list();
}