<?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.
/**
 * PaintOps - Follow-Up Visits (Callbacks / Punch Lists)
 *
 * A callback or punch-list visit is fundamentally different from a new
 * job/quote: the customer, address, and category are already known --
 * the only new information is what needs to happen, when, and who's
 * going. Re-running the full New Job/Quote form for this would mean
 * re-entering everything that hasn't changed.
 *
 * Instead, a follow-up is just another row in the same jobs table
 * (parent_job_id points back at the job it followed up on), so it
 * shows up on Calendar, Pipeline, Dashboard, and worker-conflict-
 * checking with zero extra code anywhere else -- only the *entry form*
 * here is different, not the underlying record. jobType is hardcoded
 * to 2 (Job) for a follow-up, since scheduling one means the work is
 * already agreed to -- there's no Quote/Estimate/Bid acceptance step
 * to go through first.
 */

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

function paintops_visit_types(): array {
    return [
        'callback'   => 'Callback',
        'punch_list' => 'Punch List',
        // Add-On Work (Mike 2026-07-22): extra work the customer approved
        // after the original was sold ("paint the porch as well"). Same
        // mini-form, same child-job machinery; the difference is it's
        // normally BILLABLE, with its own agreed price (see the price
        // field below), so it carries its own balance like any job.
        'addon'      => 'Add-On Work',
    ];
}

/**
 * Small colored badge for a visit_type, used on the job view page and
 * in the Related Visits list. Returns '' for 'original' (no badge on
 * ordinary jobs/quotes).
 */
function paintops_visit_type_badge_html(string $visit_type): string {
    $styles = [
        'callback'   => ['label' => 'CALLBACK',   'bg' => '#f0ad4e'],
        'punch_list' => ['label' => 'PUNCH LIST', 'bg' => '#5bc0de'],
        'addon'      => ['label' => 'ADD-ON',     'bg' => '#5cb85c'],
    ];

    if (!isset($styles[$visit_type])) {
        return '';
    }

    $info = $styles[$visit_type];
    return '<span style="display:inline-block;margin-left:8px;padding:3px 10px;border-radius:12px;background:'
        . esc_attr($info['bg']) . ';color:#fff;font-size:12px;font-weight:700;vertical-align:middle;">'
        . esc_html($info['label']) . '</span>';
}

/**
 * All follow-up visits scheduled off a given job, newest first.
 */
