<?php

namespace SugarCalendarBookings\Calendar\Admin;

use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use SugarCalendarBookings\Appointments\AppointmentModel;
use SugarCalendarBookings\Availability\WorkingPeriod;
use SugarCalendarBookings\Availability\WorkingPeriodsResolver;
use SugarCalendarBookings\Availability\WorkingScheduleService;
use SugarCalendarBookings\Calendar\CalendarDisplayRangeService;
use SugarCalendarBookings\Calendar\CalendarHelpers;
use SugarCalendarBookings\Calendar\DisplayBounds;
use SugarCalendarBookings\Calendar\DisplayBoundsContext;
use SugarCalendarBookings\Options;
use SugarCalendarBookings\Plugin;
use SugarCalendarBookings\Services\ServiceModel;
use SugarCalendarBookings\Support\Helpers\Date;
use SugarCalendarBookings\Support\Helpers\Helpers;
use SugarCalendarBookings\Support\Helpers\Time;
use SugarCalendarBookings\Support\Helpers\UI;

/**
 * Calendar week view.
 *
 * @since 1.0.0
 */
class CalendarWeekView extends AbstractCalendarView {

	/**
	 * Display bounds.
	 *
	 * @since 1.4.0
	 *
	 * @var DisplayBounds|null
	 */
	private $display_bounds;

	/**
	 * Full display bounds (all days, ignoring screen options).
	 *
	 * @since 1.4.0
	 *
	 * @var DisplayBounds|null
	 */
	private $full_bounds;

	/**
	 * View mode.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	const MODE = 'week';

	/**
	 * Get pagination first link URL.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	protected function pagination_get_first_link_url() {

		return $this->query->get_url_with_date( $this->query->get_previous_month() );
	}

	/**
	 * Get pagination previous link URL.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	protected function pagination_get_previous_link_url() {

		return $this->query->get_url_with_date( $this->query->get_previous_week() );
	}

	/**
	 * Get pagination next link URL.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	protected function pagination_get_next_link_url() {

		return $this->query->get_url_with_date( $this->query->get_next_week() );
	}

	/**
	 * Get pagination last link URL.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	protected function pagination_get_last_link_url() {

		return $this->query->get_url_with_date( $this->query->get_next_month() );
	}

	/**
	 * Get pagination first link text.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	protected function pagination_get_first_link_text() {

		return esc_html__( 'Previous Month', 'sugar-calendar-bookings-scheduling-appointments-lite' );
	}

	/**
	 * Get pagination previous link text.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	protected function pagination_get_previous_link_text() {

		return esc_html__( 'Previous Week', 'sugar-calendar-bookings-scheduling-appointments-lite' );
	}

	/**
	 * Get pagination next link text.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	protected function pagination_get_next_link_text() {

		return esc_html__( 'Next Week', 'sugar-calendar-bookings-scheduling-appointments-lite' );
	}

	/**
	 * Get pagination last link text.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	protected function pagination_get_last_link_text() {

		return esc_html__( 'Next Month', 'sugar-calendar-bookings-scheduling-appointments-lite' );
	}

	/**
	 * Get column options.
	 *
	 * @since 1.0.0
	 * @since 1.3.0 Removed Week column from column options.
	 *
	 * @return array
	 */
	public function get_column_options() {

		return array_values(
			array_filter(
				array_map(
					function ( $slot ) {

						if ( is_a( $slot, WeekCalendarSlot::class ) ) {
							return null;
						}

						return $slot->get_start()->format( 'l' );
					},
					$this->get_header_slots()
				)
			)
		);
	}

	/**
	 * Get header slots.
	 *
	 * @since 1.0.0
	 *
	 * @return array<BaseCalendarSlot>
	 */
	protected function get_header_slots() {

		$slots = $this->get_slots(
			$this->query->get_start_date(),
			$this->query->get_end_date()
		);

		// Insert week column.
		$week_slot = new WeekCalendarSlot( $slots[0]->get_start(), $slots[0]->get_end() );

		array_unshift( $slots, $week_slot );

		return $slots;
	}

