<?php
/**
 * PDF 生成エンジン（ライブラリ抽象化レイヤー）。
 *
 * - 注文 → テンプレート変数 → HTML → PDF の流れを担う。
 * - PDF はサーバーに保存せず、リクエスト毎に動的生成してブラウザへ
 *   ストリーミングする（直リンク漏洩リスクを排除）。
 * - レンダラは `wcjbp_render_pdf` フィルタで差し替え可能。
 *   無料版の既定実装は mPDF（日本語/CJK対応）。
 *
 * @package Neutrope_Japanese_Invoice_PDFs_For_WooCommerce
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class WCJBP_PDF
 */
class WCJBP_PDF {

	/**
	 * 帳票テンプレート用のデータを組み立てる。
	 *
	 * @param WC_Order $order 注文。
	 * @param string   $type  帳票タイプ。
	 * @return array<string,mixed>
	 */
	public static function build_template_data( WC_Order $order, $type ) {
		$data = array(
			'type'            => $type,
			'document_title'  => __( '請求書', 'neutrope-japanese-invoice-pdfs-for-woocommerce' ),
			'order'           => $order,
			'order_number'    => $order->get_order_number(),
			'order_date'      => $order->get_date_created()
				? wc_format_datetime( $order->get_date_created() )
				: '',
			'billing_name'    => trim( $order->get_formatted_billing_full_name() ),
			'billing_company' => $order->get_billing_company(),
			// 宛名・会社名と重複しないよう、住所のみを組み立てる。
			'billing_address' => self::format_address_only( $order ),
			'company_name'    => wcjbp_get_setting( 'company_name' ),
			'company_address' => wcjbp_get_setting( 'company_address' ),
			'logo_path'       => wcjbp_get_logo_path(),
			'items'           => $order->get_items(),
			'currency'        => $order->get_currency(),
			'total'           => $order->get_total(),
			'total_tax'       => $order->get_total_tax(),
			'is_pro'          => wcjbp_is_pro(),
		);

		/**
		 * 帳票テンプレートへ渡すデータをフィルタする。
		 *
		 * Pro版はこのフィルタで適格請求書番号・税率別内訳・支払期限などを
		 * 追加し、テンプレート側のフックで描画する。無料版コードは変更不要。
		 *
		 * @param array<string,mixed> $data  テンプレートデータ。
		 * @param WC_Order             $order 注文。
		 * @param string               $type  帳票タイプ。
		 */
		return apply_filters( 'wcjbp_invoice_template_data', $data, $order, $type );
	}

	/**
	 * 氏名・会社名を含まない住所文字列を組み立てる。
	 *
	 * WooCommerce の get_formatted_billing_address() は氏名・会社名も含むため、
	 * 宛名ブロックと重複する。住所要素のみを WC の整形ロジックで生成する。
	 *
	 * @param WC_Order $order 注文。
	 * @return string 改行（<br/>）区切りの住所HTML。
	 */
	protected static function format_address_only( WC_Order $order ) {
		$address = array(
			'postcode'  => $order->get_billing_postcode(),
			'state'     => $order->get_billing_state(),
			'city'      => $order->get_billing_city(),
			'address_1' => $order->get_billing_address_1(),
			'address_2' => $order->get_billing_address_2(),
			'country'   => $order->get_billing_country(),
		);

		return WC()->countries->get_formatted_address( $address );
	}

	/**
	 * 帳票 HTML を生成する。
	 *
	 * @param WC_Order $order 注文。
	 * @param string   $type  帳票タイプ。
	 * @return string
	 */
	public static function build_html( WC_Order $order, $type ) {
		$data = self::build_template_data( $order, $type );
		return wcjbp_get_template_html( 'invoice.php', $data );
	}

