<?php
/**
 * Customer-facing portal shortcode: [wrenchpress_portal].
 *
 * One builder-embeddable, server-rendered shortcode that RETURNS a string (never
 * echoes) and branches on session state. Logged out it returns the magic-link
 * request form; authenticated it returns a strictly read-only dashboard
 * (vehicles, per-vehicle service history, current visit status, and a per-work
 * -order detail mirror). There are no write affordances anywhere in the portal.
 *
 * The authenticated customer is resolved ONLY from
 * WrenchPress_Portal_Session::current_customer_id(); no id is ever read from a
 * request parameter, and every repository read is scoped by that id (the
 * PORT-04 / IDOR invariant). This class NEVER sets the session cookie: the
 * magic-link click verification and issue_cookie() run early on template_redirect
 * in WrenchPress_Portal_Request (Pitfall 6). The shortcode only reads a session
 * that already exists.
 *
 * Styling is delegated to the host builder/theme exactly like the visit form:
 * one structural stylesheet scoped to .egcrm-portal, reusing the visit-form
 * --egcrm-* token names, drawing only in currentColor, and declaring no plugin
 * colour or typography.
 *
 * @package WrenchPress
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class WrenchPress_Portal
 */
class WrenchPress_Portal {

	/**
	 * Shortcode tag.
	 */
	const TAG = 'wrenchpress_portal';

	/**
	 * Registered stylesheet handle for the portal assets.
	 */
	const ASSET_HANDLE = 'wrenchpress-portal';

	/**
	 * The admin-post action slug for the nonced sign-out POST.
	 */
	const SIGNOUT_ACTION = 'wrenchpress_portal_signout';

	/**
	 * Nonce action guarding the sign-out POST.
	 */
	const SIGNOUT_NONCE = 'wrenchpress_portal_signout';

	/**
	 * Register the shortcode and the nonced sign-out handler.
	 *
	 * Both admin_post_nopriv_ and admin_post_ are registered for sign-out so a
	 * logged-in staffer previewing the portal can also sign a portal session out,
	 * mirroring the request-handler registration.
	 *
	 * @return void
	 */
	public static function register() {
		add_shortcode( self::TAG, array( __CLASS__, 'render' ) );
		add_action( 'admin_post_nopriv_' . self::SIGNOUT_ACTION, array( __CLASS__, 'handle_signout' ) );
		add_action( 'admin_post_' . self::SIGNOUT_ACTION, array( __CLASS__, 'handle_signout' ) );
	}

	/**
	 * Register and enqueue the structural portal stylesheet.
	 *
	 * Mirrors the visit-form idiom: register with a version, then enqueue from the
	 * render path so late-loading builders (the DIVI 5 lazy-load case) still get
	 * the stylesheet in the footer. Registration is idempotent.
	 *
	 * @return void
	 */
	public static function enqueue_assets() {
		wp_register_style(
			self::ASSET_HANDLE,
			WRENCHPRESS_URL . 'assets/css/portal.css',
			array(),
			'0.1.0'
		);

		if ( did_action( 'wp_enqueue_scripts' ) ) {
			wp_enqueue_style( self::ASSET_HANDLE );
		} else {
			add_action(
				'wp_enqueue_scripts',
				static function () {
					wp_enqueue_style( self::ASSET_HANDLE );
				}
			);
		}
	}

	/**
	 * Render the portal for the shortcode. ALWAYS returns a string, never echoes.
	 *
	 * Branches on the session: an unauthenticated visitor gets the magic-link
	 * request form; an authenticated customer gets the read-only dashboard scoped
	 * to their own id. current_customer_id() is the single source of identity.
	 *
	 * @param array|string $atts Shortcode attributes (unused; reserved).
	 * @return string Portal markup.
	 */
	public static function render( $atts = array() ) {
		unset( $atts );

		self::enqueue_assets();

		$customer_id = WrenchPress_Portal_Session::current_customer_id();

		if ( $customer_id > 0 ) {
			return self::render_dashboard( $customer_id );
		}

		return self::render_request_form();
	}

	/**
	 * Handle the nonced sign-out POST: destroy the session and return to the form.
	 *
	 * Verifies the sign-out nonce (CSRF), destroys the current session row and
	 * clears the cookie via WrenchPress_Portal_Session::destroy_current(), then
	 * safe-redirects back to the portal page so the visitor lands on the
	 * logged-out request form. destroy_current() is a benign, read-only-scoped
	 * state change, but it is still nonce-guarded (T-08-04-03).
	 *
	 * @return void
	 */
	public static function handle_signout() {
		$nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';

		if ( wp_verify_nonce( $nonce, self::SIGNOUT_NONCE ) ) {
			WrenchPress_Portal_Session::destroy_current();
		}

		$target = isset( $_POST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_POST['redirect_to'] ) ) : '';

