<?php
/**
 * Single source-of-truth invoice/estimate document builder (D-01, INV-05).
 *
 * A single render() call returns ONE complete, standalone HTML document string
 * (doctype, head with an inline <style>, body) and NEVER echoes. That identical string
 * feeds both output paths: the server-side Dompdf render (WrenchPress_Pdf_Renderer)
 * and the browser Save-as-PDF print view. Because both paths render the same
 * string from the same frozen snapshot, they cannot diverge.
 *
 * Every figure comes from the frozen document record (07-05 snapshot), never a
 * fresh totals() recompute (INV-03). Money is formatted through
 * WrenchPress_Money::format_money() using the currency symbol and placement
 * frozen into the document's branding snapshot at issue time, so a later
 * currency-setting change never rewrites a historical reprint (INV-03). The
 * optional shop logo is embedded as a single
 * base64 data: URI (never a filesystem path or URL), so the same markup shows
 * the same logo in Dompdf and in the browser (B2). Dynamic values are escaped on
 * output (esc_html / esc_attr). No payment-status marker is shown (D-11).
 *
 * @package WrenchPress
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class WrenchPress_Document_Template
 */
class WrenchPress_Document_Template {

	/**
	 * Build the complete standalone HTML document for a frozen document record.
	 *
	 * The returned string is used verbatim by both the PDF renderer and the
	 * browser print view. It reads exclusively from the stored snapshot passed in
	 * as $document_record (the shape produced by WrenchPress_Documents_Repository::get()).
	 *
	 * @param object      $document_record Frozen document row: doc_type, doc_number,
	 *                                     labor, parts, subtotal, tax_rate, tax_amount,
	 *                                     grand_total, notes, issued_at, decoded
	 *                                     line_items array and branding array.
	 * @param object|null $customer        Customer row (first_name, last_name, phone,
	 *                                     email, address) or null.
	 * @param object|null $vehicle         Vehicle row (year, make, model, plate, vin,
	 *                                     mileage) or null.
	 * @return string Complete HTML document string. Never echoed.
	 */
	public static function render( $document_record, $customer, $vehicle ) {
		$branding = ( isset( $document_record->branding ) && is_array( $document_record->branding ) )
			? $document_record->branding
			: array();

		$items = ( isset( $document_record->line_items ) && is_array( $document_record->line_items ) )
			? $document_record->line_items
			: array();

		$doc_type = ( isset( $document_record->doc_type ) && 'estimate' === $document_record->doc_type )
			? 'estimate'
			: 'invoice';

		// Currency is read from the frozen branding snapshot, never live settings,
		// so a reprint keeps the currency the document was issued with (INV-03).
		$symbol    = isset( $branding['currency_symbol'] ) ? (string) $branding['currency_symbol'] : null;
		$placement = isset( $branding['currency_placement'] ) ? (string) $branding['currency_placement'] : null;

		$html  = '<!DOCTYPE html>';
		$html .= '<html><head><meta charset="utf-8" />';
		// doc_label() already returns an escaped string; escape the number once here
		// so the title is escaped exactly once (no double-encoding of translated labels).
		$html .= '<title>' . self::doc_label( $doc_type ) . ' ' . esc_html( self::doc_number( $doc_type, $document_record ) ) . '</title>';
		$html .= '<style>' . self::styles() . '</style>';
		$html .= '</head><body>';

		$html .= self::header_block( $doc_type, $document_record, $branding );
		$html .= self::parties_block( $customer, $vehicle );
		$html .= self::items_block( $items, $document_record, $symbol, $placement );
		$html .= self::totals_block( $document_record, $symbol, $placement );
		$html .= self::notes_block( $document_record );

		$html .= '</body></html>';

		return $html;
	}

	/**
	 * Human label for a document type.
	 *
	 * @param string $doc_type Either 'estimate' or 'invoice'.
	 * @return string Translated label.
	 */
	private static function doc_label( $doc_type ) {
		return 'estimate' === $doc_type
			? esc_html__( 'Estimate', 'pitdesk-repair-crm' )
			: esc_html__( 'Invoice', 'pitdesk-repair-crm' );
	}

