<?php
/**
 * PDF generator using TCPDF.
 *
 * v1.9.1  -  Unified, professional template engine inspired by polished
 * invoice templates (Sliced-style header / from / to / meta / items /
 * totals / footer). Every PDF in the plugin (invoices, statements,
 * report cards, generic summaries / reports) now shares the same
 * branded layout, alignment, typography and column rules.
 *
 * @package TradeSchoolManager
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class TSMP_PDF {

	/* =====================================================================
	 *  CORE / TCPDF
	 * ===================================================================== */

	public static function load_tcpdf() {
		if ( ! class_exists( 'TCPDF' ) ) {
			$file = TSMP_PLUGIN_DIR . 'vendor/tcpdf-src/tcpdf.php';
			if ( file_exists( $file ) ) {
				require_once $file;
			}
		}
	}

	/**
	 * Load the FPDI autoloader so add-ons can import existing PDF pages
	 * (used as backgrounds for certificate templates, etc.).
	 * Returns true if the FPDI/TCPDF integration class is available.
	 */
	public static function load_fpdi() {
		self::load_tcpdf();
		if ( ! class_exists( '\\setasign\\Fpdi\\Tcpdf\\Fpdi' ) ) {
			$autoload = TSMP_PLUGIN_DIR . 'vendor/fpdi/src/autoload.php';
			if ( file_exists( $autoload ) ) {
				require_once $autoload;
			}
		}
		return class_exists( '\\setasign\\Fpdi\\Tcpdf\\Fpdi' );
	}

	/**
	 * Build a fresh TCPDF instance with brand defaults.
	 *
	 * @param string $title       Document title (PDF metadata).
	 * @param string $orientation 'P' or 'L'.
	 */
	private static function new_pdf( $title = '', $orientation = 'P' ) {
		self::load_tcpdf();
		$pdf = new TCPDF( $orientation, 'mm', 'LETTER', true, 'UTF-8', false );
		$pdf->SetCreator( 'TradeSchool Manager' );
		$pdf->SetAuthor( tsm_get_setting( 'school_name', get_bloginfo( 'name' ) ) );
		$pdf->SetTitle( $title );
		$pdf->setPrintHeader( false );
		$pdf->setPrintFooter( true );
		$pdf->SetMargins( 16, 18, 16 );
		$pdf->SetAutoPageBreak( true, 22 );
		// Custom page footer (page numbers + small school line).
		$footer_text = tsm_get_setting( 'school_name', get_bloginfo( 'name' ) );
		$pdf->setFooterFont( array( 'helvetica', '', 8 ) );
		$pdf->setFooterMargin( 12 );
		$pdf->SetFooterData( '', 0, $footer_text, '' );
		return $pdf;
	}

	/* =====================================================================
	 *  SHARED THEME HELPERS
	 * ===================================================================== */

	/** Brand colour (hex). */
	public static function color() {
		return esc_attr( tsm_get_setting( 'brand_color', '#1f6feb' ) );
	}

	/**
	 * Resolve a Media Library image URL to a local file path for TCPDF.
	 *
	 * TCPDF may fail on public URLs from the same site depending on hosting,
	 * SSL, redirects, or server-side loopback restrictions. Local upload paths
	 * are more reliable and avoid remote requests during PDF generation.
	 */
	private static function pdf_image_src( $url ) {
		$url = esc_url_raw( $url );
		if ( '' === $url ) {
			return '';
		}

		$uploads = wp_upload_dir();
		if ( ! empty( $uploads['error'] ) || empty( $uploads['basedir'] ) || empty( $uploads['baseurl'] ) ) {
			return '';
		}

		$url_parts  = wp_parse_url( $url );
		$base_parts = wp_parse_url( $uploads['baseurl'] );
		if ( empty( $url_parts['path'] ) || empty( $base_parts['path'] ) ) {
			return '';
		}

		$url_path  = wp_normalize_path( rawurldecode( $url_parts['path'] ) );
		$base_path = untrailingslashit( wp_normalize_path( rawurldecode( $base_parts['path'] ) ) );
		if ( 0 !== strpos( $url_path, $base_path . '/' ) ) {
			return '';
		}

		$relative = ltrim( substr( $url_path, strlen( $base_path ) ), '/' );
		if ( '' === $relative || false !== strpos( $relative, '..' ) ) {
			return '';
		}

		$basedir = realpath( $uploads['basedir'] );
		$file    = realpath( trailingslashit( $uploads['basedir'] ) . $relative );
		if ( false === $basedir || false === $file ) {
			return '';
		}

		$basedir = wp_normalize_path( $basedir );
		$file    = wp_normalize_path( $file );
		if ( 0 !== strpos( $file, trailingslashit( $basedir ) ) || ! is_readable( $file ) ) {
			return '';
		}

		return $file;
	}

	/** Branded document header band. */
	private static function header_html( $title = '', $subtitle = '' ) {
		$name     = esc_html( tsm_get_setting( 'school_name', get_bloginfo( 'name' ) ) );
		$logo_url = tsm_get_setting( 'school_logo', '' );
		$logo     = self::pdf_image_src( $logo_url );
		$color    = self::color();
		$addr     = esc_html( tsm_get_setting( 'school_address', '' ) );
		$phone    = esc_html( tsm_get_setting( 'school_phone', '' ) );
		$email    = esc_html( tsm_get_setting( 'school_email', get_bloginfo( 'admin_email' ) ) );

		$logo_html = $logo
			? '<img src="' . esc_attr( $logo ) . '" height="46">'
			: '<span style="font-size:20pt;font-weight:bold;color:' . $color . ';letter-spacing:.5px;">' . $name . '</span>';

		$contact_lines = array_filter( array( $addr ? nl2br( $addr ) : '', $phone, $email ) );
		$contact_html  = $contact_lines
			? '<span style="font-size:8.5pt;color:#555;line-height:1.5;">' . implode( '<br>', $contact_lines ) . '</span>'
			: '';

		$right = '';
		if ( $title )    $right .= '<span style="font-size:24pt;font-weight:bold;color:#222;letter-spacing:1px;">' . esc_html( strtoupper( $title ) ) . '</span><br>';
		if ( $subtitle ) $right .= '<span style="font-size:10pt;color:#666;">' . esc_html( $subtitle ) . '</span>';

		$out  = '<table cellpadding="0" cellspacing="0" border="0" width="100%"><tr>'
			. '<td width="60%" valign="top">' . $logo_html
			. ( $logo ? '<br><span style="font-size:11pt;font-weight:bold;color:#222;">' . $name . '</span>' : '' )
			. '<br>' . $contact_html . '</td>'
			. '<td width="40%" align="right" valign="top">' . $right . '</td>'
			. '</tr></table>';
		// Coloured rule under header.
		$out .= '<br><table cellpadding="0" cellspacing="0" border="0" width="100%">
	<tr>
		<td style="border-bottom:3px solid ' . $color . ';">&nbsp;</td>
	</tr>
</table><br>';
		return $out;
	}

	/** "From:" school identity block. */
	private static function school_block_html( $label = 'From:' ) {
		$color = self::color();
		$name  = esc_html( tsm_get_setting( 'school_name', get_bloginfo( 'name' ) ) );
		$addr  = esc_html( tsm_get_setting( 'school_address', '' ) );
		$phone = esc_html( tsm_get_setting( 'school_phone', '' ) );
		$email = esc_html( tsm_get_setting( 'school_email', get_bloginfo( 'admin_email' ) ) );

		$h  = '<b style="color:#222;font-size:11pt;">' . esc_html( $label ) . '</b><br>';
		$h .= '<span style="color:' . $color . ';font-weight:bold;font-size:11pt;">' . $name . '</span><br>';
		if ( $addr )  $h .= '<span style="font-size:9.5pt;color:#444;">' . nl2br( $addr ) . '</span><br>';
		if ( $phone ) $h .= '<span style="font-size:9.5pt;color:#444;">' . $phone . '</span><br>';
		if ( $email ) $h .= '<span style="font-size:9.5pt;color:#444;">' . $email . '</span>';
		return $h;
	}

	/** "To:" student identity block. */
	private static function student_block_html( $student, $label = 'To:' ) {
		$h  = '<b style="color:#222;font-size:11pt;">' . esc_html( $label ) . '</b><br>';
		if ( ! $student ) return $h . '<span style="font-size:9.5pt;color:#888;"><i> - </i></span>';
		$h .= '<span style="font-size:11pt;color:#222;">' . esc_html( $student->first_name . ' ' . $student->last_name ) . '</span><br>';
		if ( ! empty( $student->address ) ) $h .= '<span style="font-size:9.5pt;color:#444;">' . nl2br( esc_html( $student->address ) ) . '</span><br>';
		if ( ! empty( $student->email ) )   $h .= '<span style="font-size:9.5pt;color:#444;">' . esc_html( $student->email ) . '</span><br>';
		if ( ! empty( $student->phone ) )   $h .= '<span style="font-size:9.5pt;color:#444;">' . esc_html( $student->phone ) . '</span>';
		return $h;
	}

	/**
	 * Bordered label/value meta table.
	 *
	 * @param array $rows  array of [ 'label'=>'', 'value'=>'', 'highlight'=>bool ]
	 */
	private static function meta_table_html( array $rows ) {
		$color = self::color();
		$h  = '<table cellpadding="6" cellspacing="0" border="1" width="100%" style="border-color:#cfd4dc;font-size:10pt;">';
		foreach ( $rows as $r ) {
			$hl = ! empty( $r['highlight'] );
			if ( $hl ) {
				$h .= '<tr><td width="55%" style="background:#eaeef4;color:#222;font-weight:bold;"><b>' . esc_html( $r['label'] ) . '</b></td>'
					. '<td align="right" style="background:#eaeef4;font-weight:bold;color:' . $color . ';">' . $r['value'] . '</td></tr>';
			} else {
				$h .= '<tr><td width="55%" style="background:#f7f9fc;color:#444;"><b>' . esc_html( $r['label'] ) . '</b></td>'
					. '<td align="right">' . $r['value'] . '</td></tr>';
			}
		}
		$h .= '</table>';
		return $h;
	}

	/**
	 * From / To + meta in one row.
	 */
	private static function from_to_meta_html( $left_html, $meta_html ) {
		return '<br><table cellpadding="6" cellspacing="0" border="0" width="100%"><tr>'
			. '<td width="50%" valign="top">' . $left_html . '</td>'
			. '<td width="50%" valign="top">' . $meta_html . '</td>'
			. '</tr></table><br>';
	}

	/**
	 * Bordered data table with branded header band & alternating rows.
	 *
	 * @param array $cols [ ['label'=>'', 'width'=>'20%', 'align'=>'left|center|right'], ... ]
	 * @param array $rows array of arrays where each row is [ ['value'=>'', 'align'=>'', 'bold'=>bool], ... ]
	 * @param string $empty_text  text shown when rows is empty
	 */
	private static function data_table_html( array $cols, array $rows, $empty_text = 'No data' ) {
		$color = self::color();

		$h = '<table cellpadding="6" cellspacing="0" border="0" width="100%" style="font-size:9.5pt;">';

		// Header row - use TDs, not TH/THEAD, for better TCPDF rendering.
		$h .= '<tr>';
		foreach ( $cols as $c ) {
			$w = isset( $c['width'] ) ? ' width="' . esc_attr( $c['width'] ) . '"' : '';
			$a = isset( $c['align'] ) ? esc_attr( $c['align'] ) : 'left';

			$h .= '<td' . $w . ' align="' . $a . '" style="background-color:' . $color . '; color:#ffffff; border:1px solid #cfd4dc; font-weight:bold;">'
				. '<b>' . esc_html( $c['label'] ) . '</b>'
				. '</td>';
		}
		$h .= '</tr>';

		if ( empty( $rows ) ) {
			$h .= '<tr>';
			$h .= '<td colspan="' . count( $cols ) . '" align="center" style="border:1px solid #cfd4dc; color:#999;">'
				. '<i>' . esc_html( $empty_text ) . '</i>'
				. '</td>';
			$h .= '</tr>';
		} else {
			$alt = false;

			foreach ( $rows as $row ) {
				$bg  = $alt ? '#fafbfc' : '#ffffff';
				$alt = ! $alt;

				$h .= '<tr>';

				foreach ( $cols as $i => $c ) {
					$cell  = isset( $row[ $i ] ) ? $row[ $i ] : array();
					$val   = isset( $cell['value'] ) ? $cell['value'] : '';
					$align = isset( $cell['align'] ) ? $cell['align'] : ( isset( $c['align'] ) ? $c['align'] : 'left' );
					$bold  = ! empty( $cell['bold'] );

					$extra_style = ! empty( $cell['style'] ) ? $cell['style'] : '';

					$style = 'background-color:' . $bg . '; border:1px solid #cfd4dc; color:#222; ' . $extra_style;

					$h .= '<td align="' . esc_attr( $align ) . '" style="' . esc_attr( $style ) . '">'
						. ( $bold ? '<b>' . $val . '</b>' : $val )
						. '</td>';
				}

				$h .= '</tr>';
			}
		}

		$h .= '</table>';

		return $h;
	}

	/** Right-anchored totals panel (50% wide, right aligned). */
	private static function totals_panel_html( array $rows ) {
		$color = self::color();
		$tot   = '<table cellpadding="6" cellspacing="0" border="1" width="100%" style="border-color:#cfd4dc;font-size:10pt;">';
		foreach ( $rows as $r ) {
			$hl = ! empty( $r['highlight'] );
			if ( $hl ) {
				$tot .= '<tr style="background:#eaeef4;"><td width="55%" align="right" style="color:#222;font-weight:bold;font-size:11pt;"><b>' . esc_html( $r['label'] ) . '</b></td>'
					. '<td align="right" style="color:' . $color . ';font-weight:bold;font-size:11pt;">' . $r['value'] . '</td></tr>';
			} else {
				$tot .= '<tr><td width="55%" align="right" style="color:#444;"><b>' . esc_html( $r['label'] ) . '</b></td>'
					. '<td align="right">' . $r['value'] . '</td></tr>';
			}
		}
		$tot .= '</table>';
		return '<br><br><table cellpadding="0" cellspacing="0" border="0" width="100%">
	<tr>
		<td height="10" style="font-size:1px;line-height:1px;">&nbsp;</td>
	</tr>
</table>
<table cellpadding="0" cellspacing="0" border="0" width="100%"><tr>'
			. '<td width="50%">&nbsp;</td>'
			. '<td width="50%">' . $tot . '</td>'
			. '</tr></table>';
	}

	/** Footer / terms band. */
	private static function footer_terms_html( $default = 'Thank you.' ) {
		$terms = tsm_get_setting( 'invoice_terms', '' );
		$h  = '<br><br><table cellpadding="0" cellspacing="0" border="0" width="100%"><tr><td style="border-top:1px solid #cfd4dc;">&nbsp;</td></tr></table>';
		$h .= '<div align="center" style="font-size:8.5pt;color:' . ( $terms ? '#666' : '#999' ) . ';padding-top:8px;">'
			. nl2br( esc_html( $terms ?: $default ) ) . '</div>';
		return $h;
	}

	/** Section heading. */
	private static function section_h( $label ) {
		return '<br><span style="font-size:12pt;font-weight:bold;color:#222;">' . esc_html( $label ) . '</span><br><br>';
	}

	/** Render & ship the PDF. */
	public static function print_pdf( $pdf, $filename ) {
		$pdf->Output( $filename, 'I' );
		exit;
	}

	/* =====================================================================
	 *  REPORT CARD (rebuilt  -  full brand template)
	 * ===================================================================== */

	public static function output_report_card( $student_id ) {
		$student = TSM_Students::get( $student_id );
		if ( ! $student ) wp_die( 'Student not found.' );

		$today   = current_time( 'F j, Y' );
		$classes = TSM_Classes::classes_for_student( $student_id );

		// Build rows + GPA-style aggregate.
		$rows = array();
		$pct_sum = 0; $pct_n = 0;
		foreach ( $classes as $c ) {
			$teacher = $c->teacher_id ? TSM_Teachers::get( $c->teacher_id ) : null;
			$g       = TSM_Gradebook::calc_class_grade( $c->id, $student_id );
			if ( null !== $g['percent'] ) { $pct_sum += (float) $g['percent']; $pct_n++; }
			$rows[] = array(
				array( 'value' => esc_html( $c->name ) ),
				array( 'value' => esc_html( $teacher ? $teacher->first_name . ' ' . $teacher->last_name : ' - ' ) ),
				array( 'value' => esc_html( null !== $g['percent'] ? $g['percent'] . '%' : ' - ' ), 'align' => 'center' ),
				array( 'value' => esc_html( $g['letter'] ?: ' - ' ), 'align' => 'center', 'bold' => true ),
			);
		}
		$overall = $pct_n ? round( $pct_sum / $pct_n, 1 ) : null;
		$letter  = ( null !== $overall && method_exists( 'TSM_Gradebook', 'percent_to_letter' ) )
			? TSM_Gradebook::percent_to_letter( $overall ) : ' - ';

		$pdf = self::new_pdf( 'Report Card  -  ' . $student->first_name . ' ' . $student->last_name );
		$pdf->AddPage();

		$html  = self::header_html( 'Report Card', $today );

		$meta = self::meta_table_html( array(
			array( 'label' => 'Issued',         'value' => esc_html( $today ) ),
			array( 'label' => 'Student ID',     'value' => '#' . esc_html( str_pad( (string) $student->id, 5, '0', STR_PAD_LEFT ) ) ),
			array( 'label' => 'Classes',        'value' => (int) count( $classes ) ),
			array( 'label' => 'Overall Grade',  'value' => null !== $overall ? esc_html( $overall . '%  (' . $letter . ')' ) : ' - ', 'highlight' => true ),
		) );
		$html .= self::from_to_meta_html(
			self::school_block_html( 'School:' ) . '<br><br>' . self::student_block_html( $student, 'Student:' ),
			$meta
		);

		$html .= self::section_h( 'Grades by Class' );
		$html .= self::data_table_html(
			array(
				array( 'label' => 'Class',   'width' => '38%', 'align' => 'left' ),
				array( 'label' => 'Teacher', 'width' => '32%', 'align' => 'left' ),
				array( 'label' => 'Grade',   'width' => '15%', 'align' => 'center' ),
				array( 'label' => 'Letter',  'width' => '15%', 'align' => 'center' ),
			),
			$rows,
			'No classes enrolled.'
		);

		$html .= self::footer_terms_html( 'This report card was generated electronically and is valid without signature.' );

		$pdf->writeHTML( $html, true, false, true, false, '' );
		self::print_pdf( $pdf, 'report-card-' . $student_id . '.pdf' );
	}

	/* =====================================================================
	 *  LEGACY simple statement (kept for back-compat callers).
	 * ===================================================================== */

	public static function output_statement( $student_id ) {
		// Forward to the polished implementation.
		self::output_account_statement( $student_id );
	}

	/* Certificate rendering moved to the "TradeSchool Manager  -  Certificates" add-on plugin. */

	/* =====================================================================
	 *  INVOICE  -  polished bordered layout
	 * ===================================================================== */

	public static function output_invoice( $invoice_id ) {
		$inv = TSMP_Invoices::get( $invoice_id );
		if ( ! $inv ) wp_die( 'Invoice not found.' );
		$student = TSM_Students::get( $inv->student_id );
		$items   = TSMP_Invoices::items( $inv->id );
		$pays    = TSMP_Invoices::payments( $inv->id );
		$cs      = TSM_Settings::currency_symbol();
		$color   = self::color();
		$pay_inst = tsm_get_setting( 'invoice_payment_instructions', '' );
		$bank_det = tsm_get_setting( 'invoice_bank_details', '' );

		$pdf = self::new_pdf( 'Invoice ' . $inv->invoice_number );
		$pdf->AddPage();

		$html = self::header_html( 'Invoice', '#' . $inv->invoice_number );

		// Meta
		$meta_rows = array(
			array( 'label' => 'Invoice Number', 'value' => esc_html( $inv->invoice_number ) ),
			array( 'label' => 'Invoice Date',   'value' => esc_html( $inv->issue_date ) ),
		);
		if ( $inv->due_date ) {
			$meta_rows[] = array( 'label' => 'Due Date', 'value' => esc_html( $inv->due_date ) );
		}
		$meta_rows[] = array( 'label' => 'Total Due', 'value' => esc_html( $cs . number_format( $inv->balance_due, 2 ) ), 'highlight' => true );

		$html .= self::from_to_meta_html(
			self::school_block_html( 'From:' ) . '<br><br>' . self::student_block_html( $student, 'To:' ),
			self::meta_table_html( $meta_rows )
		);

		if ( $inv->memo ) {
			$html .= '<p style="font-size:10pt;color:#333;margin:4px 0 6px;">' . nl2br( esc_html( $inv->memo ) ) . '</p>';
		}

		// Line items.
		$item_rows = array();
		foreach ( $items as $it ) {
			$qty_s = ( fmod( (float) $it->quantity, 1 ) === 0.0 ) ? (string) (int) $it->quantity : rtrim( rtrim( number_format( $it->quantity, 2 ), '0' ), '.' );
			$item_rows[] = array(
				array( 'value' => esc_html( $qty_s ), 'align' => 'center' ),
				array( 'value' => esc_html( $it->description ) ),
				array( 'value' => esc_html( $cs . number_format( $it->unit_price, 2 ) ), 'align' => 'right' ),
				array( 'value' => esc_html( $cs . number_format( $it->line_total, 2 ) ), 'align' => 'right' ),
			);
		}
		$html .= self::data_table_html(
			array(
				array( 'label' => 'Hrs/Qty',     'width' => '12%', 'align' => 'center' ),
				array( 'label' => 'Description', 'width' => '48%', 'align' => 'left' ),
				array( 'label' => 'Rate/Price',  'width' => '20%', 'align' => 'right' ),
				array( 'label' => 'Sub Total',   'width' => '20%', 'align' => 'right' ),
			),
			$item_rows,
			'No line items'
		);

		// Totals.
		$tot_rows = array(
			array( 'label' => 'Sub Total', 'value' => esc_html( $cs . number_format( $inv->subtotal, 2 ) ) ),
		);
		if ( $inv->discount_amount > 0 ) $tot_rows[] = array( 'label' => 'Discount',                                 'value' => '-' . esc_html( $cs . number_format( $inv->discount_amount, 2 ) ) );
		if ( $inv->tax_amount > 0 )      $tot_rows[] = array( 'label' => 'Tax (' . esc_html( $inv->tax_rate ) . '%)','value' => esc_html( $cs . number_format( $inv->tax_amount, 2 ) ) );
		if ( $inv->late_fee > 0 )        $tot_rows[] = array( 'label' => 'Late Fee',                                 'value' => esc_html( $cs . number_format( $inv->late_fee, 2 ) ) );
		if ( $inv->amount_paid > 0 )     $tot_rows[] = array( 'label' => 'Amount Paid',                              'value' => '-' . esc_html( $cs . number_format( $inv->amount_paid, 2 ) ) );
		$tot_rows[] = array( 'label' => 'Total Due', 'value' => esc_html( $cs . number_format( $inv->balance_due, 2 ) ), 'highlight' => true );
		$html .= self::totals_panel_html( $tot_rows );

		// Payment instructions panel.
		if ( $pay_inst || $bank_det ) {
			$html .= '<br><br><table cellpadding="10" cellspacing="0" border="0" width="100%" style="background:#eef1f5;"><tr>';
			$html .= '<td width="50%" valign="top" style="font-size:9.5pt;color:#333;">' . ( $pay_inst ? nl2br( esc_html( $pay_inst ) ) : '&nbsp;' ) . '</td>';
			$html .= '<td width="50%" valign="top" style="font-size:9.5pt;color:#333;">' . ( $bank_det ? nl2br( esc_html( $bank_det ) ) : '&nbsp;' ) . '</td>';
			$html .= '</tr></table>';
		}

		// Payment history.
		if ( ! empty( $pays ) ) {
			$pay_rows = array();
			foreach ( $pays as $p ) {
				$net = (float) $p->amount - (float) $p->refunded_amount;
				$pay_rows[] = array(
					array( 'value' => esc_html( substr( $p->payment_date, 0, 10 ) ) ),
					array( 'value' => esc_html( ucfirst( $p->method ) ) ),
					array( 'value' => esc_html( $p->reference ?: $p->stripe_payment_intent ) ),
					array( 'value' => esc_html( $cs . number_format( $net, 2 ) ), 'align' => 'right' ),
				);
			}
			$html .= self::section_h( 'Payment History' );
			$html .= self::data_table_html(
				array(
					array( 'label' => 'Date',      'width' => '20%', 'align' => 'left' ),
					array( 'label' => 'Method',    'width' => '20%', 'align' => 'left' ),
					array( 'label' => 'Reference', 'width' => '40%', 'align' => 'left' ),
					array( 'label' => 'Amount',    'width' => '20%', 'align' => 'right' ),
				),
				$pay_rows,
				'No payments yet.'
			);
		}

		$html .= self::footer_terms_html( 'Thank you for your business.' );

		$pdf->writeHTML( $html, true, false, true, false, '' );
		self::print_pdf( $pdf, 'invoice-' . $inv->invoice_number . '.pdf' );
	}

	/* =====================================================================
	 *  PAYMENT RECEIPT
	 * ===================================================================== */

	public static function output_payment_receipt( $payment_id ) {
		if ( ! class_exists( 'TSMP_Payments' ) ) wp_die( 'Payments unavailable.' );
		$p = TSMP_Payments::get( (int) $payment_id );
		if ( ! $p ) wp_die( 'Payment not found.' );
		$student = TSM_Students::get( (int) $p->student_id );
		if ( ! $student ) wp_die( 'Student not found.' );
		$inv = ( ! empty( $p->invoice_id ) && class_exists( 'TSMP_Invoices' ) ) ? TSMP_Invoices::get( (int) $p->invoice_id ) : null;
		$cs  = TSM_Settings::currency_symbol();
		$net = max( 0, (float) $p->amount - (float) $p->refunded_amount );

		$receipt_no = 'R-' . str_pad( (string) $p->id, 6, '0', STR_PAD_LEFT );
		$paid_on    = substr( (string) $p->payment_date, 0, 10 );

		$pdf = self::new_pdf( 'Receipt ' . $receipt_no );
		$pdf->AddPage();

		$html  = self::header_html( 'Receipt', '#' . $receipt_no );

		$meta_rows = array(
			array( 'label' => 'Receipt Number', 'value' => esc_html( $receipt_no ) ),
			array( 'label' => 'Payment Date',   'value' => esc_html( $paid_on ) ),
			array( 'label' => 'Method',         'value' => esc_html( ucfirst( (string) $p->method ) ) ),
		);
		if ( $inv ) {
			$meta_rows[] = array( 'label' => 'Invoice', 'value' => esc_html( $inv->invoice_number ) );
		}
		if ( ! empty( $p->reference ) || ! empty( $p->stripe_payment_intent ) ) {
			$meta_rows[] = array( 'label' => 'Reference', 'value' => esc_html( $p->reference ?: $p->stripe_payment_intent ) );
		}
		$meta_rows[] = array( 'label' => 'Amount Received', 'value' => esc_html( $cs . number_format( $net, 2 ) ), 'highlight' => true );

		$html .= self::from_to_meta_html(
			self::school_block_html( 'Received By:' ) . '<br><br>' . self::student_block_html( $student, 'Received From:' ),
			self::meta_table_html( $meta_rows )
		);

		// Body line.
		$desc = $inv
			? sprintf( 'Payment applied to invoice %s.', $inv->invoice_number )
			: 'Payment received on account.';
		$html .= self::section_h( 'Payment Details' );
		$html .= self::data_table_html(
			array(
				array( 'label' => 'Description', 'width' => '70%', 'align' => 'left' ),
				array( 'label' => 'Amount',      'width' => '30%', 'align' => 'right' ),
			),
			array(
				array(
					array( 'value' => esc_html( $desc ) ),
					array( 'value' => esc_html( $cs . number_format( $net, 2 ) ), 'align' => 'right' ),
				),
			),
			'No detail available.'
		);

		// Totals.
		$tot_rows = array(
			array( 'label' => 'Total Received', 'value' => esc_html( $cs . number_format( $net, 2 ) ), 'highlight' => true ),
		);
		if ( (float) $p->refunded_amount > 0 ) {
			$tot_rows[] = array( 'label' => 'Refunded', 'value' => '-' . esc_html( $cs . number_format( $p->refunded_amount, 2 ) ) );
		}
		$html .= self::totals_panel_html( $tot_rows );

		if ( ! empty( $p->notes ) ) {
			$html .= '<br><p style="font-size:10pt;color:#333;">' . nl2br( esc_html( $p->notes ) ) . '</p>';
		}

		$html .= self::footer_terms_html( 'Thank you for your payment.' );

		$pdf->writeHTML( $html, true, false, true, false, '' );
		self::print_pdf( $pdf, 'receipt-' . $receipt_no . '.pdf' );
	}

	/* =====================================================================
	 *  ACCOUNT STATEMENT  -  full professional layout
	 * ===================================================================== */

	public static function output_account_statement( $student_id ) {
		$student = TSM_Students::get( $student_id );
		if ( ! $student ) wp_die( 'Student not found.' );
		$totals = TSMP_Invoices::totals_for_student( $student_id );
		$cs     = TSM_Settings::currency_symbol();
		$color  = self::color();
		$today  = current_time( 'F j, Y' );

		$today_ymd      = current_time( 'Y-m-d' );
		$all_invoices   = TSMP_Invoices::sort_invoices_sequential( array_values( array_filter(
			TSMP_Invoices::for_student( $student_id ),
			function( $inv ) { return ! in_array( $inv->status, array( 'draft', 'void' ), true ); }
		) ) );
		$due_snapshot   = TSMP_Invoices::portal_due_snapshot( $all_invoices, $today_ymd );
		$invoices       = $due_snapshot['display_invoices'];
		$past_due       = $due_snapshot['past_due_total'];
		$due_soon       = $due_snapshot['next_due_total'];
		$amount_due_now = $due_snapshot['amount_due'];
		$next_due       = $due_snapshot['next_due_date'];

		$pdf = self::new_pdf( 'Statement  -  ' . $student->first_name . ' ' . $student->last_name );
		$pdf->AddPage();

		$html = self::header_html( 'Account Statement', $today );

		$meta_rows = array(
			array( 'label' => 'Statement Date', 'value' => esc_html( $today ) ),
			array( 'label' => 'Account ID',     'value' => '#' . esc_html( str_pad( (string) $student->id, 5, '0', STR_PAD_LEFT ) ) ),
			array( 'label' => 'Total Billed',  'value' => esc_html( $cs . number_format( $totals['charged'], 2 ) ) ),
			array( 'label' => 'Total Paid',     'value' => esc_html( $cs . number_format( $totals['paid'], 2 ) ) ),
		);
		if ( $next_due ) $meta_rows[] = array( 'label' => 'Next Due Date', 'value' => esc_html( $next_due ) );
		$meta_rows[] = array( 'label' => 'Amount Due Now', 'value' => esc_html( $cs . number_format( $amount_due_now, 2 ) ), 'highlight' => true );

		$html .= self::from_to_meta_html(
			self::school_block_html( 'From:' ) . '<br><br>' . self::student_block_html( $student, 'Account Holder:' ),
			self::meta_table_html( $meta_rows )
		);

		// Aging breakdown.
		$html .= '<table cellpadding="8" cellspacing="0" border="1" width="100%" style="border-color:#cfd4dc;font-size:10pt;">';
		$html .= '<thead><tr style="background:#f7f9fc;color:#444;font-size:9.5pt;">'
			. '<th width="33%" align="center"><b>Past Due</b></th>'
			. '<th width="34%" align="center"><b>Next Due</b></th>'
			. '<th width="33%" align="center"><b>Amount Due Now</b></th>'
			. '</tr></thead><tbody><tr>'
			. '<td align="center" style="font-size:13pt;font-weight:bold;color:' . ( $past_due > 0 ? '#b42318' : '#222' ) . ';">' . esc_html( $cs . number_format( $past_due, 2 ) ) . '</td>'
			. '<td align="center" style="font-size:13pt;font-weight:bold;color:' . ( $due_soon > 0 ? '#b54708' : '#222' ) . ';">' . esc_html( $cs . number_format( $due_soon, 2 ) ) . '</td>'
			. '<td align="center" style="font-size:13pt;font-weight:bold;color:' . $color . ';">' . esc_html( $cs . number_format( $amount_due_now, 2 ) ) . '</td>'
			. '</tr></tbody></table>';

		// Invoice summary limited to the invoices currently due/relevant in the portal.
		$inv_rows = array();
		foreach ( $invoices as $inv ) {
			$desc = TSMP_Invoices::describe_invoice( $inv );
			$is_past = ( $inv->balance_due > 0 && $inv->due_date && $inv->due_date < $today_ymd );
			$inv_rows[] = array(
				array( 'value' => esc_html( $inv->invoice_number ), 'bold' => true ),
				array( 'value' => esc_html( $desc ) ),
				array( 'value' => esc_html( $inv->due_date ),   'align' => 'center', 'style' => $is_past ? 'color:#b42318;font-weight:bold;' : '' ),
				array( 'value' => esc_html( ucfirst( str_replace( '_', ' ', $inv->status ) ) ), 'align' => 'center' ),
				array( 'value' => esc_html( $cs . number_format( $inv->total, 2 ) ),       'align' => 'right' ),
			);
		}
		$html .= self::section_h( 'Invoice Summary' );
		$html .= self::data_table_html(
			array(
				array( 'label' => 'Invoice #',   'width' => '16%', 'align' => 'left' ),
				array( 'label' => 'Description', 'width' => '46%', 'align' => 'left' ),
				array( 'label' => 'Due',         'width' => '14%', 'align' => 'center' ),
				array( 'label' => 'Status',      'width' => '12%', 'align' => 'center' ),
				array( 'label' => 'Total',       'width' => '12%', 'align' => 'right' ),
			),
			$inv_rows,
			'No invoices on file.'
		);

		// Account activity ledger in chronological / sequential order.
		$ledger = TSMP_Invoices::build_ledger( $student_id );
		$led_rows = array();
		foreach ( $ledger as $e ) {
			$desc = $e['description'];
			if ( $e['type'] === 'charge' && ! empty( $e['invoice_id'] ) ) {
				$inv2 = TSMP_Invoices::get( $e['invoice_id'] );
				if ( $inv2 ) {
					$line_desc = TSMP_Invoices::describe_invoice( $inv2 );
					if ( $line_desc && $line_desc !== ( 'Invoice ' . $inv2->invoice_number ) ) {
						$desc = $inv2->invoice_number . '  -  ' . $line_desc;
					}
				}
			}
			$led_rows[] = array(
				array( 'value' => esc_html( $e['date'] ) ),
				array( 'value' => esc_html( $desc ) ),
				array( 'value' => $e['debit']  > 0 ? esc_html( $cs . number_format( $e['debit'], 2 ) )  : '', 'align' => 'right' ),
				array( 'value' => $e['credit'] > 0 ? esc_html( $cs . number_format( $e['credit'], 2 ) ) : '', 'align' => 'right' ),
				array( 'value' => esc_html( $cs . number_format( $e['balance'], 2 ) ), 'align' => 'right', 'bold' => true ),
			);
		}
		$html .= self::section_h( 'Account Activity' );
		$html .= self::data_table_html(
			array(
				array( 'label' => 'Date',        'width' => '13%', 'align' => 'left' ),
				array( 'label' => 'Description', 'width' => '47%', 'align' => 'left' ),
				array( 'label' => 'Charges',     'width' => '13%', 'align' => 'right' ),
				array( 'label' => 'Payments',    'width' => '13%', 'align' => 'right' ),
				array( 'label' => 'Balance',     'width' => '14%', 'align' => 'right' ),
			),
			$led_rows,
			'No activity'
		);

		// Totals.
		$html .= self::totals_panel_html( array(
			array( 'label' => 'Total Billed', 'value' => esc_html( $cs . number_format( $totals['charged'], 2 ) ) ),
			array( 'label' => 'Total Paid',    'value' => '-' . esc_html( $cs . number_format( $totals['paid'], 2 ) ) ),
			array( 'label' => 'Amount Due Now', 'value' => esc_html( $cs . number_format( $amount_due_now, 2 ) ), 'highlight' => true ),
		) );

		// Payment instructions.
		$pay_inst = tsm_get_setting( 'invoice_payment_instructions', '' );
		$bank_det = tsm_get_setting( 'invoice_bank_details', '' );
		if ( $pay_inst || $bank_det ) {
			$html .= '<br><br><table cellpadding="10" cellspacing="0" border="0" width="100%" style="background:#eef1f5;"><tr>';
			$html .= '<td width="50%" valign="top" style="font-size:9.5pt;color:#333;">' . ( $pay_inst ? nl2br( esc_html( $pay_inst ) ) : '&nbsp;' ) . '</td>';
			$html .= '<td width="50%" valign="top" style="font-size:9.5pt;color:#333;">' . ( $bank_det ? nl2br( esc_html( $bank_det ) ) : '&nbsp;' ) . '</td>';
			$html .= '</tr></table>';
		}

		$html .= self::footer_terms_html( 'Thank you for your business.' );

		$pdf->writeHTML( $html, true, false, true, false, '' );
		self::print_pdf( $pdf, 'statement-' . $student_id . '.pdf' );
	}

	/* =====================================================================
	 *  GENERIC SUMMARY / REPORT  -  usable from anywhere
	 *
	 *  Accepts:
	 *    $args = [
	 *       'title'     => 'Enrollment Summary',
	 *       'subtitle'  => 'Spring 2026',
	 *       'student'   => (object|null),  // optional  -  adds To: block
	 *       'meta'      => [ ['label'=>'', 'value'=>'', 'highlight'=>bool], ... ],
	 *       'sections'  => [
	 *           [
	 *              'heading' => 'Section Title',
	 *              'columns' => [ ['label'=>'', 'width'=>'25%', 'align'=>'left'], ... ],
	 *              'rows'    => [ [ ['value'=>'', 'align'=>'right', 'bold'=>true], ... ], ... ],
	 *              'empty'   => 'No data',
	 *           ], ...
	 *       ],
	 *       'totals'    => [ ['label'=>'', 'value'=>'', 'highlight'=>bool], ... ],
	 *       'filename'  => 'summary.pdf',
	 *    ]
	 * ===================================================================== */
	public static function output_summary_report( array $args ) {
		$title    = isset( $args['title'] )    ? $args['title']    : 'Summary';
		$subtitle = isset( $args['subtitle'] ) ? $args['subtitle'] : current_time( 'F j, Y' );
		$student  = isset( $args['student'] )  ? $args['student']  : null;
		$meta     = isset( $args['meta'] )     ? (array) $args['meta']     : array();
		$sections = isset( $args['sections'] ) ? (array) $args['sections'] : array();
		$totals   = isset( $args['totals'] )   ? (array) $args['totals']   : array();
		$filename = isset( $args['filename'] ) ? $args['filename'] : sanitize_title( $title ) . '.pdf';
		$orientation = ( isset( $args['orientation'] ) && 'L' === strtoupper( (string) $args['orientation'] ) ) ? 'L' : 'P';

		$pdf = self::new_pdf( $title, $orientation );
		$pdf->AddPage();

		$html = self::header_html( $title, $subtitle );

		// From / To + Meta header row.
		$left = self::school_block_html( 'From:' );
		if ( $student ) $left .= '<br><br>' . self::student_block_html( $student, 'Subject:' );
		$html .= self::from_to_meta_html( $left, $meta ? self::meta_table_html( $meta ) : '&nbsp;' );

		// Sections.
		foreach ( $sections as $sec ) {
			$heading = isset( $sec['heading'] ) ? $sec['heading'] : '';
			$cols    = isset( $sec['columns'] ) ? (array) $sec['columns'] : array();
			$rows    = isset( $sec['rows'] )    ? (array) $sec['rows']    : array();
			$empty   = isset( $sec['empty'] )   ? $sec['empty'] : 'No data';
			if ( $heading ) $html .= self::section_h( $heading );
			if ( ! empty( $cols ) ) $html .= self::data_table_html( $cols, $rows, $empty );
		}

		if ( ! empty( $totals ) ) $html .= self::totals_panel_html( $totals );

		$html .= self::footer_terms_html( 'Generated ' . current_time( 'F j, Y g:i a' ) );

		$pdf->writeHTML( $html, true, false, true, false, '' );
		self::print_pdf( $pdf, $filename );
	}
}