function paintops_get_job_followups(int $parent_job_id): array {
    global $wpdb;

    if ($parent_job_id <= 0) {
        return [];
    }

    return $wpdb->get_results($wpdb->prepare(
        "SELECT id, visit_type, jobDesc, jobTitle, startYear, startMonth, startDay, completed, is_billable
         FROM %i
         WHERE parent_job_id = %d
         ORDER BY startYear DESC, startMonth DESC, startDay DESC", paintops_table('jobs'), $parent_job_id)) ?: [];
}

/**
 * "Related Visits" block for the job view page (?viewEid=X): a link
 * back to the parent job if $row itself is a follow-up, the list of
 * any follow-ups already scheduled off $row, and (staff only) a
 * "+ Schedule Follow-Up" link. Returns '' when there's nothing to show
 * and the viewer isn't staff, matching this plugin's "don't render an
 * empty section" convention (see paintops_render_paint_colors_list()).
 */
function paintops_render_followups_section($row, bool $is_staff): string {
    global $wpdb;

    $parent_job_id = (int) ($row->parent_job_id ?? 0);
    $followups = (function_exists('paintops_user_can_manage') && paintops_user_can_manage())
        ? paintops_get_job_followups((int) $row->id)
        : [];

    // The "+ Schedule Callback / Punch List Visit" action used to live
    // here as a button, but it's now a top-level quick-action link (see
    // paintops_render_job_view_card() in job-view.php) alongside
    // Directions/Add Receipt/Add Photos, so $is_staff no longer gates any
    // content in this function -- it's purely the informational
    // "this is a follow-up of..." / "Related Visits" list now, and both
    // of those are empty for a plain job with no follow-up history.
    if ($parent_job_id <= 0 && !$followups) {
        return '';
    }

    ob_start();

    if ($parent_job_id > 0) {
        $parent_row = $wpdb->get_row($wpdb->prepare(
            "SELECT jobType, jobTitle, jobCat FROM %i WHERE id = %d LIMIT 1", paintops_table('jobs'), $parent_job_id));
        if ($parent_row) {
            $parent_type = function_exists('paintops_job_record_label')
                ? paintops_job_record_label((int) $parent_row->jobType, 0, 'quote')
                : (((int) $parent_row->jobType === 2) ? 'Job' : 'Request');
            $parent_cat = function_exists('paintops_get_category_details') ? paintops_get_category_details((int) $parent_row->jobCat, 0) : '';
            echo wp_kses('<p style="margin-top:14px;"><strong>Follow-up for:</strong> <a href="' . paintops_href(['viewEid' => (int) $parent_job_id]) . '">'
                . esc_html($parent_type) . ' #' . (int) $parent_job_id . ' ' . esc_html(trim($parent_cat . ' ' . $parent_row->jobTitle))
                . '</a></p>', paintops_kses_allowed_html(), paintops_kses_protocols());
        }
    }

    if ($followups) {
        echo '<div style="margin-top:14px;"><strong>Related Visits:</strong><ul style="margin:6px 0 0;padding-left:20px;">';
        foreach ($followups as $f) {
            $f_date = date("M jS, Y", strtotime("{$f->startYear}-{$f->startMonth}-{$f->startDay}"));
            $badge = paintops_visit_type_badge_html((string) $f->visit_type);
            $status = ((int) $f->completed === 1) ? ' - <span style="color:green;">Done</span>' : '';
            $billable = ((int) $f->is_billable === 1) ? 'billable' : 'free/warranty';
            $snippet = wp_trim_words(stripslashes((string) $f->jobDesc), 12);
            echo wp_kses('<li style="margin-bottom:8px;"><a href="' . paintops_href(['viewEid' => (int) $f->id]) . '">' . esc_html($f_date) . '</a> '
                . $badge . ' - ' . esc_html($billable) . $status
                . '<br><span style="color:#777;">' . esc_html($snippet) . '</span></li>', paintops_kses_allowed_html(), paintops_kses_protocols());
        }
        echo '</ul></div>';
    }

    return ob_get_clean();
}

/**
 * Insert a new follow-up job row from the mini-form's POST data.
 * Customer/address/category are re-read from the parent job row
 * (never trusted from a hidden field) so there's no way to submit a
 * follow-up against the wrong customer.
 */
function paintops_save_followup_from_post(int $parent_job_id, $parent): array {
    if (!function_exists('paintops_user_can_manage') || !paintops_user_can_manage()) {
        return ['success' => false, 'message' => 'You do not have permission to do this.', 'new_id' => 0];
    }

    global $wpdb;

    $visit_type = sanitize_key(wp_unslash($_POST['visit_type'] ?? 'callback')); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- these reads run only after the caller verifies the cs_save_followup nonce before invoking this save routine
    if (!array_key_exists($visit_type, paintops_visit_types())) {
        $visit_type = 'callback';
    }

    $jobDesc = sanitize_textarea_field(wp_unslash($_POST['followupDesc'] ?? '')); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- these reads run only after the caller verifies the cs_save_followup nonce before invoking this save routine
    if ($jobDesc === '') {
        return ['success' => false, 'message' => 'Please describe what needs to happen.', 'new_id' => 0];
    }

    $startYear = intval($_POST['startYear'] ?? 0); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- these reads run only after the caller verifies the cs_save_followup nonce before invoking this save routine
    $startMonth = intval($_POST['startMonth'] ?? 0); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- these reads run only after the caller verifies the cs_save_followup nonce before invoking this save routine
    $startDay = intval($_POST['startDay'] ?? 0); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- these reads run only after the caller verifies the cs_save_followup nonce before invoking this save routine
    $startTime = sanitize_text_field(wp_unslash($_POST['startTime'] ?? '')); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- these reads run only after the caller verifies the cs_save_followup nonce before invoking this save routine

    if ($startYear < 2025 || $startYear > 2099 || $startMonth < 1 || $startMonth > 12 || $startDay < 1 || $startDay > 31) {
        return ['success' => false, 'message' => 'Please pick a valid date.', 'new_id' => 0];
    }

    $reqWorkersArr = $_POST['reqWorkers'] ?? []; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- array is iterated below and each element is individually sanitized or cast where consumed; these reads run only after the caller verifies the cs_save_followup nonce before invoking this save routine
    $reqWorkers = '';
    if (is_array($reqWorkersArr)) {
        foreach ($reqWorkersArr as $reqWorker) {
            $reqWorkers .= '#' . intval($reqWorker) . '#';
        }
    }

    $is_billable = !empty($_POST['is_billable']) ? 1 : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- these reads run only after the caller verifies the cs_save_followup nonce before invoking this save routine
    // Agreed price for billable work (typically Add-On Work). Stored as
    // the new job's gross (job_financials.gross_amount, the same row the
    // Financials form writes), so the balance badge, board cards, and
    // Profits all see it immediately, on free and Pro alike. Optional:
    // billable with no price yet just means the price gets set later.
    $followup_price = $is_billable ? round((float) str_replace(',', '', (string) ($_POST['followup_price'] ?? 0)), 2) : 0.0; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- value is cast to a number, sanitization not applicable; these reads run only after the caller verifies the cs_save_followup nonce before invoking this save routine
    $jobTitle = paintops_visit_types()[$visit_type] ?? 'Follow-Up';

    $data = [
        'custId'             => (int) $parent->custId,
        'customer_id'        => (int) $parent->customer_id,
        'custFirst'          => $parent->custFirst,
        'custLast'           => $parent->custLast,
        'custServiceAddress' => $parent->custServiceAddress,
        'custServiceCity'    => $parent->custServiceCity,
        // State was MISSING from this copy list (Mike 2026-07-23: a
        // punch-list visit stored state='' and the customer page showed
        // the same address twice, once with FL and once without).
        'custServiceState'   => (string) ($parent->custServiceState ?? ''),
        'custServiceZip'     => $parent->custServiceZip,
        'custPhone'          => $parent->custPhone,
        'custEmail'          => $parent->custEmail,
        'reqWorkers'         => $reqWorkers,
        'reqDays'            => 0,
        'jobCat'             => (int) $parent->jobCat,
        'jobTitle'           => $jobTitle,
        'jobDesc'            => $jobDesc,
        'estimateID'         => 0,
        'invoiceID'          => 0,
        'startTime'          => $startTime,
        'startDay'           => $startDay,
        'startMonth'         => $startMonth,
        'startYear'          => $startYear,
        'completed'          => 0,
        'addedBy'            => get_current_user_id(),
        'notifsSent'         => 0,
        'jobType'            => 2,
        'isBid'              => 0,
        'quote_label'        => 'quote',
        'jobClass'           => (int) $parent->jobClass,
        'payment_plan'       => $parent->payment_plan,
        'company_id'         => (int) $parent->company_id,
        'parent_job_id'      => $parent_job_id,
        'visit_type'         => $visit_type,
        'is_billable'        => $is_billable,
    ];

    // No explicit $format array here -- $wpdb infers a safe default
    // ('%s', which MySQL casts fine against INT/TINYINT columns) for
    // every field, which is one less place for a column/format
    // ordering mistake to silently corrupt data.
    $inserted = $wpdb->insert(paintops_table('jobs'), $data);

    if (!$inserted) {
        if (function_exists('paintops_report_db_error')) {
            paintops_report_db_error('paintops_save_followup_from_post');
        }
        return ['success' => false, 'message' => 'Something went wrong saving this -- please try again.', 'new_id' => 0];
    }

    $new_id = (int) $wpdb->insert_id;

    if ($followup_price > 0) {
        $wpdb->replace(paintops_table('job_financials'), [
            'job_id' => $new_id,
            'gross_amount' => $followup_price,
            'updated_by' => get_current_user_id(),
        ], ['%d', '%f', '%d']);
    }

    return ['success' => true, 'message' => '', 'new_id' => $new_id];
}

/**
 * The Schedule Follow-Up mini-form page (?scheduleFollowUp=parentJobId).
 * Deliberately short: customer, address, and category are shown
 * read-only (inherited from the parent job) rather than re-collected.
 */
function paintops_render_schedule_followup_form(int $parent_job_id): void {
    global $wpdb;

    if (!function_exists('paintops_user_can_manage') || !paintops_user_can_manage()) {
        echo '<p>You do not have permission to schedule a follow-up.</p>';
        return;
    }

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

    if (!$parent) {
        echo '<p>Job not found.</p>';
        return;
    }

    $parent_type = function_exists('paintops_job_record_label')
        ? paintops_job_record_label((int) $parent->jobType, (int) ($parent->isBid ?? 0), (string) ($parent->quote_label ?? 'quote'))
        : (((int) $parent->jobType === 2) ? 'Job' : 'Request');
    $parent_cat = function_exists('paintops_get_category_details') ? paintops_get_category_details((int) $parent->jobCat, 0) : '';

    if (isset($_POST['cs_save_followup']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['cs_followup_nonce'] ?? '')), 'cs_save_followup_' . $parent_job_id)
        && (!function_exists('paintops_once_check') || paintops_once_check())) {
        $result = paintops_save_followup_from_post($parent_job_id, $parent);
        if ($result['success']) {
            echo '<a href="' . paintops_href(['viewEid' => (int) $parent_job_id]) . '" class="paintops-back-link"><span class="dashicons dashicons-arrow-left-alt2" aria-hidden="true"></span>View ' . esc_html($parent_type) . ' #' . (int) $parent_job_id . '</a>';
            echo '<div style="color:green;margin-bottom:14px;">Follow-up scheduled. <a href="' . paintops_href(['viewEid' => (int) $result['new_id']]) . '">View it here</a>.</div>';
            return; // Don't re-render the entry form after a successful save.
        }
        echo '<div style="color:red;margin-bottom:14px;">' . esc_html($result['message']) . '</div>';
    }
    ?>
    <a href="<?php echo paintops_href(['viewEid' => (int) $parent_job_id]); ?>" class="paintops-back-link"><span class="dashicons dashicons-arrow-left-alt2" aria-hidden="true"></span>View <?php echo esc_html($parent_type); ?> #<?php echo (int) $parent_job_id; ?></a>
    <h2>Schedule a Callback / Punch List Visit</h2>
    <?php
    // $include_scheduled=false: $parent's startYear/startTime is the
    // *original* job's schedule, not the new visit being scheduled
    // below, so showing it here would look like the new visit's date.
    echo wp_kses(function_exists('paintops_render_job_customer_summary') ? paintops_render_job_customer_summary($parent, false) : '', paintops_kses_allowed_html(), paintops_kses_protocols());
    ?>
    <p style="color:#777;margin-top:-8px;">Customer and address carry over automatically<?php echo wp_kses($parent_cat !== '' ? ', category: ' . esc_html($parent_cat) : '', paintops_kses_allowed_html(), paintops_kses_protocols()); ?>.</p>

    <div class="job-form">
    <form method="post">
        <input type="hidden" name="cs_save_followup" value="1">
        <input type="hidden" name="cs_followup_nonce" value="<?php echo esc_attr(wp_create_nonce('cs_save_followup_' . $parent_job_id)); ?>">
        <?php echo wp_kses(function_exists('paintops_once_field') ? paintops_once_field() : '', paintops_kses_allowed_html(), paintops_kses_protocols()); ?>

        <label>Visit Type <span class="paintops-required">*</span></label>
        <div class="paintops-status-pills" id="followupTypePills">
          <div class="paintops-status-pill active" data-value="callback">Callback</div>
          <div class="paintops-status-pill" data-value="punch_list">Punch List</div>
          <div class="paintops-status-pill" data-value="addon">Add-On Work</div>
        </div>
        <input type="hidden" id="followupVisitType" name="visit_type" value="callback">
        <p class="description">Callback: the customer reported something after the fact. Punch List: touch-ups noted at the walkthrough, not yet finished. Add-On Work: extra work the customer approved after the original was sold (i.e. "paint the porch as well"), usually billable with its own price.</p>

        <label for="followupDesc">What needs to happen? <span class="paintops-required">*</span></label>
        <textarea id="followupDesc" name="followupDesc" rows="5" placeholder="i.e. Customer noticed peeling paint on the north wall, needs to be scraped and repainted."></textarea>

        <div class="job-form-section">Scheduling</div>
        <div class="addr-row" style="grid-template-columns: 1fr 1fr;">
            <div>
                <label for="followupDate">Date <span class="paintops-required">*</span></label>
                <input type="date" id="followupDate" min="2025-01-01" max="2099-12-31" value="<?php echo esc_attr(gmdate('Y-m-d', current_time('timestamp'))); ?>">
                <input type="hidden" id="fpyear" name="startYear" value="<?php echo esc_attr(gmdate('Y', current_time('timestamp'))); ?>">
                <input type="hidden" id="fpmonth" name="startMonth" value="<?php echo esc_attr(gmdate('m', current_time('timestamp'))); ?>">
                <input type="hidden" id="fpday" name="startDay" value="<?php echo esc_attr(gmdate('d', current_time('timestamp'))); ?>">
            </div>
            <div>
                <label for="startTime">Arrival Time <span class="paintops-required">*</span></label>
                <select id="startTime" name="startTime">
                    <?php
                    for ($h = 7; $h <= 19; $h++) {
                        for ($m = 0; $m < 60; $m += 15) {
                            $time = sprintf('%02d:%02d', $h, $m);
                            $label = date('g:i A', strtotime($time));
                            echo wp_kses("<option value=\"$time\">$label</option>", paintops_kses_allowed_html(), paintops_kses_protocols());
                        }
                    }
                    ?>
                </select>

                <label for="reqWorkers" style="margin-top:10px;">Workers Or Estimator(s)</label>
                <?php
                $workers = function_exists('paintops_get_assignable_workers') ? paintops_get_assignable_workers() : [];
                if ($workers) {
                    foreach ($workers as $wrow) {
                        $display_name = esc_html($wrow->display_name);
                        $uid = (int) $wrow->ID;
                        $po_tpl_html = "<div style='display: inline-flex; align-items: center; margin-bottom: 8px;'>
    <input style='width: 28px; height: 28px; margin: 0 8px 0 0;' type='checkbox' id='fp_worker_{$uid}' value='{$uid}' name='reqWorkers[]'>
    <label for='fp_worker_{$uid}' style='margin: 0; line-height: 28px;'>{$display_name}</label>
</div><br>";
                        echo wp_kses($po_tpl_html, paintops_kses_allowed_html(), paintops_kses_protocols());
                    }
                }
                ?>
            </div>
        </div>

        <label style="margin-top:14px;">
            <input type="checkbox" id="followupBillable" name="is_billable" value="1" style="width:24px;height:24px;vertical-align:middle;margin-right:8px;">
            This needs its own price / invoice (billable)
        </label>
        <p class="description">Leave unchecked for warranty/touch-up work you're not charging for, the common case for callbacks and punch lists. Check it for paid work like an add-on and enter the agreed price, the new job then carries its own balance until it's collected.</p>

        <div id="followupPriceRow" style="display:none;">
            <label for="followupPrice">Agreed Price (<?php echo esc_html(function_exists('paintops_currency_symbol') ? paintops_currency_symbol() : '$'); ?>)</label>
            <input type="number" id="followupPrice" name="followup_price" min="0" step="0.01" inputmode="decimal" placeholder="i.e. 450">
            <p class="description" style="margin-top:-18px;">Becomes the new job's gross amount. Leave blank to price it later<?php echo wp_kses((function_exists('paintops_pro_active') && paintops_pro_active()) ? ' under Financials &amp; Profitability' : '', paintops_kses_allowed_html(), paintops_kses_protocols()); ?>.</p>
        </div>

        <div class="paintops-sticky-save">
            <button type="submit" class="button button-primary">Schedule Follow-Up</button>
        </div>
    </form>
    </div>
    <?php
    paintops_enqueue_script('job-followups-form');
}