	/**
	 * Zero-padded display number derived from the stored integer (never stored padded).
	 *
	 * @param string $doc_type        Either 'estimate' or 'invoice'.
	 * @param object $document_record Frozen document row.
	 * @return string For example INV-0001 or EST-0007.
	 */
	private static function doc_number( $doc_type, $document_record ) {
		$prefix = 'estimate' === $doc_type ? 'EST' : 'INV';
		$number = isset( $document_record->doc_number ) ? absint( $document_record->doc_number ) : 0;

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

	/**
	 * Header: shop identity, optional logo, and the document label/number/date.
	 *
	 * @param string              $doc_type        Either 'estimate' or 'invoice'.
	 * @param object              $document_record Frozen document row.
	 * @param array<string,mixed> $branding        Decoded branding snapshot.
	 * @return string HTML fragment.
	 */
	private static function header_block( $doc_type, $document_record, array $branding ) {
		$logo_uri = self::logo_data_uri( $branding );

		$html = '<table class="wp-doc-head"><tr>';

		$html .= '<td class="wp-doc-head-left">';
		if ( '' !== $logo_uri ) {
			$html .= '<img class="wp-doc-logo" src="' . esc_attr( $logo_uri ) . '" alt="" />';
		}
		$html .= self::identity_lines( $branding );
		$html .= '</td>';

		$html .= '<td class="wp-doc-head-right">';
		$html .= '<div class="wp-doc-title">' . self::doc_label( $doc_type ) . '</div>';
		$html .= '<div class="wp-doc-number">' . esc_html( self::doc_number( $doc_type, $document_record ) ) . '</div>';

		$issued = isset( $document_record->issued_at ) ? (string) $document_record->issued_at : '';
		$stamp  = strtotime( $issued );
		if ( $stamp ) {
			$html .= '<div class="wp-doc-date">';
			$html .= esc_html__( 'Date:', 'pitdesk-repair-crm' ) . ' ';
			$html .= esc_html( date_i18n( (string) get_option( 'date_format' ), $stamp ) );
			$html .= '</div>';
		}
		$html .= '</td>';

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

		return $html;
	}

	/**
	 * Shop identity lines, skipping any blank optional field (INV-04 best-effort).
	 *
	 * @param array<string,mixed> $branding Decoded branding snapshot.
	 * @return string HTML fragment.
	 */
	private static function identity_lines( array $branding ) {
		$name    = isset( $branding['shop_name'] ) ? trim( (string) $branding['shop_name'] ) : '';
		$address = isset( $branding['shop_address'] ) ? trim( (string) $branding['shop_address'] ) : '';
		$phone   = isset( $branding['shop_phone'] ) ? trim( (string) $branding['shop_phone'] ) : '';
		$email   = isset( $branding['shop_email'] ) ? trim( (string) $branding['shop_email'] ) : '';
		$tax_no  = isset( $branding['shop_tax_number'] ) ? trim( (string) $branding['shop_tax_number'] ) : '';

		$html = '';
		if ( '' !== $name ) {
			$html .= '<div class="wp-doc-shop-name">' . esc_html( $name ) . '</div>';
		}
		if ( '' !== $address ) {
			$html .= '<div class="wp-doc-shop-line">' . nl2br( esc_html( $address ) ) . '</div>';
		}
		if ( '' !== $phone ) {
			$html .= '<div class="wp-doc-shop-line">' . esc_html( $phone ) . '</div>';
		}
		if ( '' !== $email ) {
			$html .= '<div class="wp-doc-shop-line">' . esc_html( $email ) . '</div>';
		}
		if ( '' !== $tax_no ) {
			$html .= '<div class="wp-doc-shop-line">';
			$html .= esc_html__( 'Tax/License #:', 'pitdesk-repair-crm' ) . ' ' . esc_html( $tax_no );
			$html .= '</div>';
		}

		return $html;
	}

	/**
	 * Resolve the optional logo attachment to a single base64 data: URI.
	 *
	 * The bytes are read from the LOCAL path the renderer resolves under the
	 * uploads chroot (WrenchPress_Pdf_Renderer::logo_path()); a path or URL is
	 * never emitted as the img src. Returns an empty string when no logo is
	 * configured or the file is unreadable, so the document simply omits the
	 * image (INV-04 best-effort).
	 *
	 * @param array<string,mixed> $branding Decoded branding snapshot.
	 * @return string A data:image/<mime>;base64,... URI, or '' when unavailable.
	 */
	private static function logo_data_uri( array $branding ) {
		$logo_id = isset( $branding['shop_logo_id'] ) ? absint( $branding['shop_logo_id'] ) : 0;
		if ( 0 === $logo_id || ! class_exists( 'WrenchPress_Pdf_Renderer' ) ) {
			return '';
		}

		$path = WrenchPress_Pdf_Renderer::logo_path( $logo_id );
		if ( '' === $path ) {
			return '';
		}

		$filetype = wp_check_filetype( $path );
		$mime     = (string) $filetype['type'];
		if ( '' === $mime || 0 !== strpos( $mime, 'image/' ) ) {
			return '';
		}
		$subtype = substr( $mime, strlen( 'image/' ) );

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading a local uploads-dir file (resolved and containment-checked by the renderer) to inline the logo as a single data: URI for both output paths; not a remote fetch.
		$bytes = file_get_contents( $path );
		if ( false === $bytes || '' === $bytes ) {
			return '';
		}

		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Base64 is the data: URI transport for the logo image, not obfuscation.
		return 'data:image/' . $subtype . ';base64,' . base64_encode( $bytes );
	}

	/**
	 * Customer and vehicle blocks, including the odometer in the shop unit.
	 *
	 * @param object|null $customer Customer row or null.
	 * @param object|null $vehicle  Vehicle row or null.
	 * @return string HTML fragment.
	 */
	private static function parties_block( $customer, $vehicle ) {
		$html = '<table class="wp-doc-parties"><tr>';

		$html .= '<td class="wp-doc-party">';
		$html .= '<div class="wp-doc-party-label">' . esc_html__( 'Customer', 'pitdesk-repair-crm' ) . '</div>';
		if ( $customer ) {
			$name = trim( sprintf( '%s %s', (string) $customer->first_name, (string) $customer->last_name ) );
			if ( '' !== $name ) {
				$html .= '<div class="wp-doc-party-line">' . esc_html( $name ) . '</div>';
			}
			$address = trim( (string) $customer->address );
			if ( '' !== $address ) {
				$html .= '<div class="wp-doc-party-line">' . esc_html( $address ) . '</div>';
			}
			$phone = trim( (string) $customer->phone );
			if ( '' !== $phone ) {
				$html .= '<div class="wp-doc-party-line">' . esc_html( $phone ) . '</div>';
			}
			$email = trim( (string) $customer->email );
			if ( '' !== $email ) {
				$html .= '<div class="wp-doc-party-line">' . esc_html( $email ) . '</div>';
			}
		}
		$html .= '</td>';

		$html .= '<td class="wp-doc-party">';
		$html .= '<div class="wp-doc-party-label">' . esc_html__( 'Vehicle', 'pitdesk-repair-crm' ) . '</div>';
		if ( $vehicle ) {
			$desc = trim(
				sprintf(
					'%s %s %s',
					(int) $vehicle->year > 0 ? (string) (int) $vehicle->year : '',
					(string) $vehicle->make,
					(string) $vehicle->model
				)
			);
			if ( '' !== $desc ) {
				$html .= '<div class="wp-doc-party-line">' . esc_html( $desc ) . '</div>';
			}
			$plate = trim( (string) $vehicle->plate );
			if ( '' !== $plate ) {
				$html .= '<div class="wp-doc-party-line">';
				$html .= esc_html__( 'Plate:', 'pitdesk-repair-crm' ) . ' ' . esc_html( $plate );
				$html .= '</div>';
			}
			$vin = trim( (string) $vehicle->vin );
			if ( '' !== $vin ) {
				$html .= '<div class="wp-doc-party-line">';
				$html .= esc_html__( 'VIN:', 'pitdesk-repair-crm' ) . ' ' . esc_html( $vin );
				$html .= '</div>';
			}
			$html .= '<div class="wp-doc-party-line">';
			$html .= esc_html__( 'Odometer:', 'pitdesk-repair-crm' ) . ' ' . esc_html( self::odometer( $vehicle ) );
			$html .= '</div>';
		}
		$html .= '</td>';

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

		return $html;
	}

	/**
	 * Odometer reading in the shop's configured mileage unit.
	 *
	 * @param object $vehicle Vehicle row.
	 * @return string For example "84,120 mi".
	 */
	private static function odometer( $vehicle ) {
		$unit       = WrenchPress_Settings_Page::mileage_unit();
		$unit_label = 'km' === $unit ? __( 'km', 'pitdesk-repair-crm' ) : __( 'mi', 'pitdesk-repair-crm' );
		$display    = WrenchPress_Reminders_Repository::mileage_to_display( (int) $vehicle->mileage, $unit );

		return number_format_i18n( $display ) . ' ' . $unit_label;
	}

	/**
	 * Two grouped line-item tables (Labor then Parts), each with its section subtotal.
	 *
	 * Items are split by the frozen item_type; the section subtotals are the
	 * frozen labor/parts figures from the snapshot, never a recompute (INV-03).
	 *
	 * @param array<int,mixed> $items           Decoded line_items rows.
	 * @param object           $document_record Frozen document row.
	 * @param string|null      $symbol          Frozen currency symbol, or null for live.
	 * @param string|null      $placement       Frozen currency placement, or null for live.
	 * @return string HTML fragment.
	 */
	private static function items_block( array $items, $document_record, $symbol = null, $placement = null ) {
		$labor = array();
		$parts = array();
		foreach ( $items as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			$type = isset( $item['item_type'] ) ? (string) $item['item_type'] : '';
			if ( 'labor' === $type ) {
				$labor[] = $item;
			} else {
				$parts[] = $item;
			}
		}

		$labor_subtotal = isset( $document_record->labor ) ? $document_record->labor : 0;
		$parts_subtotal = isset( $document_record->parts ) ? $document_record->parts : 0;

		$html  = self::items_table( esc_html__( 'Labor', 'pitdesk-repair-crm' ), $labor, $labor_subtotal, $symbol, $placement );
		$html .= self::items_table( esc_html__( 'Parts', 'pitdesk-repair-crm' ), $parts, $parts_subtotal, $symbol, $placement );

		return $html;
	}

	/**
	 * One grouped items table with a section-subtotal footer row.
	 *
	 * @param string           $heading   Already-escaped section heading.
	 * @param array<int,mixed> $items     Rows for this section.
	 * @param mixed            $subtotal  Frozen section subtotal.
	 * @param string|null      $symbol    Frozen currency symbol, or null for live.
	 * @param string|null      $placement Frozen currency placement, or null for live.
	 * @return string HTML fragment.
	 */
	private static function items_table( $heading, array $items, $subtotal, $symbol = null, $placement = null ) {
		$html  = '<table class="wp-doc-items">';
		$html .= '<thead><tr><th class="wp-doc-section" colspan="4">' . $heading . '</th></tr>';
		$html .= '<tr>';
		$html .= '<th class="wp-doc-col-desc">' . esc_html__( 'Description', 'pitdesk-repair-crm' ) . '</th>';
		$html .= '<th class="wp-doc-col-num">' . esc_html__( 'Qty', 'pitdesk-repair-crm' ) . '</th>';
		$html .= '<th class="wp-doc-col-num">' . esc_html__( 'Unit price', 'pitdesk-repair-crm' ) . '</th>';
		$html .= '<th class="wp-doc-col-num">' . esc_html__( 'Amount', 'pitdesk-repair-crm' ) . '</th>';
		$html .= '</tr></thead><tbody>';

		if ( empty( $items ) ) {
			$html .= '<tr><td class="wp-doc-empty" colspan="4">' . esc_html__( 'None', 'pitdesk-repair-crm' ) . '</td></tr>';
		} else {
			foreach ( $items as $item ) {
				$description = isset( $item['description'] ) ? (string) $item['description'] : '';
				$quantity    = isset( $item['quantity'] ) ? (float) $item['quantity'] : 0.0;
				$unit_price  = isset( $item['unit_price'] ) ? (float) $item['unit_price'] : 0.0;
				$amount      = $quantity * $unit_price;

				$html .= '<tr>';
				$html .= '<td class="wp-doc-col-desc">' . esc_html( $description ) . '</td>';
				$html .= '<td class="wp-doc-col-num">' . esc_html( number_format_i18n( $quantity, 2 ) ) . '</td>';
				$html .= '<td class="wp-doc-col-num">' . esc_html( WrenchPress_Money::format_money( $unit_price, $symbol, $placement ) ) . '</td>';
				$html .= '<td class="wp-doc-col-num">' . esc_html( WrenchPress_Money::format_money( $amount, $symbol, $placement ) ) . '</td>';
				$html .= '</tr>';
			}
		}

		$html .= '<tr class="wp-doc-subtotal-row">';
		$html .= '<td class="wp-doc-col-num" colspan="3">' . $heading . ' ' . esc_html__( 'subtotal', 'pitdesk-repair-crm' ) . '</td>';
		$html .= '<td class="wp-doc-col-num">' . esc_html( WrenchPress_Money::format_money( $subtotal, $symbol, $placement ) ) . '</td>';
		$html .= '</tr>';

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

		return $html;
	}

	/**
	 * Subtotal, tax (frozen rate/amount), and grand total block.
	 *
	 * @param object      $document_record Frozen document row.
	 * @param string|null $symbol          Frozen currency symbol, or null for live.
	 * @param string|null $placement       Frozen currency placement, or null for live.
	 * @return string HTML fragment.
	 */
	private static function totals_block( $document_record, $symbol = null, $placement = null ) {
		$subtotal    = isset( $document_record->subtotal ) ? $document_record->subtotal : 0;
		$tax_amount  = isset( $document_record->tax_amount ) ? $document_record->tax_amount : 0;
		$grand_total = isset( $document_record->grand_total ) ? $document_record->grand_total : 0;
		$tax_rate    = isset( $document_record->tax_rate ) ? (float) $document_record->tax_rate : 0.0;
		$percent     = rtrim( rtrim( number_format( $tax_rate * 100, 4, '.', '' ), '0' ), '.' );
		if ( '' === $percent ) {
			$percent = '0';
		}

		$html = '<table class="wp-doc-totals"><tbody>';

		$html .= '<tr>';
		$html .= '<td class="wp-doc-totals-label">' . esc_html__( 'Subtotal', 'pitdesk-repair-crm' ) . '</td>';
		$html .= '<td class="wp-doc-totals-value">' . esc_html( WrenchPress_Money::format_money( $subtotal, $symbol, $placement ) ) . '</td>';
		$html .= '</tr>';

		$html .= '<tr>';
		$html .= '<td class="wp-doc-totals-label">';
		$html .= esc_html(
			sprintf(
				/* translators: %s: tax rate as a percentage. */
				__( 'Tax (%s%%)', 'pitdesk-repair-crm' ),
				$percent
			)
		);
		$html .= '</td>';
		$html .= '<td class="wp-doc-totals-value">' . esc_html( WrenchPress_Money::format_money( $tax_amount, $symbol, $placement ) ) . '</td>';
		$html .= '</tr>';

		$html .= '<tr class="wp-doc-grand">';
		$html .= '<td class="wp-doc-totals-label">' . esc_html__( 'Grand total', 'pitdesk-repair-crm' ) . '</td>';
		$html .= '<td class="wp-doc-totals-value">' . esc_html( WrenchPress_Money::format_money( $grand_total, $symbol, $placement ) ) . '</td>';
		$html .= '</tr>';

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

		return $html;
	}

	/**
	 * Notes / terms block, omitted when the frozen notes are blank.
	 *
	 * @param object $document_record Frozen document row.
	 * @return string HTML fragment.
	 */
	private static function notes_block( $document_record ) {
		$notes = isset( $document_record->notes ) ? trim( (string) $document_record->notes ) : '';
		if ( '' === $notes ) {
			return '';
		}

		$html  = '<div class="wp-doc-notes">';
		$html .= '<div class="wp-doc-notes-label">' . esc_html__( 'Notes / Terms', 'pitdesk-repair-crm' ) . '</div>';
		$html .= '<div class="wp-doc-notes-body">' . nl2br( esc_html( $notes ) ) . '</div>';
		$html .= '</div>';

		return $html;
	}

	/**
	 * Inline, Dompdf-safe stylesheet shared by both output paths.
	 *
	 * Table-based layout only (no flexbox/grid), simple block flow, explicit
	 * widths, an @page margin for the PDF, and an @media print block so the
	 * browser Save-as-PDF view matches the rasterized PDF.
	 *
	 * @return string CSS (without the surrounding style tags).
	 */
	private static function styles() {
		return '
@page { margin: 18mm 16mm; }
body { font-family: DejaVu Sans, Arial, Helvetica, sans-serif; color: #1a1a1a; font-size: 12px; line-height: 1.45; margin: 0; }
table { width: 100%; border-collapse: collapse; }
.wp-doc-head td { vertical-align: top; padding: 0 0 16px 0; }
.wp-doc-head-right { text-align: right; }
.wp-doc-logo { max-height: 70px; max-width: 220px; margin-bottom: 8px; }
.wp-doc-shop-name { font-size: 16px; font-weight: bold; margin-bottom: 2px; }
.wp-doc-shop-line { font-size: 11px; }
.wp-doc-title { font-size: 22px; font-weight: bold; text-transform: uppercase; letter-spacing: 1px; }
.wp-doc-number { font-size: 14px; margin-top: 2px; }
.wp-doc-date { font-size: 11px; margin-top: 2px; }
.wp-doc-parties td { vertical-align: top; width: 50%; padding: 0 12px 16px 0; }
.wp-doc-party-label { font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: #666; margin-bottom: 4px; }
.wp-doc-party-line { font-size: 11px; }
.wp-doc-items { margin-bottom: 12px; }
.wp-doc-items th, .wp-doc-items td { border-bottom: 1px solid #ddd; padding: 6px 8px; text-align: left; }
.wp-doc-section { background: #f2f2f2; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; }
.wp-doc-col-num { text-align: right; white-space: nowrap; }
.wp-doc-empty { color: #888; font-style: italic; }
.wp-doc-subtotal-row td { font-weight: bold; border-bottom: 2px solid #999; }
.wp-doc-totals { width: 45%; margin-left: 55%; }
.wp-doc-totals td { padding: 5px 8px; }
.wp-doc-totals-label { text-align: left; }
.wp-doc-totals-value { text-align: right; white-space: nowrap; }
.wp-doc-grand td { font-size: 14px; font-weight: bold; border-top: 2px solid #333; }
.wp-doc-notes { margin-top: 20px; padding-top: 10px; border-top: 1px solid #ddd; }
.wp-doc-notes-label { font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: #666; margin-bottom: 4px; }
.wp-doc-notes-body { font-size: 11px; }
@media print {
	body { font-size: 12px; }
	.wp-doc-items th, .wp-doc-items td { border-bottom: 1px solid #ccc; }
	.wp-doc-grand td { border-top: 2px solid #000; }
}
';
	}
}