	/**
	 * 請求書PDF用のCSSを取得する。
	 *
	 * テンプレートへ生のCSSを echo するのではなく、mPDF のヘッダーCSSとして
	 * 読み込ませるために用いる（WordPress.org の出力エスケープ指針に適合）。
	 * `wcjbp_invoice_css` フィルタで差し替え可能。
	 *
	 * @return string CSS文字列。
	 */
	public static function get_invoice_css() {
		$css_file = WCJBP_PLUGIN_DIR . 'assets/css/invoice.css';
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- 同梱ローカルCSSの読み込み（リモート取得ではない）。
		$css = is_readable( $css_file ) ? (string) file_get_contents( $css_file ) : '';

		/**
		 * 請求書PDFへ適用するCSSをフィルタする。
		 *
		 * @param string $css 既定のCSS（assets/css/invoice.css）。
		 */
		return (string) apply_filters( 'wcjbp_invoice_css', $css );
	}

	/**
	 * 帳票をPDFとして生成しブラウザへストリーミングする。
	 *
	 * 呼び出し側で nonce・権限・所有者検証を必ず済ませてから呼ぶこと。
	 *
	 * @param WC_Order $order       注文。
	 * @param string   $type        帳票タイプ。
	 * @param string   $disposition 'inline'（表示）または 'attachment'（DL）。
	 * @return void
	 */
	public static function stream( WC_Order $order, $type = 'invoice', $disposition = 'inline' ) {
		if ( ! WCJBP_Pro_Gate::can_generate( $type ) ) {
			wp_die(
				esc_html__( 'この帳票タイプは無料版では利用できません。', 'neutrope-japanese-invoice-pdfs-for-woocommerce' ),
				'',
				array( 'response' => 403 )
			);
		}

		$html     = self::build_html( $order, $type );
		$filename = sanitize_file_name(
			sprintf( '%s-%s.pdf', $type, $order->get_order_number() )
		);

		/**
		 * PDF レンダリングを差し替えるフィルタ。
		 *
		 * null を返すと既定の mPDF レンダラが使われる。文字列（PDFバイナリ）
		 * を返すとその内容がそのまま出力される。Pro版や他ライブラリ連携用。
		 *
		 * @param string|null $pdf      PDFバイナリ、または null（既定処理）。
		 * @param string      $html     生成済みHTML。
		 * @param WC_Order    $order    注文。
		 * @param string      $type     帳票タイプ。
		 */
		$pdf = apply_filters( 'wcjbp_render_pdf', null, $html, $order, $type );

		if ( null === $pdf ) {
			// mPDF 既定処理は内部でストリーミングして exit する。
			self::render_with_mpdf( $html, $filename, $disposition );
			return;
		}

		self::output_binary( (string) $pdf, $filename, $disposition );
	}