	/**
	 * Get body slots.
	 *
	 * @since 1.0.0
	 *
	 * @return array<BaseCalendarSlot>
	 */
	protected function get_body_slots() {

		return $this->get_header_slots();
	}

	/**
	 * Get header cell class.
	 *
	 * @since 1.0.0
	 *
	 * @param BaseCalendarSlot $slot Calendar slot.
	 *
	 * @return array
	 */
	protected function get_header_cell_class( $slot ) {

		$class = parent::get_header_cell_class( $slot );

		if ( is_a( $slot, WeekCalendarSlot::class ) ) {
			$class[] = 'sugar-calendar-bookings-calendar__cell--week';
		} else {
			$class[] = 'sugar-calendar-bookings-calendar__cell--' . strtolower( $slot->get_start()->format( 'l' ) );

			if ( $slot->is_same_day( $this->query->get_today() ) ) {
				$class[] = 'sugar-calendar-bookings-calendar__cell--today';
			}
		}

		return $class;
	}

	/**
	 * Get body cell class.
	 *
	 * @since 1.0.0
	 *
	 * @param BaseCalendarSlot $slot Calendar slot.
	 *
	 * @return array
	 */
	protected function get_body_cell_class( $slot ) {

		$class = parent::get_body_cell_class( $slot );

		if ( is_a( $slot, WeekCalendarSlot::class ) ) {
			$class[] = 'sugar-calendar-bookings-calendar__cell--week';
		} else {
			$class[] = 'sugar-calendar-bookings-calendar__cell--weekday';
			$class[] = 'sugar-calendar-bookings-calendar__cell--' . strtolower( $slot->get_start()->format( 'l' ) );

			if ( $slot->is_same_day( $this->query->get_today() ) ) {
				$class[] = 'sugar-calendar-bookings-calendar__cell--today';
			}
		}

		return $class;
	}

