<?php
/**
 * Admin controller for generating invoice/estimate documents.
 *
 * This is the staff-only write surface that turns a work order into a numbered,
 * frozen document and streams it back to the browser. It registers a single
 * admin-post action and, on each request, runs a fixed security order before any
 * work: verify the per-work-order nonce (CSRF), then confirm the MANAGE_WORKORDERS
 * capability, then load the work order and refuse a 'new' work order (D-03). Only
 * after those checks does it issue-or-reuse the frozen record through
 * WrenchPress_Document_Service and render the single source-of-truth HTML template.
 *
 * Two outputs share one template string (D-01): format=pdf streams a downloadable
 * Dompdf .pdf (buffers flushed first so nothing precedes the %PDF- signature), and
 * format=html emits the identical markup as a bare print page for the browser to
 * print. When the bundled PDF engine is unavailable the handler degrades to the
 * print view instead of fataling.
 *
 * The class also renders the "Generate document" section on the work-order detail
 * screen; that UI is capability-gated and hidden for a 'new' work order, and its
 * form carries the same per-work-order nonce the handler verifies.
 *
 * @package WrenchPress
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class WrenchPress_Documents_Admin
 */
class WrenchPress_Documents_Admin {

	/**
	 * Admin-post action name for the generate/download request.
	 */
	const ACTION = 'wrenchpress_generate_document';

	/**
	 * Nonce-action prefix for a per-work-order generate request. The work-order id
	 * is appended so a nonce minted for one work order cannot generate a document
	 * for another.
	 */
	const GENERATE_NONCE = 'wrenchpress_wo_generate_';

	/**
	 * Register the logged-in (priv only) generate handler on admin-post.php.
	 *
	 * This is a staff feature, so no nopriv hook is registered: an anonymous
	 * request is rejected by admin-post before it reaches handle().
	 *
	 * @return void
	 */
	public static function register() {
		add_action( 'admin_post_' . self::ACTION, array( __CLASS__, 'handle' ) );
	}

