<?php
/**
 * Service reminders worklist built on WP_List_Table.
 *
 * Renders pending reminders read from the reminders repository under three
 * status-view tabs (Overdue, Due soon, All) each carrying a live count, and
 * defaulting to All so the full worklist lands first. The
 * Customer column is primary and carries the nonce-protected row actions: Mark
 * handled and Dismiss (state-changing, wrapped in wp_nonce_url and gated by the
 * manage-reminders capability), plus plain View vehicle and View work order
 * links. The bucket param is sanitized with sanitize_key and whitelisted against
 * the three known buckets before it reaches any query; it is never interpolated
 * into SQL. Every cell is escaped because it renders customer and vehicle data.
 *
 * @package WrenchPress
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'WP_List_Table' ) ) {
	require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

/**
 * Class WrenchPress_Reminders_List_Table
 */
class WrenchPress_Reminders_List_Table extends WP_List_Table {

	/**
	 * Admin page slug the row actions and status tabs link back to.
	 */
	const PAGE_SLUG = WrenchPress_Reminders_Admin_Page::MENU_SLUG;

	/**
	 * Per-request cache of resolved customer display names, keyed by id.
	 *
	 * @var array<int,string>
	 */
	private $customer_names = array();

	/**
	 * Per-request cache of resolved vehicle labels, keyed by id.
	 *
	 * @var array<int,string>
	 */
	private $vehicle_labels = array();

	/**
	 * Per-request cache of resolved vehicle current mileage (canonical), keyed by id.
	 *
	 * @var array<int,int>
	 */
	private $vehicle_mileage = array();

	/**
	 * The sanitized, whitelisted bucket for the current request. Set in
	 * prepare_items() and read by column_due() to decide overdue emphasis.
	 *
	 * @var string
	 */
	private $bucket = 'all';

	/**
	 * Configure the list table.
	 */
	public function __construct() {
		parent::__construct(
			array(
				'singular' => 'reminder',
				'plural'   => 'reminders',
				'ajax'     => false,
			)
		);
	}

	/**
	 * Visible columns (UI-SPEC: Customer, Vehicle, Due, Status).
	 *
	 * @return array<string,string>
	 */
	public function get_columns() {
		return array(
			'customer' => __( 'Customer', 'pitdesk-repair-crm' ),
			'vehicle'  => __( 'Vehicle', 'pitdesk-repair-crm' ),
			'due'      => __( 'Due', 'pitdesk-repair-crm' ),
			'status'   => __( 'Status', 'pitdesk-repair-crm' ),
		);
	}

	/**
	 * Customer is the primary column (carries the row actions).
	 *
	 * @return string
	 */
	protected function get_default_primary_column_name() {
		return 'customer';
	}

	/**
	 * Status-view tabs with per-bucket counts.
	 *
	 * Builds three tabs (Overdue, Due soon, All), each carrying its live count
	 * from the repository, and hands the link data to the core get_views_links()
	 * helper which renders the anchors and counts. The default landing tab is All
	 * (the full pending worklist); History surfaces terminal rows. The current
	 * bucket request param is sanitized with sanitize_key and only ever compared
	 * against known slugs; it is never interpolated into SQL here.
	 *
	 * @return array<string,string> Map of view id to rendered anchor markup.
	 */
	protected function get_views() {
		$base = add_query_arg( array( 'page' => self::PAGE_SLUG ), admin_url( 'admin.php' ) );
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only status filter from a GET request.
		$current = isset( $_REQUEST['status'] ) ? sanitize_key( wp_unslash( $_REQUEST['status'] ) ) : 'all';

		$link_data = array(
			'overdue'  => array(
				'url'     => add_query_arg( 'status', 'overdue', $base ),
				'label'   => __( 'Overdue', 'pitdesk-repair-crm' ),
				'count'   => WrenchPress_Reminders_Repository::count_due( 'overdue' ),
				'current' => 'overdue' === $current,
			),
			'due_soon' => array(
				'url'     => add_query_arg( 'status', 'due_soon', $base ),
				'label'   => __( 'Due soon', 'pitdesk-repair-crm' ),
				'count'   => WrenchPress_Reminders_Repository::count_due( 'due_soon' ),
				'current' => 'due_soon' === $current,
			),
			'all'      => array(
				'url'     => add_query_arg( 'status', 'all', $base ),
				'label'   => __( 'All', 'pitdesk-repair-crm' ),
				'count'   => WrenchPress_Reminders_Repository::count_due( 'all' ),
				'current' => 'all' === $current,
			),
			'history'  => array(
				'url'     => add_query_arg( 'status', 'history', $base ),
				'label'   => __( 'History', 'pitdesk-repair-crm' ),
				'count'   => WrenchPress_Reminders_Repository::count_history(),
				'current' => 'history' === $current,
			),
		);

		// The 'count' key is read by core get_views_links() to render the bubble.
		return $this->get_views_links( $link_data );
	}