	/**
	 * Display header cell.
	 *
	 * @since 1.0.0
	 *
	 * @param BaseCalendarSlot $slot Calendar slot.
	 *
	 * @return void
	 */
	protected function display_header_cell( $slot ) {

		if ( is_a( $slot, WeekCalendarSlot::class ) ) : ?>

			<?php
			echo esc_html(
				sprintf(
				/* Translators: %1$s - ISO 8601 week number. */
					__( 'Week %1$s', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
					$slot->get_start()->format( 'W' )
				)
			);
			?>

		<?php else : ?>

			<div class="sugar-calendar-bookings-calendar__day">
				<?php echo esc_html( wp_date( 'M j, D', $slot->get_start()->getTimestamp(), Date::get_default_timezone() ) ); ?>
			</div>

		<?php
		endif;
	}

	/**
	 * Get column identifier for a calendar slot.
	 *
	 * Returns the string used for grid-column placement and the data-column
	 * attribute on hour cells. Pro Day View overrides this for employee slots.
	 *
	 * @since 1.4.0
	 *
	 * @param BaseCalendarSlot $slot Calendar slot.
	 *
	 * @return string
	 */
	protected function get_slot_column_identifier( $slot ) {

		if ( is_a( $slot, WeekCalendarSlot::class ) ) {
			return 'week';
		}

		return strtolower( $slot->get_start()->format( 'l' ) );
	}

	/**
	 * Output cell rows.
	 *
	 * @since 1.0.0
	 *
	 * @param BaseCalendarSlot $slot Calendar slot.
	 */
	private function output_body_cell_rows( $slot ) {

		$grid_column = $this->get_slot_column_identifier( $slot );

		foreach ( $this->get_cell_row_periods( $slot ) as $hour ) {
			$grid_row      = CalendarHelpers::get_hour_grid_template_row_track( $hour );
			$inline_styles = UI::sanitize_inline_styles(
				[
					'grid-row'    => $grid_row,
					'grid-column' => $grid_column,
				]
			);
			?>

			<div class="sugar-calendar-bookings-calendar__cell__hour"
				style="<?php echo esc_attr( $inline_styles ); ?>"
				data-column="<?php echo esc_attr( $grid_column ); ?>"
				data-row="<?php echo esc_attr( $grid_row ); ?>">

				<?php if ( is_a( $slot, WeekCalendarSlot::class ) ) : ?>

					<div>
						<?php echo esc_html( Date::format_display_date( $hour, Plugin::get( Options::class )->get( 'display.time_format' ) ) ); ?>
					</div>

				<?php else : ?>

					<?php CalendarHelpers::add_appointment_button( $hour, $hour->modify( '+ 60 minutes' ) ); ?>

				<?php endif; ?>
			</div>

			<?php
		}
	}

	/**
	 * Get cell row periods.
	 *
	 * @since 1.4.0
	 *
	 * @param BaseCalendarSlot $slot Calendar slot.
	 *
	 * @return DatePeriod
	 */
	private function get_cell_row_periods( $slot ) {

		$bounds = $this->get_full_bounds();

		return CalendarHelpers::get_hours_for_day(
            $slot->get_start(),
            [
				'start' => $bounds->start,
				'end'   => $bounds->end,
            ]
        );
	}

	/**
	 * Get display bounds.
	 *
	 * @since 1.4.0
	 *
	 * @return DisplayBounds
	 */
	protected function get_display_bounds() {

		if ( $this->display_bounds !== null ) {
			return $this->display_bounds;
		}

		$context = new DisplayBoundsContext(
			[
				'dates'        => $this->get_visible_dates(),
				'appointments' => $this->entries,
				'query'        => $this->query,
				'display_tz'   => Date::get_default_timezone(),
			]
		);

		$this->display_bounds = Plugin::get( CalendarDisplayRangeService::class )->get_bounds( $context );

		return $this->display_bounds;
	}

	/**
	 * Ensure a date is DateTimeImmutable so the returned array is safe from mutability.
	 *
	 * @since 1.4.0
	 *
	 * @param DateTimeInterface $date Date from query (may be mutable).
	 *
	 * @return DateTimeImmutable
	 */
	private function to_immutable_date( DateTimeInterface $date ): DateTimeImmutable {

		return $date instanceof DateTimeImmutable ? $date : DateTimeImmutable::createFromMutable( $date );
	}

	/**
	 * Get the visible dates for the current view based on screen options.
	 *
	 * Iterates through each day in the view's date range and excludes
	 * days hidden via screen options.
	 *
	 * @since 1.4.0
	 *
	 * @return DateTimeImmutable[]
	 */
	protected function get_visible_dates(): array {

		$hidden_columns = Plugin::get( CalendarScreenOptions::class )->get( 'hidden_columns' );
		$hidden_days    = array_filter( $hidden_columns[ static::MODE ] ?? [], fn( $col ) => $col !== 'week' );

		$start    = $this->query->get_start_date();
		$end      = $this->query->get_end_date();
		$end_date = $end->format( 'Y-m-d' );
		$dates    = [];
		$current  = $start;

		while ( $current->format( 'Y-m-d' ) <= $end_date ) {
			if ( empty( $hidden_days ) || ! in_array( strtolower( $current->format( 'l' ) ), $hidden_days, true ) ) {
				$dates[] = $this->to_immutable_date( $current );
			}

			$current = $current->modify( '+1 day' );
		}

		// If all days are hidden, fall back to the full range.
		if ( empty( $dates ) ) {
			return $this->get_all_dates();
		}

		return $dates;
	}

	/**
	 * Get display bounds for the full date range, ignoring screen options.
	 *
	 * Used for HTML rendering to ensure all possible hour rows exist in the DOM,
	 * allowing CSS to control visibility via grid-template-rows.
	 *
	 * @since 1.4.0
	 *
	 * @return DisplayBounds
	 */
	protected function get_full_bounds() {

		if ( $this->full_bounds !== null ) {
			return $this->full_bounds;
		}

		$context = new DisplayBoundsContext(
			[
				'dates'        => $this->get_all_dates(),
				'appointments' => $this->entries,
				'query'        => $this->query,
				'display_tz'   => Date::get_default_timezone(),
			]
		);

		$this->full_bounds = Plugin::get( CalendarDisplayRangeService::class )->get_bounds( $context );

		return $this->full_bounds;
	}

	/**
	 * Get all dates in the view's date range.
	 *
	 * @since 1.4.0
	 *
	 * @return DateTimeImmutable[]
	 */
	private function get_all_dates(): array {

		$start    = $this->query->get_start_date();
		$end      = $this->query->get_end_date();
		$end_date = $end->format( 'Y-m-d' );
		$dates    = [];
		$current  = $start;

		while ( $current->format( 'Y-m-d' ) <= $end_date ) {
			$dates[] = $this->to_immutable_date( $current );
			$current = $current->modify( '+1 day' );
		}

		return $dates;
	}

	/**
	 * Output appointment entry.
	 *
	 * @since 1.0.0
	 * @since 1.3.0 Added half-row class for appointments with duration less than 60 minutes.
	 *
	 * @param array $node Calendar node array.
	 *
	 * @return void
	 */
	private function output_appointment_node( $node ) {

		$appointment = $node['appointment'];
		$start_row   = CalendarHelpers::get_hour_grid_template_row_track( $node['grid_slot']->get_start() );
		$end_row     = CalendarHelpers::get_hour_grid_template_row_track( $node['grid_slot']->get_end() );
		$class       = [
			'sugar-calendar-bookings-calendar__entry',
			"sugar-calendar-bookings-calendar__entry--$appointment->status",
		];

		if ( $node['duration'] >= 60 ) {
			$class[] = 'sugar-calendar-bookings-calendar__entry--full-row';
		} else {
			$class[] = 'sugar-calendar-bookings-calendar__entry--half-row';
		}

		$class = UI::sanitize_class( $class );
		$style = UI::sanitize_inline_styles(
			[
				'--entry-color'  => $appointment->service->color,
				'--nesting'      => $node['nesting'],
				'--start-offset' => $node['start_offset'],
				'--end-offset'   => $node['end_offset'],
				'grid-row'       => "$start_row / $end_row",
				'grid-column'    => $node['column'] . ' / -1',
			]
		);
		?>
		<div class="<?php echo esc_attr( $class ); ?>"
			style="<?php echo esc_attr( $style ); ?>"
			data-preview
			data-appointment-id="<?php echo esc_attr( $appointment->id ); ?>">
			<div class="sugar-calendar-bookings-calendar__entry__details">
				<span class="sugar-calendar-bookings-calendar__entry__title"><?php echo esc_html( $appointment->service->name ); ?></span>
				<span class="sugar-calendar-bookings-calendar__entry__time">
					<?php
					echo esc_html(
						sprintf(
						/* Translators: %1$s - appointment start date; %2$s - appointment end date. */
							__( '%1$s - %2$s', 'sugar-calendar-bookings-scheduling-appointments-lite' ),
							Date::format_display_date( $appointment->start_at, 'g:iA' ),
							Date::format_display_date( $appointment->end_at, 'g:iA' )
						)
					);
					?>
				</span>
			</div>
		</div>
		<?php
	}

	/**
	 * Output node cluster.
	 *
	 * @since 1.0.0
	 *
	 * @param array $cluster Cluster data.
	 *
	 * @return void
	 */
	private function output_appointment_cluster( $cluster ) {

		$columns   = $cluster['columns'];
		$start_row = CalendarHelpers::get_hour_grid_template_row_track( $cluster['slot']->get_start() );
		$end_row   = CalendarHelpers::get_hour_grid_template_row_track( $cluster['slot']->get_end() );

		// If end_row is midnight (am-12-00) but the grid doesn't extend that far,
		// use start_row to create a single-row span. This happens for appointments
		// that cross midnight (e.g., 11 PM to 12 AM).
		if ( strpos( $end_row, 'am-12-' ) === 0 ) {
			$end_row = $start_row;
		}

		$style = UI::sanitize_inline_styles(
			[
				'grid-row'              => "$start_row / $end_row",
				'grid-template-columns' => "repeat($columns, 1fr)",
			]
		);
		?>
		<div class="sugar-calendar-bookings-calendar__entry-cluster" style="<?php echo esc_attr( $style ); ?>">
			<?php
			foreach ( $cluster['nodes'] as $node ) {
				$this->output_appointment_node( $node );
			}
			?>
		</div>
		<?php
	}

	/**
	 * Display body cell.
	 *
	 * @since 1.0.0
	 *
	 * @param BaseCalendarSlot        $slot         Calendar slot.
	 * @param array<AppointmentModel> $appointments Appointments.
	 *
	 * @return void
	 */
	protected function display_body_cell( $slot, $appointments ) {

		$this->output_body_cell_rows( $slot );

		if ( is_a( $slot, WeekCalendarSlot::class ) ) {
			return;
		}

		$clusters = CalendarHelpers::get_appointment_clusters( $appointments, $slot );

		foreach ( $clusters as $cluster ) {
			$this->output_appointment_cluster( $cluster );
		}
	}

	/**
	 * Get available hours CSS selectors for the style output.
	 *
	 * Pro overrides this to use employee-specific hours.
	 *
	 * @since 1.4.0
	 *
	 * @return array
	 */
	protected function get_style_available_hours(): array {

		$service_id = $this->query->get_service_id();
		$schedule   = Plugin::get( WorkingScheduleService::class );
		$start      = $this->query->get_start_date();
		$end        = $this->query->get_end_date();

		if ( $service_id !== '' ) {
			$service = ServiceModel::query()->where( 'id', '=', (int) $service_id, '%d' )->with( [ 'meta' ] )->first();

			if ( $service ) {
				$periods = $schedule->get_working_periods( $start, $end, $service );

				return $this->build_available_selectors( $periods, $this->entries );
			}
		}

		// No service filter: resolve global schedule (regular + overrides).
		$rules   = array_merge( $schedule->get_default_regular_rules(), $schedule->get_default_override_rules() );
		$periods = ( new WorkingPeriodsResolver( $rules, $start, $end ) )->get_periods();

		return $this->build_available_selectors( $periods, $this->entries );
	}

	/**
	 * Build available hour CSS selectors from working periods and appointments.
	 *
	 * For each day, computes the union of working hours and appointment hours,
	 * then generates selectors for the full continuous range. This ensures cells
	 * between the schedule edge and out-of-hours appointments are white, not gray.
	 *
	 * @since 1.4.0
	 *
	 * @param WorkingPeriod[] $periods      Resolved working periods (UTC).
	 * @param array           $appointments Appointments in the current view.
	 *
	 * @return array CSS selectors like `[data-row=am-9-00][data-column=monday]`.
	 */
	protected function build_available_selectors( array $periods, array $appointments ): array {

		$tz         = Date::get_default_timezone();
		$day_ranges = $this->collect_day_ranges_from_periods( $periods, $tz );

		$this->extend_day_ranges_from_appointments( $day_ranges, $appointments, $tz );

		return $this->generate_selectors_from_day_ranges( $day_ranges );
	}

	/**
	 * Collect per-day hour ranges from working periods.
	 *
	 * @since 1.4.0
	 *
	 * @param WorkingPeriod[] $periods Working periods (UTC).
	 * @param DateTimeZone    $tz      Display timezone.
	 *
	 * @return array<string, array{column: string, min: int, max: int, date: DateTimeImmutable}> Keyed by Y-m-d.
	 */
	protected function collect_day_ranges_from_periods( array $periods, $tz ): array {

		$day_ranges = [];

		foreach ( $periods as $period ) {
			if ( ! $period instanceof WorkingPeriod ) {
				continue;
			}

			$start = $period->start()->setTimezone( $tz );
			$end   = $period->end()->setTimezone( $tz );
			$day   = $start->format( 'Y-m-d' );

			$this->widen_day_range( $day_ranges, $day, $start, $end );
		}

		return $day_ranges;
	}

	/**
	 * Extend all available day ranges using the global appointment hour range.
	 *
	 * If any appointment falls outside working hours, all available days
	 * in the week extend to that range so the grid is consistent.
	 *
	 * @since 1.4.0
	 *
	 * @param array        $day_ranges   Per-day ranges (passed by reference).
	 * @param array        $appointments Appointments in the current view.
	 * @param DateTimeZone $tz           Display timezone.
	 */
	protected function extend_day_ranges_from_appointments( array &$day_ranges, array $appointments, $tz ): void { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		$global_min = null;
		$global_max = null;

		foreach ( $appointments as $appointment ) {
			if ( ! isset( $appointment->start_at, $appointment->end_at ) ) {
				continue;
			}

			$start_hour = (int) $appointment->start_at->setTimezone( $tz )->format( 'G' );
			$end_dt     = $appointment->end_at->setTimezone( $tz );
			$end_hour   = (int) $end_dt->format( 'G' );

			if ( (int) $end_dt->format( 'i' ) > 0 ) {
				$end_hour = min( 23, $end_hour + 1 );
			}

			$global_min = $global_min === null ? $start_hour : min( $global_min, $start_hour );
			$global_max = $global_max === null ? $end_hour : max( $global_max, $end_hour );
		}

		if ( $global_min === null ) {
			return;
		}

		foreach ( $day_ranges as $day => $range ) {
			$day_ranges[ $day ]['min'] = min( $range['min'], $global_min );
			$day_ranges[ $day ]['max'] = max( $range['max'], $global_max );
		}
	}

	/**
	 * Widen a day's hour range, initializing it if not yet set.
	 *
	 * @since 1.4.0
	 *
	 * @param array             $day_ranges Per-day ranges (passed by reference).
	 * @param string            $day        Date key (Y-m-d).
	 * @param DateTimeImmutable $start      Period start in display timezone.
	 * @param DateTimeImmutable $end        Period end in display timezone.
	 */
	private function widen_day_range( array &$day_ranges, string $day, DateTimeImmutable $start, DateTimeImmutable $end ): void {

		$start_hour = (int) $start->format( 'G' );
		$end_hour   = (int) $end->format( 'G' );

		if ( (int) $end->format( 'i' ) > 0 ) {
			$end_hour = min( 23, $end_hour + 1 );
		}

		if ( ! isset( $day_ranges[ $day ] ) ) {
			$day_ranges[ $day ] = [
				'column' => strtolower( $start->format( 'l' ) ),
				'min'    => $start_hour,
				'max'    => $end_hour,
				'date'   => $start,
			];
		} else {
			$day_ranges[ $day ]['min'] = min( $day_ranges[ $day ]['min'], $start_hour );
			$day_ranges[ $day ]['max'] = max( $day_ranges[ $day ]['max'], $end_hour );
		}
	}

	/**
	 * Generate CSS selectors from per-day hour ranges.
	 *
	 * @since 1.4.0
	 *
	 * @param array $day_ranges Per-day ranges.
	 *
	 * @return array CSS selectors.
	 */
	protected function generate_selectors_from_day_ranges( array $day_ranges ): array {

		$selectors = [];

		foreach ( $day_ranges as $range ) {
			$current = $range['date']->setTime( $range['min'], 0, 0 );
			$end     = $range['date']->setTime( $range['max'], 0, 0 );

			while ( $current <= $end ) {
				$row         = CalendarHelpers::get_hour_grid_template_row_track( $current );
				$selectors[] = "[data-row=$row][data-column={$range['column']}]";
				$current     = $current->modify( '+1 hour' );
			}
		}

		return array_values( array_unique( $selectors ) );
	}

	/**
	 * Output dynamic styles.
	 *
	 * @since 1.0.0
	 * @since 1.4.0 Changed from static to instance method.
	 *
	 * @return void
	 */
	public function output_styles() {

		$hidden_columns        = Plugin::get( CalendarScreenOptions::class )->get( 'hidden_columns' );
		$hidden_columns        = $hidden_columns[ static::MODE ] ?? [];
		$hidden_days           = array_filter( $hidden_columns, fn( $column ) => $column !== 'week' );
		$visible_days          = array_values( array_diff( CalendarHelpers::get_week_days(), $hidden_days ) );
		$full_bounds           = $this->get_full_bounds();
		$display_bounds        = $this->get_display_bounds();
		$full_bounds_array     = [
			'start' => $full_bounds->start,
			'end'   => $full_bounds->end,
		];
		$display_bounds_array  = [
			'start' => $display_bounds->start,
			'end'   => $display_bounds->end,
		];
		$grid_template_rows    = CalendarHelpers::get_hour_grid_template_rows( $this->query->get_start_date(), $full_bounds_array, $display_bounds_array );
		$available_hours       = $this->get_style_available_hours();
		$grid_template_columns = [];

		// Compute hour tracks outside the visible range to hide via display: none.
		$hidden_hour_tracks = CalendarHelpers::get_hidden_hour_tracks( $this->query->get_start_date(), $full_bounds_array, $display_bounds_array );
		$marker_visible     = ! empty( $visible_days ) && $this->is_marker_visible();

		if ( ! in_array( 'week', $hidden_columns, true ) ) {
			$grid_template_columns[] = '[week] 120px';
		}

		foreach ( $visible_days as $day ) {
			$grid_template_columns[] = "[$day] minmax(0, 1fr)";
		}

		Helpers::template(
			'calendar/week-view-styles.php',
			[
				'grid_template_rows'    => $grid_template_rows,
				'grid_template_columns' => $grid_template_columns,
				'available_hours'       => $available_hours,
				'visible_days'          => $visible_days,
				'hidden_columns'        => $hidden_columns,
				'hidden_hour_tracks'    => $hidden_hour_tracks,
				'marker_visible'        => $marker_visible,
			]
		);
	}

	/**
	 * Check if the current time marker should be visible within the display bounds.
	 *
	 * @since 1.4.0
	 *
	 * @return bool
	 */
	protected function is_marker_visible(): bool {

		$display_bounds  = $this->get_display_bounds();
		$now             = Date::create()->setTimezone( Date::get_default_timezone() );
		$current_minutes = Time::minutes_from_midnight( $now );

		return $current_minutes >= $display_bounds->start
			&& $current_minutes < $display_bounds->end + 60;
	}

	/**
	 * Display content after body.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	protected function display_body_after() {

		$now       = Date::create()->setTimezone( Date::get_default_timezone() );
		$half_hour = CalendarHelpers::round_datetime( $now, 60 );
		$minutes   = ( $now->getTimestamp() - $half_hour->getTimestamp() ) / 60;
		$offset    = round( $minutes / 60, 2 ) * 100;
		$row       = CalendarHelpers::get_hour_grid_template_row_track( $half_hour );
		$style     = UI::sanitize_inline_styles(
			[
				'grid-row' => $row,
				'--offset' => "$offset%",
			]
		);

		$this->display_current_time_marker( $style );
	}
}
