<?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.
if (!defined('ABSPATH')) { exit; } // direct-access guard

// Wrapped in a named function (2026-07-27, wp.org hosted Plugin Check run):
// this file only ever runs via require from inside paintops_fetch_csjobs()
// in manage-jobs.php, so every variable below is function-local at runtime.
// PHPCS reads the file in isolation, saw code at file scope, and warned that
// every variable here was an unprefixed global. The wrapper makes the real
// scope explicit. function_exists() guard because the file is require'd (not
// require_once) and some SEO plugins run do_shortcode() early, which can pull
// it in twice in one request.
if (!function_exists('paintops_render_calendar_screen')) {
function paintops_render_calendar_screen() {
global $wpdb;

// Handle "Generate a new link" for the calendar subscribe feed before
// any HTML is built below, so the notice (if any) appears above the
// rest of the page.
$calendarTokenNotice = '';
if (
    isset($_POST['paintops_regen_calendar_token'])
    && paintops_user_can_manage()
    && check_admin_referer('paintops_regen_calendar_token_action')
    && function_exists('paintops_regenerate_calendar_feed_token')
) {
    paintops_regenerate_calendar_feed_token();
    $calendarTokenNotice = '<div class="notice notice-success" style="margin:12px 0;padding:8px 12px;background:#d7f5d7;border-radius:4px;display:inline-block;">Calendar link regenerated. Update it in any calendar app you had it added to.</div>';
}

// --- Configuration ---
$dateToday = gmdate("j", current_time('timestamp'));
$currentMonth = isset($_GET['cmonth']) ? (int)$_GET['cmonth'] : (int)gmdate("m", current_time('timestamp'));
$currentYear = isset($_GET['cyear']) ? (int)$_GET['cyear'] : (int)gmdate("Y", current_time('timestamp'));
$monthName = date("F", mktime(0, 0, 0, $currentMonth, 10));

$monthStart = "$currentYear-" . str_pad($currentMonth, 2, '0', STR_PAD_LEFT) . "-01";
$monthEnd = date('Y-m-t', strtotime($monthStart));

// Worker badge colors (2026-07-19, audit polish): DETERMINISTIC by user
// id, the same worker always gets the same color on every render and
// every device. Replaces the old first-come assignment that hardcoded
// the original Coastal-era worker IDs [1,5,6] and fell back to
// mt_rand() colors that changed on every page load. crc32 spreads
// small sequential ids across the palette well enough; collisions are
// possible past 9 workers on one calendar, which the old scheme
// couldn't handle either.
$colorPalette = [
    '#6C8EA4', '#8CB9A4', '#B3A580', '#A26769',
    '#D1A38F', '#8895A7', '#C3A995', '#A59EB6', '#ADC2A9'
];
$workerColors = [];
$paintops_worker_color = function (int $userId) use ($colorPalette): string {
    return $colorPalette[crc32('po-worker-' . $userId) % count($colorPalette)];
};

$timeOffData = $wpdb->get_results( $wpdb->prepare("SELECT user_id, day, month, year, daysOff, isRepeat FROM %i", paintops_table('timeoff')) );

$timeOffByDate = [];

foreach ($timeOffData as $entry) {
    $userIds = paintops_parse_worker_ids($entry->user_id);
    $daysOff = max((int)$entry->daysOff, 1);

    // Handle repeating time off
    $isRepeat = (bool)$entry->isRepeat;

    // Use current year if it's a repeat event
    $startYear = $isRepeat ? $currentYear : $entry->year;

    $start = mktime(0, 0, 0, $entry->month, $entry->day, $startYear);

    for ($i = 0; $i < $daysOff; $i++) {
        $timestamp = strtotime("+$i day", $start);

        $y = (int)date('Y', $timestamp);
        $m = (int)date('n', $timestamp);
        $d = (int)date('j', $timestamp);

        // Only include dates in the current calendar month/year
        if ($y == $currentYear && $m == $currentMonth) {
            $key = date('Y-m-d', $timestamp);

            if (!isset($timeOffByDate[$key])) {
                $timeOffByDate[$key] = [];
            }

            foreach ($userIds as $userId) {
                $timeOffByDate[$key][] = (int)$userId;
            }
        }
    }
}


// --- Optimized job query by visible range ---
// Crew filter (2026-07-19, v1.0.105): ?crew=<id> narrows the calendar to
// jobs stamped with that crew (see the job form's crew quick-fill). The
// crew_id is bound via prepare; 0/absent = everyone.
$calendarCrewId = isset($_GET['crew']) ? (int) $_GET['crew'] : 0;
// Crew narrowing is part of the one static query: with no crew selected
// the (%d = 0) side is true for every row, so no SQL is ever assembled
// conditionally.
$jobs = $wpdb->get_results($wpdb->prepare(
  "SELECT * FROM %i
  WHERE TO_DAYS(DATE_ADD(CONCAT(startYear, '-', LPAD(startMonth, 2, '0'), '-', LPAD(startDay, 2, '0')), INTERVAL reqDays DAY)) >= TO_DAYS(%s)
    AND TO_DAYS(CONCAT(startYear, '-', LPAD(startMonth, 2, '0'), '-', LPAD(startDay, 2, '0'))) <= TO_DAYS(%s)
    AND not_won != 1
    AND (crew_id = %d OR %d = 0)",
  paintops_table('jobs'), $monthStart, $monthEnd, $calendarCrewId, $calendarCrewId
));

$jobsByDay = [];
foreach ($jobs as $job) {
    $startTimestamp = mktime(0, 0, 0, $job->startMonth, (int)ltrim($job->startDay, '0'), $job->startYear);
		// This is a job
		if ($job->jobType==2){
		$duration = max((int)$job->reqDays, 1);
		}
		// This is a quote. 1 day only
		else{
		$duration = 1;
		}
		
    $userIds = paintops_parse_worker_ids($job->reqWorkers);
    $userBadges = [];
    foreach ($userIds as $userId) {
        $userId = (int)$userId;
        $user = get_userdata($userId);
        if ($user) {
			if (!isset($workerColors[$userId])) {
				$workerColors[$userId] = $paintops_worker_color($userId);
			}

            $badgeName = function_exists('paintops_worker_display_label') ? paintops_worker_display_label($userId) : '';
            if ($badgeName === '') { $badgeName = $user->display_name; }
            $userBadges[] = "<span class='worker-badge' style='background-color: {$workerColors[$userId]}'>" . esc_html($badgeName) . "</span>";
        }
    }

    $job->workerBadges = implode(' ', $userBadges);

    for ($i = 0; $i < $duration; $i++) {
        $timestamp = strtotime("+$i day", $startTimestamp);
        $y = (int)date('Y', $timestamp);
        $m = (int)date('n', $timestamp);
        $d = (int)date('j', $timestamp);

        if ($y == $currentYear && $m == $currentMonth) {
            if (!isset($jobsByDay[$d])) $jobsByDay[$d] = [];
            $jobsByDay[$d][] = $job;
        }
    }
}

// Whether the current user (admin, or a Painter/Helper with the
// "Can create jobs" permission, see paintops_user_can_create_job() in
// paintops.php) may see the "+" add-job button on each day. Same single
// gate used everywhere else a "New Request" link appears.
$paintopsCalendarCanAddJob = function_exists('paintops_user_can_create_job') && paintops_user_can_create_job();

$prevMonth = $currentMonth == 1 ? 12 : $currentMonth - 1;
$prevYear = $currentMonth == 1 ? $currentYear - 1 : $currentYear;
$nextMonth = $currentMonth == 12 ? 1 : $currentMonth + 1;
$nextYear = $currentMonth == 12 ? $currentYear + 1 : $currentYear;

// Built once and reused for both the desktop and mobile nav bars (see
// the mobile-scroll markup further down) so the two never drift,
// round circular arrow buttons flanking the month/year, rather than
// plain "<<"/">>" text sized however the theme's <h1> happens to be.
$calendarPrevArgs = ['calendar' => 1, 'cmonth' => $prevMonth, 'cyear' => $prevYear];
$calendarNextArgs = ['calendar' => 1, 'cmonth' => $nextMonth, 'cyear' => $nextYear];
if ($calendarCrewId > 0) {
    $calendarPrevArgs['crew'] = $calendarCrewId;
    $calendarNextArgs['crew'] = $calendarCrewId;
}
$calendarNavHtml = "<div class='calendar-nav'>"
    . "<a class='calendar-nav-arrow' href='" . paintops_href($calendarPrevArgs) . "' aria-label='Previous month'>&#8592;</a>"
    . "<h1>$monthName $currentYear</h1>"
    . "<a class='calendar-nav-arrow' href='" . paintops_href($calendarNextArgs) . "' aria-label='Next month'>&#8594;</a>"
    . "</div>";

// Holiday labels for the month being viewed, keyed by day-of-month.
// Display-only notes (NOT time-off/scheduling), managed under PaintOps >
// Settings > Calendar Holidays. paintops_calendar_holiday_labels() resolves
// floating rules (e.g. Thanksgiving = 4th Thursday of Nov) to their real
// day for $currentYear, so the right date is highlighted every year.
$paintopsHolidays = function_exists('paintops_calendar_holiday_labels')
    ? paintops_calendar_holiday_labels($currentYear, $currentMonth)
    : [];

// Crew filter chips (v1.0.105), shown only when crews exist: "Everyone"
// plus one chip per active crew (its color as a dot), preserving the
// month being viewed. Shown on both desktop and mobile (part of the
// wrapper, above whichever calendar layout renders).
$calendarCrewChips = '';
$calendarCrews = function_exists('paintops_get_crews') ? paintops_get_crews() : [];
if ($calendarCrews) {
    $chip = function (int $id, string $label, string $color, bool $active) use ($currentMonth, $currentYear): string {
        $url_args = ['calendar' => 1, 'cmonth' => $currentMonth, 'cyear' => $currentYear];
        if ($id > 0) {
            $url_args['crew'] = $id;
        }
        $url = paintops_admin_url($url_args);
        $style = $active
            ? 'background:var(--po-accent,#4682B4);color:#fff;border-color:var(--po-accent,#4682B4);'
            : 'background:var(--po-card-bg,#fff);color:var(--po-text,#333);border-color:var(--po-card-border,#ddd);';
        $dot = ($id > 0 && $color !== '') ? "<span style='display:inline-block;width:10px;height:10px;border-radius:50%;background:" . esc_attr($color) . ";margin-right:6px;vertical-align:middle;'></span>" : '';
        return "<a href='" . esc_url($url) . "' style='display:inline-block;padding:6px 14px;border:1px solid;border-radius:20px;text-decoration:none;font-size:13px;font-weight:600;$style'>" . $dot . esc_html($label) . "</a>";
    };
    $calendarCrewChips = "<div style='display:flex;gap:8px;flex-wrap:wrap;margin:0 0 12px;'>";
    $calendarCrewChips .= $chip(0, 'Everyone', '', $calendarCrewId === 0);
    foreach ($calendarCrews as $cc) {
        $calendarCrewChips .= $chip((int) $cc->id, (string) $cc->name, paintops_sanitize_crew_color((string) $cc->color), $calendarCrewId === (int) $cc->id);
    }
    $calendarCrewChips .= "</div>";
}

$calendar = "<div class='calendar-wrapper'>";
$calendar .= $calendarCrewChips;
$calendar .= $calendarNavHtml;

$calendar .= "<div class='calendar-desktop'><table class='calendar-table'><thead><tr>";
$daysOfWeek = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
foreach ($daysOfWeek as $day) $calendar .= "<th class='calendar-day-head'>$day</th>";
$calendar .= "</tr></thead><tbody><tr class='calendar-row'>";

$running_day = date('w', mktime(0, 0, 0, $currentMonth, 1, $currentYear));
$days_in_month = date('t', mktime(0, 0, 0, $currentMonth, 1, $currentYear));
for ($x = 0; $x < $running_day; $x++) $calendar .= "<td class='calendar-day-np'>&nbsp;</td>";

$day = 1;
for (; $day <= $days_in_month; $day++) {
    $dateKey = $currentYear . '-' . str_pad($currentMonth, 2, '0', STR_PAD_LEFT) . '-' . str_pad($day, 2, '0', STR_PAD_LEFT);
    $timeOffUsers = $timeOffByDate[$dateKey] ?? [];
    $calendar .= "<td class='calendar-day'>";
    $calendar .= ($day == $dateToday && $currentMonth == gmdate("m", current_time('timestamp')))
        ? "<span class='day-number-today'>$day</span>"
        : "<span class='day-number'>$day</span>";

    if ($paintopsCalendarCanAddJob) {
        $addJobUrl = esc_url(paintops_admin_url(['newJob' => 1, 'prefillDate' => $dateKey]));
        $addJobTitle = esc_attr('Add a job on ' . date('M j, Y', strtotime($dateKey)));
        $calendar .= "<a href='$addJobUrl' class='calendar-add-job' title='$addJobTitle' aria-label='Add a job'>+</a>";
    }

    $holidayLabel = $paintopsHolidays[$day] ?? '';
    if ($holidayLabel !== '') {
        $calendar .= "<div class='calendar-holiday'>" . esc_html($holidayLabel) . "</div>";
    }

    if (!empty($jobsByDay[$day])) {
        foreach ($jobsByDay[$day] as $i => $job) {
			$categoryName = paintops_get_category_details($job->jobCat, 0);
			$jobType = $job->jobType;
				if ($jobType==1){
					$isJobType = "Request";
				}
				else{
					$isJobType = "Job";
				}
            // esc_attr(stripslashes(...)) on the tooltip and esc_html() on
            // the visible category/title text, both job description and
            // job title are staff-entered free text and need escaping
            // before being echoed into raw HTML here. The trailing "}"
            // that used to follow $job->id was a leftover half-converted
            // {$job->id} interpolation, not part of a valid URL.
            $calendar .= "<div class='job' title='" . esc_attr(stripslashes($job->jobDesc)) . "'>$isJobType: <a href='" . paintops_href(['viewEid' => (int) $job->id]) . "'>" . esc_html($categoryName) . " " . esc_html($job->jobTitle) . "</a>";

            if (!empty($job->workerBadges)) {
                $calendar .= "<br>{$job->workerBadges}";
            }
            $calendar .= "</div>";
        }
    }

    foreach ($timeOffUsers as $userId) {
        $user = get_userdata($userId);
        if ($user) {
            if (!isset($workerColors[$userId])) {
                $workerColors[$userId] = $paintops_worker_color((int) $userId);
            }
            $calendar .= "<div class='timeoff-badge' style='background-color: {$workerColors[$userId]}'>" . esc_html($user->display_name) . " off</div>";
        }
    }

    $calendar .= "</td>";
    $running_day++;
    if ($running_day == 7 && $day != $days_in_month) {
        $calendar .= "</tr><tr class='calendar-row'>";
        $running_day = 0;
    }
}
while ($running_day > 0 && $running_day < 7) {
    $calendar .= "<td class='calendar-day-np'>&nbsp;</td>";
    $running_day++;
}
$calendar .= "</tr></tbody></table></div>";

$calendar .= "<div class='calendar-mobile-scroll'>" . $calendarNavHtml . "<div class='calendar-mobile'>";
for ($i = 1; $i <= $days_in_month; $i++) {
    $dayName = date('l', mktime(0, 0, 0, $currentMonth, $i, $currentYear));
    $dateKey = $currentYear . '-' . str_pad($currentMonth, 2, '0', STR_PAD_LEFT) . '-' . str_pad($i, 2, '0', STR_PAD_LEFT);
    $todayScrollId = ($i == $dateToday && $currentMonth == gmdate("m", current_time('timestamp')) && $currentYear == gmdate("Y", current_time('timestamp'))) ? " id='scrollToToday'" : "";
	$calendar .= "<div class='calendar-mobile-day'$todayScrollId><div class='calendar-mobile-header'>$dayName, $i</div>";

    $holidayLabelM = $paintopsHolidays[$i] ?? '';
    if ($holidayLabelM !== '') {
        $calendar .= "<div class='calendar-holiday'>" . esc_html($holidayLabelM) . "</div>";
    }

    if ($paintopsCalendarCanAddJob) {
        $addJobUrlMobile = esc_url(paintops_admin_url(['newJob' => 1, 'prefillDate' => $dateKey]));
        $addJobTitleMobile = esc_attr('Add a job on ' . date('M j, Y', strtotime($dateKey)));
        $calendar .= "<a href='$addJobUrlMobile' class='calendar-add-job' title='$addJobTitleMobile' aria-label='Add a job'>+</a>";
    }

    if (!empty($jobsByDay[$i])) {
        foreach ($jobsByDay[$i] as $job) {
			$categoryName = paintops_get_category_details($job->jobCat, 0);
			$jobType = $job->jobType;
				if ($jobType==1){
					$isJobType = "Request";
				}
				else{
					$isJobType = "Job";
				}
            $calendar .= "<div class='calendar-mobile-job' title='" . esc_attr(stripslashes($job->jobDesc)) . "'><b>$isJobType: </b><a href='" . paintops_href(['viewEid' => (int) $job->id]) . "'><strong>" . esc_html($categoryName) . " " . esc_html($job->jobTitle) . "</strong></a><br>{$job->workerBadges}</div>";
        }
    } else {
        $calendar .= "<div class='calendar-mobile-job calendar-no-job'>No jobs</div>";
    }

    if (!empty($timeOffByDate[$dateKey])) {
        foreach ($timeOffByDate[$dateKey] as $userId) {
            $user = get_userdata($userId);
            if ($user) {
                if (!isset($workerColors[$userId])) {
                    $workerColors[$userId] = $paintops_worker_color((int) $userId);
                }
                $calendar .= "<div class='timeoff-badge' style='background-color: {$workerColors[$userId]}'>" . esc_html($user->display_name) . " off</div>";
            }
        }
    }

    $calendar .= "</div>";
}
$calendar .= "</div></div></div>";

// --- Output ---
// NOTE: $paintops_timeoff_url is used below in the "Manage Time Off"
// button. It was previously referenced without ever being assigned,
// so it silently interpolated to an empty string (href="") instead of
// linking anywhere, clicking it just reloaded the current page.
$paintops_timeoff_url = esc_url(paintops_admin_url(['timeOff' => 1]));
$calendarFeedUrlEsc = esc_attr(function_exists('paintops_calendar_feed_url') ? paintops_calendar_feed_url() : '');
$calendarRegenNonce = wp_nonce_field('paintops_regen_calendar_token_action', '_wpnonce', true, false);
// Calendar screen CSS (assets/css/calendar.css) and the mobile
// scroll-to-today script (assets/js/calendar.js) through the enqueue
// APIs (wp.org review 2026-07); both were literal <style>/<script>
// blocks in the template region below.
paintops_enqueue_style('calendar');
paintops_enqueue_script('calendar');
// Raw template output (no heredoc, per wp.org review guidance); every
// dynamic value below is pre-escaped immediately above (esc_url/esc_attr/
// wp_nonce_field).
?>
<?php
// In-app screen heading (2026-07-28): every route prints its own title
// because the app hides the theme's page H1 on app pages; Calendar was
// the one screen relying on that theme H1, so on themes where it is
// hidden the page had no visible title at all.
echo wp_kses(paintops_screen_title('Calendar'), paintops_kses_allowed_html(), paintops_kses_protocols());
?>
<?php echo wp_kses($calendarTokenNotice, paintops_kses_allowed_html(), paintops_kses_protocols()); ?>
<a class="paintops-btn paintops-btn-primary" style="margin:24px 0 12px;" href="<?php echo esc_url($paintops_timeoff_url); ?>">Manage Time Off</a><br><br>

<details style="max-width:640px;margin-bottom:20px;">
  <summary style="cursor:pointer;font-weight:600;font-size:15px;color:var(--po-accent, #4682B4);display:inline-flex;align-items:center;gap:6px;"><span class="dashicons dashicons-calendar-alt" aria-hidden="true" style="font-size:18px;width:18px;height:18px;"></span>Subscribe to this calendar (Google, Apple, Outlook)</summary>
  <div style="margin-top:10px;padding:14px;background:#f5f5f5;border-radius:6px;">
    <p style="margin-top:0;">Paste this link into your calendar app's "Subscribe by URL" / "Add calendar from URL" option to see every job and quote on your own phone or computer calendar. It's read-only, and most calendar apps only check for updates every few hours, that's a limit of how subscribed calendars work, not something PaintOps controls.</p>
    <input type="text" readonly value="<?php echo esc_attr(function_exists('paintops_calendar_feed_url') ? paintops_calendar_feed_url() : ''); ?>" onclick="this.select();" style="width:100%;padding:8px;box-sizing:border-box;margin-bottom:10px;">
    <form method="post" onsubmit="return confirm('Regenerating will break this link for anyone already subscribed to it. Continue?');" style="margin:0;">
      <?php echo wp_kses($calendarRegenNonce, paintops_kses_allowed_html(), paintops_kses_protocols()); ?>
      <button type="submit" name="paintops_regen_calendar_token" value="1" class="paintops-btn paintops-btn-secondary">Generate a new link</button>
    </form>
  </div>
</details>
<div class="calendar-output"><?php echo wp_kses($calendar, paintops_kses_allowed_html(), paintops_kses_protocols()); ?></div>
<?php
?>
<?php
} // end paintops_render_calendar_screen()
} // end function_exists() guard
paintops_render_calendar_screen();