	/**
	 * Per-tab empty-state copy (UI-SPEC lines 115-120).
	 *
	 * @return void
	 */
	public function no_items() {
		if ( 'overdue' === $this->bucket ) {
			echo '<strong>' . esc_html__( 'No overdue reminders', 'pitdesk-repair-crm' ) . '</strong><br />';
			echo esc_html__( 'Nothing is past due right now. A vehicle appears here once its service date or mileage is reached.', 'pitdesk-repair-crm' );
			return;
		}

		if ( 'due_soon' === $this->bucket ) {
			echo '<strong>' . esc_html__( 'No reminders due soon', 'pitdesk-repair-crm' ) . '</strong><br />';
			echo esc_html__( 'No vehicles are due in the next 30 days. Mileage-only reminders appear under Overdue once the mileage is reached, not here.', 'pitdesk-repair-crm' );
			return;
		}

		if ( 'history' === $this->bucket ) {
			echo '<strong>' . esc_html__( 'No reminder history yet', 'pitdesk-repair-crm' ) . '</strong><br />';
			echo esc_html__( 'Reminders you mark handled or dismiss, and ones replaced by a newer reminder, appear here for reference.', 'pitdesk-repair-crm' );
			return;
		}

		echo '<strong>' . esc_html__( 'No active reminders', 'pitdesk-repair-crm' ) . '</strong><br />';
		echo esc_html__( 'Reminders you set when closing a work order will appear here. Close a work order to set the first one.', 'pitdesk-repair-crm' );
	}

	/**
	 * Render the Customer column as "Last, First" with the nonce-protected row
	 * actions: Mark handled, Dismiss, View vehicle, and (when present) View work
	 * order. The two state-changing actions are per-record wp_nonce_url links
	 * gated on MANAGE_REMINDERS; their handlers live on the admin page.
	 *
	 * @param object $item Reminder row.
	 * @return string
	 */
	public function column_customer( $item ) {
		$display = $this->customer_name( (int) $item->customer_id );
		$base    = admin_url( 'admin.php' );
		$actions = array();

		if ( 'pending' === (string) $item->status && current_user_can( WrenchPress_Capabilities::MANAGE_REMINDERS ) ) {
			$mark_url                = wp_nonce_url(
				add_query_arg(
					array(
						'page'     => self::PAGE_SLUG,
						'action'   => 'mark_handled',
						'reminder' => (int) $item->id,
					),
					$base
				),
				WrenchPress_Reminders_Admin_Page::MARK_NONCE . (int) $item->id
			);
			$actions['mark_handled'] = '<a href="' . esc_url( $mark_url ) . '">' . esc_html__( 'Mark handled', 'pitdesk-repair-crm' ) . '</a>';

			$dismiss_url = wp_nonce_url(
				add_query_arg(
					array(
						'page'     => self::PAGE_SLUG,
						'action'   => 'dismiss',
						'reminder' => (int) $item->id,
					),
					$base
				),
				WrenchPress_Reminders_Admin_Page::DISMISS_NONCE . (int) $item->id
			);

			/*
			 * The link keeps its href, and that href keeps its nonce. The data
			 * attributes are read by assets/js/wrenchpress-dialog.js, which
			 * intercepts the click and confirms in a native <dialog>. With the
			 * script absent the link simply navigates, and the server-side nonce
			 * and capability checks are unchanged either way.
			 */
			$actions['dismiss'] = '<a href="' . esc_url( $dismiss_url ) . '"'
				. ' data-wrenchpress-confirm="' . esc_attr__( 'It will be removed from the worklist. This cannot be undone.', 'pitdesk-repair-crm' ) . '"'
				. ' data-wrenchpress-confirm-title="' . esc_attr__( 'Dismiss this reminder?', 'pitdesk-repair-crm' ) . '"'
				. ' data-wrenchpress-confirm-cancel="' . esc_attr__( 'Keep reminder', 'pitdesk-repair-crm' ) . '"'
				. ' data-wrenchpress-confirm-ok="' . esc_attr__( 'Dismiss reminder', 'pitdesk-repair-crm' ) . '"'
				. '>' . esc_html__( 'Dismiss', 'pitdesk-repair-crm' ) . '</a>';
		}

		$vehicle_id = (int) $item->vehicle_id;
		if ( $vehicle_id > 0 ) {
			$vehicle_url             = add_query_arg(
				array(
					'page'    => WrenchPress_Vehicles_Admin_Page::MENU_SLUG,
					'action'  => 'view',
					'vehicle' => $vehicle_id,
				),
				$base
			);
			$actions['view_vehicle'] = '<a href="' . esc_url( $vehicle_url ) . '">' . esc_html__( 'View vehicle', 'pitdesk-repair-crm' ) . '</a>';
		}

		$work_order_id = (int) $item->work_order_id;
		if ( $work_order_id > 0 ) {
			$work_order_url             = add_query_arg(
				array(
					'page'       => WrenchPress_Work_Orders_Admin_Page::MENU_SLUG,
					'action'     => 'view',
					'work_order' => $work_order_id,
				),
				$base
			);
			$actions['view_work_order'] = '<a href="' . esc_url( $work_order_url ) . '">' . esc_html__( 'View work order', 'pitdesk-repair-crm' ) . '</a>';
		}

		return '<strong>' . esc_html( $display ) . '</strong>' . $this->row_actions( $actions );
	}