	/**
	 * Handle a generate/download request.
	 *
	 * Security order is fixed and must not be reordered: nonce first (CSRF), then
	 * capability, then the 'new'-status refusal. Input is whitelisted (doc_type
	 * against invoice/estimate, format against pdf/html) and the notes override is
	 * sanitized. The frozen record is fetched through the idempotent service, so a
	 * reprint reuses the original number and figures. A null record (a
	 * should-not-happen failure after race recovery, B1) redirects back to the
	 * detail screen with an error notice rather than emitting an empty download.
	 *
	 * @return void
	 */
	public static function handle() {
		$id = isset( $_POST['work_order'] ) ? absint( wp_unslash( $_POST['work_order'] ) ) : 0;

		// 1. Nonce (CSRF) FIRST, before any capability check or data access.
		check_admin_referer( self::GENERATE_NONCE . $id );

		// 2. Capability. Staff feature gated on MANAGE_WORKORDERS (V4 access control).
		if ( ! current_user_can( WrenchPress_Capabilities::MANAGE_WORKORDERS ) ) {
			wp_die( esc_html__( 'You do not have permission to generate documents.', 'pitdesk-repair-crm' ) );
		}

		// 3. Load the work order and refuse a missing one or a 'new' work order (D-03).
		$work_order = $id > 0 ? WrenchPress_Work_Orders_Repository::get( $id ) : null;

		if ( ! $work_order ) {
			wp_die( esc_html__( 'That work order could not be found.', 'pitdesk-repair-crm' ) );
		}

		if ( 'new' === (string) $work_order->status ) {
			wp_die( esc_html__( 'A document cannot be generated for a new work order.', 'pitdesk-repair-crm' ) );
		}

		// 4. Whitelist the document type: use the posted value only when it is one of
		// the two known types, otherwise fall back to the status-derived default.
		$default_type = WrenchPress_Document_Service::default_doc_type( (string) $work_order->status );
		$requested    = isset( $_POST['doc_type'] ) ? sanitize_key( wp_unslash( $_POST['doc_type'] ) ) : '';
		$doc_type     = in_array( $requested, array( 'invoice', 'estimate' ), true ) ? $requested : $default_type;

		// 5. Whitelist the output format.
		$requested_fmt = isset( $_POST['format'] ) ? sanitize_key( wp_unslash( $_POST['format'] ) ) : 'pdf';
		$format        = in_array( $requested_fmt, array( 'pdf', 'html' ), true ) ? $requested_fmt : 'pdf';

		// 6. Sanitize the notes override; a blank value falls back to the Settings
		// default inside the service (pass null rather than an empty string).
		$notes = isset( $_POST['notes'] ) ? sanitize_textarea_field( wp_unslash( $_POST['notes'] ) ) : '';
		$notes = ( '' === $notes ) ? null : $notes;

		// 7. Issue-or-reuse the frozen record (idempotent: reprints reuse the number).
		$record = WrenchPress_Document_Service::issue_or_get( $id, $doc_type, $notes );

		// Record guard (B1): a null/false record must never reach render(). Redirect
		// back to the detail screen with an error notice instead of a corrupt download.
		if ( ! $record ) {
			self::redirect_error( $id );
		}

		$customer = WrenchPress_Customers_Repository::get( (int) $work_order->customer_id );
		$vehicle  = WrenchPress_Vehicles_Repository::get( (int) $work_order->vehicle_id );

		// Single source-of-truth HTML string, used identically by both output paths.
		$html = WrenchPress_Document_Template::render( $record, $customer, $vehicle );

		if ( 'html' === $format ) {
			self::stream_html( $html );
		}

		// PDF path: only when the bundled engine is available and produced bytes;
		// otherwise degrade cleanly to the print view (T-07-07-05).
		if ( WrenchPress_Pdf_Renderer::is_available() ) {
			$pdf = WrenchPress_Pdf_Renderer::to_pdf( $html );

			if ( '' !== $pdf ) {
				self::stream_pdf( $pdf, self::filename( $record ) );
			}
		}

		self::stream_html( $html );
	}

	/**
	 * Emit the print view: the identical template HTML on a bare page.
	 *
	 * All output buffers are discarded first so the response is exactly the
	 * template string, then the request ends.
	 *
	 * @param string $html Complete standalone HTML document.
	 * @return void
	 */
	private static function stream_html( $html ) {
		while ( ob_get_level() ) {
			ob_end_clean();
		}

		nocache_headers();
		header( 'Content-Type: text/html; charset=' . get_option( 'blog_charset' ) );
		echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $html is the pre-escaped, self-contained document built by WrenchPress_Document_Template::render().
		exit;
	}

	/**
	 * Stream binary PDF bytes as a download.
	 *
	 * Every output buffer is flushed before the headers so no stray byte precedes
	 * the %PDF- signature and corrupts the file (Pitfall 4 / T-07-07-03), then the
	 * request ends.
	 *
	 * @param string $pdf      Binary PDF bytes (begins with %PDF-).
	 * @param string $filename Sanitized base filename (without extension).
	 * @return void
	 */
	private static function stream_pdf( $pdf, $filename ) {
		while ( ob_get_level() ) {
			ob_end_clean();
		}

		nocache_headers();
		header( 'Content-Type: application/pdf' );
		header( 'Content-Disposition: attachment; filename="' . $filename . '.pdf"' );
		header( 'Content-Length: ' . strlen( $pdf ) );
		echo $pdf; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- binary PDF stream, not HTML.
		exit;
	}