		wp_safe_redirect( wp_validate_redirect( $target, home_url( '/' ) ) );
		exit;
	}

	/**
	 * Build the logged-out magic-link request form markup.
	 *
	 * A .egcrm-portal__request form POSTing to admin-post.php with the
	 * WrenchPress_Portal_Request action, a nonce, the shared anti-abuse honeypot
	 * and submit-time timestamp fields, and a single email field. The neutral
	 * confirmation notice renders when the request-submitted flag is present and
	 * the expired-link error notice renders when the expired flag is present. Both
	 * flags come from our own PRG/redirect, so the reads are nonce-exempt.
	 *
	 * @return string
	 */
	private static function render_request_form() {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- status flags come from our own neutral redirect and the link-verify redirect, not a form POST.
		$sent    = isset( $_GET['wrenchpress_portal'] ) && 'sent' === sanitize_key( wp_unslash( $_GET['wrenchpress_portal'] ) );
		$expired = isset( $_GET['expired'] ) && '' !== sanitize_text_field( wp_unslash( $_GET['expired'] ) );
		// phpcs:enable WordPress.Security.NonceVerification.Recommended

		$html  = '<div class="egcrm-portal">';
		$html .= '<div class="egcrm-portal__request">';

		$html .= '<div class="egcrm-portal__header"><div>';
		$html .= '<h2 class="egcrm-portal__title">' . esc_html__( 'Sign in to your portal', 'pitdesk-repair-crm' ) . '</h2>';
		$html .= '<p class="egcrm-portal__subtitle">' . esc_html__( 'Enter your email and we will send you a secure sign-in link. No password needed.', 'pitdesk-repair-crm' ) . '</p>';
		$html .= '</div></div>';

		if ( $expired ) {
			$html .= '<div class="egcrm-portal__notice egcrm-portal__notice--error" role="alert">';
			$html .= '<strong>' . esc_html__( 'Link expired.', 'pitdesk-repair-crm' ) . '</strong> ';
			$html .= esc_html__( 'This sign-in link has expired or was already used. Request a new one below.', 'pitdesk-repair-crm' );
			$html .= '</div>';
		} elseif ( $sent ) {
			$html .= '<div class="egcrm-portal__notice egcrm-portal__notice--success" role="status">';
			$html .= esc_html__( 'If that email is on file, we have sent a sign-in link. Please check your inbox.', 'pitdesk-repair-crm' );
			$html .= '</div>';
		}

		$html .= '<form class="egcrm-portal__form" method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" aria-label="' . esc_attr__( 'Request a sign-in link', 'pitdesk-repair-crm' ) . '">';

		$html .= '<input type="hidden" name="action" value="' . esc_attr( WrenchPress_Portal_Request::ACTION ) . '" />';
		$html .= wp_nonce_field( WrenchPress_Portal_Request::NONCE_ACTION, '_wpnonce', true, false );
		$html .= self::honeypot_field();
		$html .= '<input type="hidden" name="' . esc_attr( WrenchPress_Anti_Abuse::TIMESTAMP_FIELD ) . '" value="' . esc_attr( (string) time() ) . '" data-egcrm-ts="1" />';

		$html .= '<div class="egcrm-field egcrm-field--full">';
		$html .= '<label class="egcrm-field__label" for="egcrm_portal_email">' . esc_html__( 'Email address', 'pitdesk-repair-crm' ) . '</label>';
		$html .= '<input id="egcrm_portal_email" name="email" type="email" class="egcrm-field__input" value="" autocomplete="email" required aria-required="true" />';
		$html .= '</div>';

		$html .= '<div class="egcrm-portal__actions">';
		$html .= '<button type="submit" class="egcrm-btn egcrm-btn--primary">' . esc_html__( 'Email me a sign-in link', 'pitdesk-repair-crm' ) . '</button>';
		$html .= '</div>';

		$html .= '</form>';

		$html .= '</div>'; // .egcrm-portal__request
		$html .= '</div>'; // .egcrm-portal

		return $html;
	}

	/**
	 * Render the authenticated, read-only dashboard scoped to one customer.
	 *
	 * The customer id is derived ONCE from the session and threaded into every
	 * repository read; nothing is read from a request parameter (PORT-04). The
	 * dashboard is strictly read-only: it renders the header with a sign-out
	 * control, a "Current visit" region of open work orders, and a "Your
	 * vehicles" region where each vehicle lists its service history and each work
	 * order renders a read-only mirror of the staff detail (status, line items,
	 * and computed totals). There are no inputs and no edit/delete/status
	 * affordances anywhere.
	 *
	 * @param int $customer_id Session-derived customer id (never from a request).
	 * @return string
	 */
	private static function render_dashboard( $customer_id ) {
		$customer_id = absint( $customer_id );

		$html  = '<div class="egcrm-portal">';
		$html .= self::dashboard_header();
		$html .= self::render_current_visit( $customer_id );
		$html .= self::render_vehicles( $customer_id );
		$html .= '</div>';

		return $html;
	}

	/**
	 * Render the "Current visit" region: open (non-closed) work orders.
	 *
	 * Pulls every work order for the session customer via list_for_customer and
	 * shows the ones whose status is not 'closed', displaying each one's status
	 * label. Both the list and the status vocabulary are scoped to the customer;
	 * no id is read from the request.
	 *
	 * @param int $customer_id Session-derived customer id.
	 * @return string
	 */
	private static function render_current_visit( $customer_id ) {
		$statuses = WrenchPress_Work_Orders_Repository::statuses();
		$orders   = WrenchPress_Work_Orders_Repository::list_for_customer( $customer_id );

		$open = array();

		foreach ( $orders as $order ) {
			if ( 'closed' !== (string) $order->status ) {
				$open[] = $order;
			}
		}

		$html  = '<section class="egcrm-portal__region" aria-labelledby="egcrm_portal_visit">';
		$html .= '<h3 class="egcrm-portal__region-title" id="egcrm_portal_visit">' . esc_html__( 'Current visit', 'pitdesk-repair-crm' ) . '</h3>';

		if ( empty( $open ) ) {
			$html .= '<p class="egcrm-portal__empty-body">' . esc_html__( 'You have no visits in progress right now.', 'pitdesk-repair-crm' ) . '</p>';
		} else {
			$html .= '<ul class="egcrm-portal__visit-list">';

			foreach ( $open as $order ) {
				$label = isset( $statuses[ $order->status ] ) ? $statuses[ $order->status ] : (string) $order->status;

				$html .= '<li class="egcrm-portal__visit-item">';
				$html .= sprintf(
					/* translators: %d: work-order number. */
					esc_html__( 'Job #%d', 'pitdesk-repair-crm' ),
					absint( $order->id )
				);
				$html .= ' - <span class="egcrm-portal__status">' . esc_html__( 'Status', 'pitdesk-repair-crm' ) . ': ' . esc_html( $label ) . '</span>';
				$html .= '</li>';
			}

			$html .= '</ul>';
		}

		$html .= '</section>';

		return $html;
	}

	/**
	 * Render the "Your vehicles" region and each vehicle's service history.
	 *
	 * Vehicles come only from list_active filtered by the session customer id.
	 * Each vehicle's service history comes from list_for_vehicle (which is bound
	 * to a vehicle the customer owns), so every work order rendered is already
	 * ownership-scoped without reading any request id.
	 *
	 * @param int $customer_id Session-derived customer id.
	 * @return string
	 */
	private static function render_vehicles( $customer_id ) {
		$vehicles = WrenchPress_Vehicles_Repository::list_active(
			array(
				'customer_id' => $customer_id,
				'per_page'    => 100,
			)
		);

		$html  = '<section class="egcrm-portal__region" aria-labelledby="egcrm_portal_vehicles">';
		$html .= '<h3 class="egcrm-portal__region-title" id="egcrm_portal_vehicles">' . esc_html__( 'Your vehicles', 'pitdesk-repair-crm' ) . '</h3>';

		if ( empty( $vehicles ) ) {
			$html .= '<div class="egcrm-portal__empty">';
			$html .= '<p class="egcrm-portal__empty-title">' . esc_html__( 'No vehicles on file yet', 'pitdesk-repair-crm' ) . '</p>';
			$html .= '<p class="egcrm-portal__empty-body">' . esc_html__( 'When the shop adds a vehicle to your account it will appear here.', 'pitdesk-repair-crm' ) . '</p>';
			$html .= '</div>';
		} else {
			foreach ( $vehicles as $vehicle ) {
				$html .= self::render_vehicle( $vehicle, $customer_id );
			}
		}

		$html .= '</section>';

		return $html;
	}

	/**
	 * Render one vehicle card with its per-vehicle service history.
	 *
	 * Re-asserts that the vehicle belongs to the session customer before
	 * rendering (defence in depth: list_active already filters by customer_id,
	 * but ownership is confirmed on the row so a scoping regression can never leak
	 * another customer's vehicle). A mismatch renders nothing.
	 *
	 * @param object $vehicle     Vehicle row.
	 * @param int    $customer_id Session-derived customer id.
	 * @return string
	 */
	private static function render_vehicle( $vehicle, $customer_id ) {
		if ( (int) $vehicle->customer_id !== (int) $customer_id ) {
			return '';
		}

		$name = trim( sprintf( '%d %s %s', absint( $vehicle->year ), (string) $vehicle->make, (string) $vehicle->model ) );

		if ( '' === $name ) {
			$name = __( 'Vehicle', 'pitdesk-repair-crm' );
		}

		$html  = '<article class="egcrm-portal__vehicle">';
		$html .= '<h4 class="egcrm-portal__vehicle-title">' . esc_html( $name ) . '</h4>';

		if ( '' !== trim( (string) $vehicle->plate ) ) {
			$html .= '<p class="egcrm-portal__vehicle-meta">' . esc_html__( 'Plate', 'pitdesk-repair-crm' ) . ': ' . esc_html( (string) $vehicle->plate ) . '</p>';
		}

		$html .= '<h5 class="egcrm-portal__history-title">' . esc_html__( 'Service history', 'pitdesk-repair-crm' ) . '</h5>';

		$orders = WrenchPress_Work_Orders_Repository::list_for_vehicle( absint( $vehicle->id ) );

		if ( empty( $orders ) ) {
			$html .= '<div class="egcrm-portal__empty">';
			$html .= '<p class="egcrm-portal__empty-title">' . esc_html__( 'No service history yet', 'pitdesk-repair-crm' ) . '</p>';
			$html .= '<p class="egcrm-portal__empty-body">' . esc_html__( 'Completed and in-progress work for this vehicle will show up here.', 'pitdesk-repair-crm' ) . '</p>';
			$html .= '</div>';
		} else {
			$html .= '<div class="egcrm-portal__history">';

			foreach ( $orders as $order ) {
				$html .= self::render_work_order( $order, $customer_id );
			}

			$html .= '</div>';
		}

		$html .= '</article>';

		return $html;
	}

	/**
	 * Render a single work order as a read-only mirror of the staff detail.
	 *
	 * Re-asserts row ownership against the session customer id, then renders the
	 * status label, an Item / Qty / Price / Total line-items table, and the
	 * computed subtotal / tax / grand total from the items repository's totals().
	 * Every money figure is formatted through WrenchPress_Money::format_money() so
	 * the portal matches the staff screen and the PDF exactly. There are NO
	 * inputs and NO edit / delete / status controls. The only affordance is the
	 * ownership-scoped, nonced document download from WrenchPress_Portal_Documents.
	 *
	 * @param object $order       Work-order row.
	 * @param int    $customer_id Session-derived customer id.
	 * @return string
	 */
	private static function render_work_order( $order, $customer_id ) {
		if ( (int) $order->customer_id !== (int) $customer_id ) {
			return '';
		}

		$statuses = WrenchPress_Work_Orders_Repository::statuses();
		$label    = isset( $statuses[ $order->status ] ) ? $statuses[ $order->status ] : (string) $order->status;

		$items  = WrenchPress_Work_Order_Items_Repository::list_for_work_order( absint( $order->id ) );
		$totals = WrenchPress_Work_Order_Items_Repository::totals( absint( $order->id ), $order->tax_rate );

		$date = mysql2date( get_option( 'date_format' ), (string) $order->created_at );

		$html  = '<div class="egcrm-portal__workorder">';
		$html .= '<div class="egcrm-portal__wo-head">';
		$html .= '<h6 class="egcrm-portal__wo-title">' . sprintf(
			/* translators: %d: work-order number. */
			esc_html__( 'Job #%d', 'pitdesk-repair-crm' ),
			absint( $order->id )
		) . '</h6>';
		$html .= '<span class="egcrm-portal__wo-date">' . esc_html( $date ) . '</span>';
		$html .= '</div>';

		$html .= '<p class="egcrm-portal__status">' . esc_html__( 'Status', 'pitdesk-repair-crm' ) . ': ' . esc_html( $label ) . '</p>';

		$html .= '<table class="egcrm-portal__items">';
		$html .= '<thead><tr>';
		$html .= '<th scope="col">' . esc_html__( 'Item', 'pitdesk-repair-crm' ) . '</th>';
		$html .= '<th scope="col" class="egcrm-portal__num">' . esc_html__( 'Qty', 'pitdesk-repair-crm' ) . '</th>';
		$html .= '<th scope="col" class="egcrm-portal__num">' . esc_html__( 'Price', 'pitdesk-repair-crm' ) . '</th>';
		$html .= '<th scope="col" class="egcrm-portal__num">' . esc_html__( 'Total', 'pitdesk-repair-crm' ) . '</th>';
		$html .= '</tr></thead><tbody>';

		foreach ( $items as $item ) {
			$line = (float) $item->quantity * (float) $item->unit_price;

			$html .= '<tr>';
			$html .= '<td>' . esc_html( (string) $item->description ) . '</td>';
			$html .= '<td class="egcrm-portal__num">' . esc_html( number_format( (float) $item->quantity, 2 ) ) . '</td>';
			$html .= '<td class="egcrm-portal__num">' . esc_html( WrenchPress_Money::format_money( (float) $item->unit_price ) ) . '</td>';
			$html .= '<td class="egcrm-portal__num">' . esc_html( WrenchPress_Money::format_money( $line ) ) . '</td>';
			$html .= '</tr>';
		}

		$html .= '</tbody></table>';

		$html .= '<table class="egcrm-portal__totals">';
		$html .= '<tbody>';
		$html .= '<tr><th scope="row">' . esc_html__( 'Subtotal', 'pitdesk-repair-crm' ) . '</th><td>' . esc_html( WrenchPress_Money::format_money( $totals['subtotal'] ) ) . '</td></tr>';
		$html .= '<tr><th scope="row">' . esc_html__( 'Tax', 'pitdesk-repair-crm' ) . '</th><td>' . esc_html( WrenchPress_Money::format_money( $totals['tax'] ) ) . '</td></tr>';
		$html .= '<tr class="egcrm-portal__grand"><th scope="row">' . esc_html__( 'Total', 'pitdesk-repair-crm' ) . '</th><td>' . esc_html( WrenchPress_Money::format_money( $totals['grand_total'] ) ) . '</td></tr>';
		$html .= '</tbody></table>';

		$html .= WrenchPress_Portal_Documents::download_link( $order );

		$html .= '</div>';

		return $html;
	}

	/**
	 * Build the dashboard header: the heading and the nonced sign-out control.
	 *
	 * @return string
	 */
	private static function dashboard_header() {
		$permalink = get_permalink();
		$return_to = $permalink ? (string) $permalink : home_url( '/' );

		$html  = '<div class="egcrm-portal__header">';
		$html .= '<h2 class="egcrm-portal__title">' . esc_html__( 'Your vehicles and service history', 'pitdesk-repair-crm' ) . '</h2>';

		$html .= '<form class="egcrm-portal__signout" method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
		$html .= '<input type="hidden" name="action" value="' . esc_attr( self::SIGNOUT_ACTION ) . '" />';
		$html .= wp_nonce_field( self::SIGNOUT_NONCE, '_wpnonce', true, false );
		$html .= '<input type="hidden" name="redirect_to" value="' . esc_url( $return_to ) . '" />';
		$html .= '<button type="submit" class="egcrm-btn egcrm-btn--secondary">' . esc_html__( 'Sign out', 'pitdesk-repair-crm' ) . '</button>';
		$html .= '</form>';

		$html .= '</div>';

		return $html;
	}

	/**
	 * Render the off-screen, non-focusable honeypot field.
	 *
	 * Visually hidden via an absolute off-screen position (not display:none) so
	 * cooperative bots still see and fill it. Mirrors the visit form so the
	 * anti-abuse gauntlet in WrenchPress_Portal_Request reads the same field name.
	 *
	 * @return string
	 */
	private static function honeypot_field() {
		$name = WrenchPress_Anti_Abuse::HONEYPOT_FIELD;

		$html  = '<div class="egcrm-portal__hp" aria-hidden="true">';
		$html .= '<label for="' . esc_attr( $name ) . '">' . esc_html__( 'Leave this field empty', 'pitdesk-repair-crm' ) . '</label>';
		$html .= '<input type="text" id="' . esc_attr( $name ) . '" name="' . esc_attr( $name ) . '" value="" tabindex="-1" autocomplete="off" />';
		$html .= '</div>';

		return $html;
	}
}