	/**
	 * Render non-primary columns with escaped output.
	 *
	 * @param object $item        Reminder row.
	 * @param string $column_name Column key.
	 * @return string
	 */
	public function column_default( $item, $column_name ) {
		switch ( $column_name ) {
			case 'vehicle':
				return esc_html( $this->vehicle_label( (int) $item->vehicle_id ) );

			case 'due':
				return $this->column_due( $item );

			case 'status':
				$statuses = WrenchPress_Reminders_Repository::statuses();
				$slug     = (string) $item->status;
				$label    = isset( $statuses[ $slug ] ) ? $statuses[ $slug ] : $slug;
				return '<span class="wrenchpress-status wrenchpress-status--' . esc_attr( $slug ) . '">' . esc_html( $label ) . '</span>';

			default:
				return '';
		}
	}

	/**
	 * Render the Due column: whichever of due date / due mileage is set, date
	 * first, joined with "or" on a second line when both are present. Sentinel
	 * values ('0000-00-00' date, 0 mileage) are never rendered. In the Overdue
	 * bucket the reached value(s) are wrapped in .wrenchpress-due--overdue; a
	 * date is reached when it is on or before today, a mileage when the vehicle's
	 * current mileage has met it. Canonical mileage is converted to the active
	 * display unit at this boundary only.
	 *
	 * @param object $item Reminder row.
	 * @return string
	 */
	private function column_due( $item ) {
		$is_overdue = ( 'overdue' === $this->bucket );
		$today      = current_time( 'Y-m-d' );
		$parts      = array();

		$due_date = (string) $item->due_date;
		if ( '' !== $due_date && '0000-00-00' !== $due_date ) {
			$timestamp = strtotime( $due_date );

			if ( $timestamp ) {
				$rendered     = esc_html( date_i18n( get_option( 'date_format' ), $timestamp ) );
				$date_reached = ( $due_date <= $today );

				/*
				 * The reached trigger must not be signalled by red alone (SC 1.4.1).
				 * The icon and the visually-hidden text say which of the two triggers
				 * (date or mileage) is the one that has been reached, which is the
				 * distinction the colour carries. "(reached)" rather than "Overdue":
				 * the whole table is the Overdue view, so "Overdue" would be noise.
				 */
				$parts['date'] = ( $is_overdue && $date_reached )
					? '<span class="wrenchpress-due--overdue"><span class="dashicons dashicons-warning" aria-hidden="true"></span>' . $rendered . ' <span class="screen-reader-text">' . esc_html__( '(reached)', 'pitdesk-repair-crm' ) . '</span></span>'
					: $rendered;
			}
		}

		$due_mileage = (int) $item->due_mileage;
		if ( $due_mileage > 0 ) {
			$unit          = WrenchPress_Settings_Page::mileage_unit();
			$display       = WrenchPress_Reminders_Repository::mileage_to_display( $due_mileage, $unit );
			$rendered      = esc_html( number_format_i18n( $display ) . ' ' . $unit );
			$vehicle_miles = $this->vehicle_current_mileage( (int) $item->vehicle_id );
			$mile_reached  = ( $vehicle_miles >= $due_mileage );

			// Same non-colour cue as the date branch: only the reached trigger carries it.
			$mileage_html     = ( $is_overdue && $mile_reached )
				? '<span class="wrenchpress-due--overdue"><span class="dashicons dashicons-warning" aria-hidden="true"></span>' . $rendered . ' <span class="screen-reader-text">' . esc_html__( '(reached)', 'pitdesk-repair-crm' ) . '</span></span>'
				: $rendered;
			$parts['mileage'] = $mileage_html;
		}

		if ( isset( $parts['date'] ) && isset( $parts['mileage'] ) ) {
			return $parts['date'] . '<br />' . esc_html__( 'or', 'pitdesk-repair-crm' ) . ' ' . $parts['mileage'];
		}

		if ( isset( $parts['date'] ) ) {
			return $parts['date'];
		}

		if ( isset( $parts['mileage'] ) ) {
			return $parts['mileage'];
		}

		return '';
	}