	/**
	 * 既定レンダラ：mPDF。
	 *
	 * @param string $html        HTML。
	 * @param string $filename    出力ファイル名。
	 * @param string $disposition 'inline' または 'attachment'。
	 * @return void
	 */
	protected static function render_with_mpdf( $html, $filename, $disposition ) {
		if ( ! class_exists( '\Mpdf\Mpdf' ) ) {
			wp_die(
				esc_html__(
					'PDF生成ライブラリ（mPDF）が見つかりません。プラグインディレクトリで「composer install」を実行してください。',
					'neutrope-japanese-invoice-pdfs-for-woocommerce'
				),
				esc_html__( 'PDF生成エラー', 'neutrope-japanese-invoice-pdfs-for-woocommerce' ),
				array( 'response' => 500 )
			);
		}

		// mPDF の CJK フォント（Noto Sans JP）サブセット化はメモリを多く使う。
		// 共有ホストの既定 memory_limit では不足しやすいため引き上げる。
		if ( function_exists( 'wp_raise_memory_limit' ) ) {
			wp_raise_memory_limit( 'admin' );
		}

		try {
			// 一時dir解決も try 内で行い、失敗時は下の catch でログ＆通知する。
			$temp_dir = self::get_temp_dir();

			// 同梱フォントを明示登録する。
			// 本プラグインの帳票本文は Noto Sans JP（OFL 1.1）で完結させる方針
			// だが、mPDF コア内部で sans-serif 系のフォールバックとして
			// DejaVuSansCondensed 系（Bitstream Vera / DejaVu Fonts License）
			// が参照される箇所がある（例：CSS の sans-serif マッチング、
			// useSubstitutions 無効でも一部の文字メトリクス処理で要求される）。
			// このため mPDF 既定の fontdata はそのまま維持し、対応する
			// TTF を assets/fonts に同梱して fontDir から発見できるようにしている。
			// 配布サイズを抑えるため、mPDF stock 同梱の ttfonts（87MB）全体は
			// 引き続き除外し、必要な 4 ファイルだけを assets/fonts に置く。
			$default_config = ( new \Mpdf\Config\ConfigVariables() )->getDefaults();
			$font_dirs      = $default_config['fontDir'];

			$default_fonts = ( new \Mpdf\Config\FontVariables() )->getDefaults();
			$font_data     = $default_fonts['fontdata'];

			$jp_font_dir = WCJBP_PLUGIN_DIR . 'assets/fonts';

			// mPDF stock の ttfonts は配布から除外しているため、参照される側の
			// パスを fontDir から落としておく（残しても害はないが、明示する）。
			$mpdf_stock_dir = WCJBP_PLUGIN_DIR . 'vendor/mpdf/mpdf/ttfonts';
			$mpdf_stock     = realpath( $mpdf_stock_dir );
			if ( false !== $mpdf_stock ) {
				$font_dirs = array_values(
					array_filter(
						$font_dirs,
						static function ( $dir ) use ( $mpdf_stock ) {
							$resolved = realpath( $dir );
							return false === $resolved || $resolved !== $mpdf_stock;
						}
					)
				);
			}

			$config = apply_filters(
				'wcjbp_mpdf_config',
				array(
					'mode'             => 'utf-8',
					'format'           => 'A4',
					'tempDir'          => $temp_dir,
					// A4 余白（mm）。左右を広めに取り、業務帳票として
					// 自然な本文幅（210 - 36 = 174mm）に固定する。
					'margin_top'       => 16,
					'margin_bottom'    => 16,
					'margin_left'      => 18,
					'margin_right'     => 18,

					// 帳票本文の確定的なレンダリング設定（言語自動マッピングや
					// CSS による不確実なフォント置換を抑止する）。
					'autoScriptToLang' => false,
					'autoLangToFont'   => false,
					'useSubstitutions' => false,
					'backupSubsFont'   => 'notosansjp',

					// fontDir: mPDF 既定（不在でも害なし）＋ 同梱フォント置き場。
					'fontDir'          => array_merge( $font_dirs, array( $jp_font_dir ) ),

					// fontdata: 既定値（DejaVuSansCondensed 等の宣言を含む）を
					// 維持しつつ、Noto Sans JP を追加する。
					'fontdata'         => $font_data + array(
						'notosansjp' => array(
							'R'          => 'NotoSansJP-Regular.ttf',
							'useOTL'     => 0xFF,
							'useKashida' => 75,
						),
					),
					'default_font'     => 'notosansjp',
				)
			);

			$mpdf = new \Mpdf\Mpdf( $config );
			$mpdf->SetTitle( $filename );

			// スタイルは mPDF のヘッダーCSSとして読み込む。テンプレートに生CSSを
			// echo しないため、出力エスケープ（WordPress.Security.EscapeOutput）の
			// 懸念が発生しない。本文HTMLはそのあとに書き込む。
			$css = self::get_invoice_css();
			if ( '' !== $css ) {
				$mpdf->WriteHTML( $css, \Mpdf\HTMLParserMode::HEADER_CSS );
			}
			$mpdf->WriteHTML( $html );

			$dest = ( 'attachment' === $disposition )
				? \Mpdf\Output\Destination::DOWNLOAD
				: \Mpdf\Output\Destination::INLINE;

			// バッファをクリアしてからストリーミング（破損防止）。
			if ( ob_get_length() ) {
				ob_end_clean();
			}
			nocache_headers();
			$mpdf->Output( $filename, $dest );
			exit;
		} catch ( \Throwable $e ) {
			// 真因はサーバーのエラーログへ常時記録する（画面には一切出さない）。
			// 本番で WP_DEBUG が無効でも原因追跡できるよう WP_DEBUG ガードは外す。
			error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
				sprintf(
					'WCJBP PDF generation failed: [%1$s] %2$s in %3$s:%4$d',
					get_class( $e ),
					$e->getMessage(),
					$e->getFile(),
					(int) $e->getLine()
				)
			);
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				error_log( 'WCJBP PDF trace: ' . $e->getTraceAsString() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
			}
			wp_die(
				esc_html__( 'PDFの生成中にエラーが発生しました。', 'neutrope-japanese-invoice-pdfs-for-woocommerce' ),
				esc_html__( 'PDF生成エラー', 'neutrope-japanese-invoice-pdfs-for-woocommerce' ),
				array( 'response' => 500 )
			);
		}
	}

	/**
	 * 既製の PDF バイナリをそのまま出力する（フィルタ差し替え時）。
	 *
	 * @param string $pdf         PDFバイナリ。
	 * @param string $filename    ファイル名。
	 * @param string $disposition 'inline' または 'attachment'。
	 * @return void
	 */
	protected static function output_binary( $pdf, $filename, $disposition ) {
		if ( ob_get_length() ) {
			ob_end_clean();
		}
		nocache_headers();
		header( 'Content-Type: application/pdf' );
		header(
			sprintf(
				'Content-Disposition: %s; filename="%s"',
				( 'attachment' === $disposition ) ? 'attachment' : 'inline',
				$filename
			)
		);
		header( 'Content-Length: ' . strlen( $pdf ) );
		echo $pdf; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- バイナリPDF。
		exit;
	}

	/**
	 * mPDF 用の書き込み可能な一時ディレクトリを確保する。
	 *
	 * uploads が書込不可な共有ホスト（open_basedir/権限）でも動くよう、
	 * システム一時ディレクトリへフォールバックする。いずれも不可なら
	 * 例外を投げ、上位 catch でログ＆ユーザー通知する。
	 *
	 * @return string
	 * @throws \RuntimeException 書き込み可能な一時ディレクトリが無い場合。
	 */
	protected static function get_temp_dir() {
		$candidates = array();

		$upload = wp_upload_dir();
		if ( empty( $upload['error'] ) && ! empty( $upload['basedir'] ) ) {
			$candidates[] = trailingslashit( $upload['basedir'] ) . 'wcjbp-tmp';
		}
		// フォールバック: システム一時ディレクトリ。
		$candidates[] = trailingslashit( get_temp_dir() ) . 'wcjbp-mpdf';

		foreach ( $candidates as $dir ) {
			if ( ! file_exists( $dir ) ) {
				wp_mkdir_p( $dir );
			}
			if ( ! is_dir( $dir ) || ! wp_is_writable( $dir ) ) {
				continue;
			}

			// 一時ファイルの直アクセスを拒否。
			$htaccess = trailingslashit( $dir ) . '.htaccess';
			if ( ! file_exists( $htaccess ) ) {
				// 一時dirの直アクセス防止。WP_Filesystem は生成のたびに過剰なため直接書き込む.
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents, WordPress.PHP.NoSilencedErrors.Discouraged
				@file_put_contents( $htaccess, "Deny from all\n" );
			}
			$index = trailingslashit( $dir ) . 'index.html';
			if ( ! file_exists( $index ) ) {
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents, WordPress.PHP.NoSilencedErrors.Discouraged
				@file_put_contents( $index, '' );
			}

			return $dir;
		}

		throw new \RuntimeException(
			esc_html( 'WCJBP: no writable temp directory for mPDF (tried: ' . implode( ', ', $candidates ) . ')' )
		);
	}
}