	/**
	 * Build a sanitized download filename from the document label and number.
	 *
	 * @param object $record Frozen document record (doc_type, doc_number).
	 * @return string Sanitized base filename, for example "Invoice-0001".
	 */
	private static function filename( $record ) {
		$label = ( isset( $record->doc_type ) && 'estimate' === $record->doc_type )
			? __( 'Estimate', 'pitdesk-repair-crm' )
			: __( 'Invoice', 'pitdesk-repair-crm' );

		$number = isset( $record->doc_number ) ? absint( $record->doc_number ) : 0;

		return sanitize_file_name( sprintf( '%s-%04d', $label, $number ) );
	}

	/**
	 * Redirect back to the work-order detail screen with a generic error notice.
	 *
	 * Reuses the detail screen's existing 'error' PRG flag so no new notice copy is
	 * needed. Ends the request.
	 *
	 * @param int $id Work-order id.
	 * @return void
	 */
	private static function redirect_error( $id ) {
		wp_safe_redirect(
			add_query_arg(
				array(
					'page'       => WrenchPress_Work_Orders_Admin_Page::MENU_SLUG,
					'action'     => 'view',
					'work_order' => (int) $id,
					'error'      => 1,
				),
				admin_url( 'admin.php' )
			)
		);
		exit;
	}

	/**
	 * Render the "Generate document" section on the work-order detail screen.
	 *
	 * Shown only to a user who holds MANAGE_WORKORDERS and only when the work order
	 * is not 'new' (D-03). The form posts to admin-post.php with the per-work-order
	 * nonce the handler verifies, a document-type override defaulting to the
	 * status-derived type, a notes/terms textarea prefilled from the Settings
	 * default, and two submit buttons that select the PDF or print output.
	 *
	 * @param object $wo Work-order row.
	 * @return void
	 */
	public static function render_generate_section( $wo ) {
		if ( ! current_user_can( WrenchPress_Capabilities::MANAGE_WORKORDERS ) ) {
			return;
		}

		if ( 'new' === (string) $wo->status ) {
			return;
		}

		$id           = (int) $wo->id;
		$default_type = WrenchPress_Document_Service::default_doc_type( (string) $wo->status );
		$settings     = WrenchPress_Settings_Page::get();
		$notes        = isset( $settings['document_notes_default'] ) ? (string) $settings['document_notes_default'] : '';

		echo '<h2>' . esc_html__( 'Generate document', 'pitdesk-repair-crm' ) . '</h2>';
		echo '<form class="wrenchpress-generate-document" method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
		wp_nonce_field( self::GENERATE_NONCE . $id );
		echo '<input type="hidden" name="action" value="' . esc_attr( self::ACTION ) . '" />';
		echo '<input type="hidden" name="work_order" value="' . esc_attr( (string) $id ) . '" />';

		echo '<p>';
		echo '<label for="wrenchpress_doc_type">' . esc_html__( 'Document type', 'pitdesk-repair-crm' ) . '</label> ';
		echo '<select name="doc_type" id="wrenchpress_doc_type">';
		echo '<option value="invoice"' . selected( $default_type, 'invoice', false ) . '>' . esc_html__( 'Invoice', 'pitdesk-repair-crm' ) . '</option>';
		echo '<option value="estimate"' . selected( $default_type, 'estimate', false ) . '>' . esc_html__( 'Estimate', 'pitdesk-repair-crm' ) . '</option>';
		echo '</select>';
		echo '</p>';

		echo '<p>';
		echo '<label for="wrenchpress_doc_notes">' . esc_html__( 'Notes / terms', 'pitdesk-repair-crm' ) . '</label><br />';
		echo '<textarea name="notes" id="wrenchpress_doc_notes" class="large-text" rows="3">' . esc_textarea( $notes ) . '</textarea>';
		echo '</p>';

		echo '<p>';
		echo '<button type="submit" name="format" value="pdf" class="button button-primary">' . esc_html__( 'Download PDF', 'pitdesk-repair-crm' ) . '</button> ';
		echo '<button type="submit" name="format" value="html" class="button">' . esc_html__( 'Print view', 'pitdesk-repair-crm' ) . '</button>';
		echo '</p>';

		echo '</form>';
	}
}