	/**
	 * Resolve a customer id to a "Last, First" display name, cached per request.
	 *
	 * @param int $customer_id Customer id.
	 * @return string
	 */
	private function customer_name( $customer_id ) {
		$customer_id = (int) $customer_id;

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

		if ( isset( $this->customer_names[ $customer_id ] ) ) {
			return $this->customer_names[ $customer_id ];
		}

		$customer = WrenchPress_Customers_Repository::get( $customer_id );
		$name     = $customer ? trim( sprintf( '%s, %s', $customer->last_name, $customer->first_name ), ', ' ) : '';

		$this->customer_names[ $customer_id ] = $name;

		return $name;
	}

	/**
	 * Resolve a vehicle id to a "Year Make Model" label, cached per request.
	 *
	 * @param int $vehicle_id Vehicle id.
	 * @return string
	 */
	private function vehicle_label( $vehicle_id ) {
		$vehicle_id = (int) $vehicle_id;

		if ( 0 === $vehicle_id ) {
			return '';
		}

		if ( isset( $this->vehicle_labels[ $vehicle_id ] ) ) {
			return $this->vehicle_labels[ $vehicle_id ];
		}

		$vehicle = WrenchPress_Vehicles_Repository::get( $vehicle_id );
		$label   = $vehicle ? WrenchPress_Vehicles_List_Table::vehicle_label( $vehicle ) : '';

		$this->vehicle_labels[ $vehicle_id ]  = $label;
		$this->vehicle_mileage[ $vehicle_id ] = ( $vehicle && isset( $vehicle->mileage ) ) ? (int) $vehicle->mileage : 0;

		return $label;
	}

	/**
	 * Resolve a vehicle id to its current canonical mileage, cached per request.
	 *
	 * Shares the cache populated by vehicle_label(); loads the vehicle only when
	 * the mileage has not already been resolved.
	 *
	 * @param int $vehicle_id Vehicle id.
	 * @return int Current canonical mileage (kilometers), or 0 when unknown.
	 */
	private function vehicle_current_mileage( $vehicle_id ) {
		$vehicle_id = (int) $vehicle_id;

		if ( 0 === $vehicle_id ) {
			return 0;
		}

		if ( isset( $this->vehicle_mileage[ $vehicle_id ] ) ) {
			return $this->vehicle_mileage[ $vehicle_id ];
		}

		$vehicle = WrenchPress_Vehicles_Repository::get( $vehicle_id );
		$mileage = ( $vehicle && isset( $vehicle->mileage ) ) ? (int) $vehicle->mileage : 0;

		$this->vehicle_mileage[ $vehicle_id ] = $mileage;

		return $mileage;
	}

	/**
	 * Query reminders for the current bucket and configure pagination. The pending
	 * due buckets read list_due(); the History bucket reads list_history() (terminal
	 * rows).
	 *
	 * The bucket request param is whitelisted against {overdue, due_soon, all,
	 * history} before it drives the repository; an unknown value collapses to 'all'
	 * so a stray value can never widen or distort the query. The value only ever
	 * picks a code path; it is never interpolated into SQL.
	 *
	 * @return void
	 */
	public function prepare_items() {
		$per_page     = 20;
		$current_page = $this->get_pagenum();
		$offset       = ( $current_page - 1 ) * $per_page;

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only list filter from a GET request.
		$bucket = isset( $_REQUEST['status'] ) ? sanitize_key( wp_unslash( $_REQUEST['status'] ) ) : 'all';

		// Whitelist: only a known bucket is allowed; anything else collapses to 'all'.
		if ( ! in_array( $bucket, array( 'overdue', 'due_soon', 'all', 'history' ), true ) ) {
			$bucket = 'all';
		}

		$this->bucket = $bucket;

		$args = array(
			'per_page' => $per_page,
			'offset'   => $offset,
		);

		if ( 'history' === $bucket ) {
			$this->items = WrenchPress_Reminders_Repository::list_history( $args );
			$total       = WrenchPress_Reminders_Repository::count_history();
		} else {
			$this->items = WrenchPress_Reminders_Repository::list_due( $bucket, $args );
			$total       = WrenchPress_Reminders_Repository::count_due( $bucket );
		}

		$this->_column_headers = array(
			$this->get_columns(),
			array(),
			array(),
		);

		$this->set_pagination_args(
			array(
				'total_items' => $total,
				'per_page'    => $per_page,
				'total_pages' => (int) ceil( $total / $per_page ),
			)
		);
	}
}
