<?php
declare(strict_types=1);
namespace ConfManager\Fonts;

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


use ConfManager\Traits\Singleton;
use ConfManager\Traits\HasHooks;

/**
 * フォントマネージャー
 *
 * PDF 生成に必要な日本語フォントのダウンロード・管理を行う。
 * 管理画面でフォント未配置時に通知を表示し、ワンクリックでダウンロード可能。
 *
 * @since 1.4.0
 */
class FontManager {

	use Singleton;
	use HasHooks;

	/**
	 * フォント定義（キー => 設定）
	 */
	private const FONTS = [
		'ipaexg' => [
			'file' => 'ipaexg.ttf',
			'name' => 'IPAexゴシック',
			'url'  => 'https://cs24.biz/conference/fonts/ipaexg00401.zip',
			'zip_entry' => 'ipaexg00401/ipaexg.ttf',
		],
		'ipaexm' => [
			'file' => 'ipaexm.ttf',
			'name' => 'IPAex明朝',
			'url'  => 'https://cs24.biz/conference/fonts/ipaexm00401.zip',
			'zip_entry' => 'ipaexm00401/ipaexm.ttf',
		],
	];

	/**
	 * 手動アップロードを受け付けるフォント拡張子とサイズ上限
	 */
	private const UPLOAD_MAX_BYTES = 20971520; // 20MB

	/**
	 * 有効な TrueType / OpenType マジックバイト（先頭4バイト）
	 */
	private const FONT_MAGIC = [ "\x00\x01\x00\x00", 'true', 'ttcf', 'OTTO' ];

	protected function init(): void {
		$this->register_hooks();
	}

	protected function register_hooks(): void {
		$this->add_action( 'rest_api_init', [ $this, 'register_routes' ] );
		$this->add_action( 'admin_notices', [ $this, 'maybe_show_font_notice' ] );
	}

	/**
	 * REST API ルート登録
	 */
	public function register_routes(): void {
		register_rest_route( 'conf-manager/v1', '/admin/fonts/download', [
			[
				'methods'             => \WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'handle_download' ],
				'permission_callback' => function () {
					return current_user_can( 'conf_manager_manage_settings' );
				},
			],
		] );

		// 手動アップロード（自動DLできない環境向けフォールバック）
		register_rest_route( 'conf-manager/v1', '/admin/fonts/upload', [
			[
				'methods'             => \WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'handle_upload' ],
				'permission_callback' => function () {
					return current_user_can( 'conf_manager_manage_settings' );
				},
			],
		] );
	}

	/**
	 * フォントディレクトリのパスを返す
	 *
	 * フォントは実行時にダウンロード／アップロードされる可変データのため、
	 * プラグインフォルダ内ではなく wp-content/uploads 配下に保存する
	 * （プラグイン更新で消えず、公開ディレクトリへの書き込みも避けられる）。
	 */
	public static function get_font_dir(): string {
		$upload_dir = wp_upload_dir();
		return $upload_dir['basedir'] . '/conf-manager/fonts';
	}

	/**
	 * 全フォントの状態を返す
	 *
	 * @return array<string, array{name: string, file: string, exists: bool}>
	 */
	public static function get_status(): array {
		$font_dir = self::get_font_dir();
		$result   = [];

		foreach ( self::FONTS as $key => $font ) {
			$result[ $key ] = [
				'name'   => $font['name'],
				'file'   => $font['file'],
				'exists' => file_exists( $font_dir . '/' . $font['file'] ),
			];
		}

		return $result;
	}

	/**
	 * 必須フォント（IPAexゴシック）が配置済みか
	 */
	public static function has_required_fonts(): bool {
		return file_exists( self::get_font_dir() . '/ipaexg.ttf' );
	}

	/**
	 * 管理画面通知（Conf Manager 管理ページ以外でも表示）
	 */
	public function maybe_show_font_notice(): void {
		if ( self::has_required_fonts() ) {
			return;
		}

		// 既に非表示にした場合はスキップ
		if ( get_option( 'conf_manager_font_notice_dismissed' ) ) {
			return;
		}

		$screen = get_current_screen();
		if ( ! $screen || ! current_user_can( 'manage_options' ) ) {
			return;
		}

		printf(
			'<div class="notice notice-warning is-dismissible"><p><strong>Conference Manager:</strong> %s <a href="%s">%s</a></p></div>',
			esc_html__( 'Japanese fonts required for PDF generation are not installed. You can download them from the Dashboard.', 'conferencemanager' ),
			esc_url( admin_url( 'admin.php?page=conferencemanager#/dashboard' ) ),
			esc_html__( 'Go to Dashboard', 'conferencemanager' )
		);
	}

	/**
	 * フォントダウンロード API ハンドラー
	 */
	public function handle_download( \WP_REST_Request $request ): \WP_REST_Response {
		$font_dir = self::get_font_dir();

		// ディレクトリ作成
		if ( ! file_exists( $font_dir ) ) {
			\wp_mkdir_p( $font_dir );
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Pre-flight permission check before calling download_url/wp_remote_get; WP_Filesystem would require FTP credentials prompt.
		if ( ! is_writable( $font_dir ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => __( 'No write permission to the font directory.', 'conferencemanager' ),
			], 500 );
		}

		$results  = [];
		$has_error = false;

		foreach ( self::FONTS as $key => $font ) {
			$dest = $font_dir . '/' . $font['file'];

			// 既にダウンロード済み
			if ( file_exists( $dest ) ) {
				$results[ $key ] = [
					'success' => true,
					'message' => __( 'Already downloaded', 'conferencemanager' ),
				];
				continue;
			}

			$result = $this->download_font( $font, $dest );
			$results[ $key ] = $result;

			if ( ! $result['success'] ) {
				$has_error = true;
			}
		}

		// 通知を非表示に
		if ( ! $has_error ) {
			update_option( 'conf_manager_font_notice_dismissed', true );
		}

		return new \WP_REST_Response( [
			'success' => ! $has_error,
			'fonts'   => $results,
			'status'  => self::get_status(),
		], $has_error ? 207 : 200 );
	}

	/**
	 * 個別フォントのダウンロード処理
	 *
	 * @param array  $font フォント定義
	 * @param string $dest 保存先パス
	 * @return array{success: bool, message: string}
	 */
	private function download_font( array $font, string $dest ): array {
		// download_url() は wp-admin/includes/file.php に定義されているため、使用直前に読み込む。
		if ( ! function_exists( 'download_url' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}

		/**
		 * フォント配信元 URL をフィルタで上書き可能にする（自社ミラー差し替え用）。
		 *
		 * @param string $url  既定の配信元 URL
		 * @param string $file フォントファイル名（ipaexg.ttf 等）
		 */
		$url = (string) apply_filters( 'conf_manager_font_source_url', $font['url'], $font['file'] );

		// 一時的なネットワーク不調に備え最大2回リトライ（1回90秒）。
		$tmp_file  = null;
		$last_err  = '';
		$max_tries = 2;
		for ( $try = 1; $try <= $max_tries; $try++ ) {
			$tmp_file = \download_url( $url, 90 );
			if ( ! is_wp_error( $tmp_file ) ) {
				break;
			}
			$last_err = $tmp_file->get_error_message();
		}

		if ( is_wp_error( $tmp_file ) ) {
			return [
				'success' => false,
				'message' => sprintf(
					/* translators: %s: error message */
					__( 'Download failed: %s', 'conferencemanager' ),
					$last_err
				),
			];
		}

		// ZIP から TTF を展開
		$result = $this->extract_ttf_from_zip( $tmp_file, $font['zip_entry'], $dest );

		// 一時ファイル削除
		wp_delete_file( $tmp_file );

		return $result;
	}

	/**
	 * ZIP ファイルから TTF を展開する
	 *
	 * @param string $zip_path  ZIP ファイルパス
	 * @param string $entry     ZIP 内のエントリパス
	 * @param string $dest      保存先パス
	 * @return array{success: bool, message: string}
	 */
	private function extract_ttf_from_zip( string $zip_path, string $entry, string $dest ): array {
		if ( ! class_exists( 'ZipArchive' ) ) {
			return [
				'success' => false,
				'message' => __( 'ZipArchive is not available. Please enable the PHP zip extension.', 'conferencemanager' ),
			];
		}

		$zip = new \ZipArchive();

		if ( true !== $zip->open( $zip_path ) ) {
			return [
				'success' => false,
				'message' => __( 'Could not open the ZIP file.', 'conferencemanager' ),
			];
		}

		$content = $zip->getFromName( $entry );
		$zip->close();

		if ( false === $content ) {
			return [
				'success' => false,
				'message' => sprintf(
					/* translators: %s: zip entry path */
					__( 'Font file not found in the ZIP: %s', 'conferencemanager' ),
					$entry
				),
			];
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- 検証済みフォントを uploads 配下の固定パスへ保存。WP_Filesystem は FTP 認証を要求するため不可。
		$written = file_put_contents( $dest, $content );

		if ( false === $written ) {
			return [
				'success' => false,
				'message' => __( 'Failed to write font file.', 'conferencemanager' ),
			];
		}

		return [
			'success' => true,
			'message' => __( 'Download complete', 'conferencemanager' ),
		];
	}

	/**
	 * フォント手動アップロード API ハンドラー
	 *
	 * 自動ダウンロードできない環境向けに、管理者が IPAex フォント（.ttf）を
	 * ダッシュボードから直接アップロードして uploads/conf-manager/fonts/ に配置できるようにする。
	 * 保存先ファイル名は FONTS 定義のホワイトリストに固定し、TrueType/OpenType の
	 * マジックバイトを検証する。
	 */
	public function handle_upload( \WP_REST_Request $request ): \WP_REST_Response {
		$key = sanitize_key( (string) ( $request->get_param( 'font' ) ?? '' ) );

		if ( ! isset( self::FONTS[ $key ] ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => __( 'Unknown font type.', 'conferencemanager' ),
			], 400 );
		}

		$files = $request->get_file_params();
		if ( empty( $files['file'] ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => __( 'No file selected.', 'conferencemanager' ),
			], 400 );
		}

		$file = $files['file'];

		// PHP 側のアップロードエラー（サイズ超過等）を tmp_name チェックより先に判定する。
		$upload_error = (int) ( $file['error'] ?? UPLOAD_ERR_OK );
		if ( UPLOAD_ERR_OK !== $upload_error ) {
			if ( UPLOAD_ERR_INI_SIZE === $upload_error || UPLOAD_ERR_FORM_SIZE === $upload_error ) {
				$message = __( 'The file size exceeds the server upload limit. Please increase php.ini (upload_max_filesize / post_max_size) on your server, or contact your hosting provider.', 'conferencemanager' );
			} else {
				$message = __( 'Upload failed. Please check your server settings.', 'conferencemanager' );
			}
			return new \WP_REST_Response( [
				'success' => false,
				'message' => $message,
			], 400 );
		}

		if ( empty( $file['tmp_name'] ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => __( 'No file selected.', 'conferencemanager' ),
			], 400 );
		}

		// 拡張子チェック（.ttf のみ）
		$ext = strtolower( pathinfo( (string) $file['name'], PATHINFO_EXTENSION ) );
		if ( 'ttf' !== $ext ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => __( 'Only .ttf font files can be uploaded.', 'conferencemanager' ),
			], 400 );
		}

		// サイズ上限
		if ( (int) $file['size'] > self::UPLOAD_MAX_BYTES ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => __( 'The file is too large (limit 20 MB).', 'conferencemanager' ),
			], 400 );
		}

		// 実際に HTTP アップロードされた一時ファイルであることを保証
		if ( ! is_uploaded_file( $file['tmp_name'] ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => __( 'Invalid upload.', 'conferencemanager' ),
			], 400 );
		}

		// アップロード一時ファイルを読み込む（is_uploaded_file 検証済み）。
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- ローカルのアップロード一時ファイル読込。WP_Filesystem は任意 tmp パスを扱えず FTP 認証を要求するため不可。
		$content = file_get_contents( $file['tmp_name'] );
		if ( false === $content || '' === $content ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => __( 'Could not read the uploaded file.', 'conferencemanager' ),
			], 400 );
		}

		// 先頭4バイトで TrueType/OpenType を検証
		if ( ! in_array( substr( $content, 0, 4 ), self::FONT_MAGIC, true ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => __( 'The file could not be recognized as a TrueType/OpenType font. Please specify a valid .ttf file.', 'conferencemanager' ),
			], 400 );
		}

		// 保存先（ファイル名はホワイトリスト固定）
		$font_dir = self::get_font_dir();
		if ( ! file_exists( $font_dir ) ) {
			\wp_mkdir_p( $font_dir );
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- 書込権限の事前確認。WP_Filesystem は FTP 認証を要求するため不可。
		if ( ! is_writable( $font_dir ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => __( 'No write permission to the font directory.', 'conferencemanager' ),
			], 500 );
		}

		$dest = $font_dir . '/' . self::FONTS[ $key ]['file'];

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- 検証済みフォントを固定パスへ保存（既存の自動DLと同方式）。WP_Filesystem は FTP 認証を要求するため不可。
		if ( false === file_put_contents( $dest, $content ) ) {
			return new \WP_REST_Response( [
				'success' => false,
				'message' => __( 'Failed to save the font file.', 'conferencemanager' ),
			], 500 );
		}

		// 全フォント揃ったら通知を非表示に
		if ( self::has_required_fonts() && file_exists( $font_dir . '/ipaexm.ttf' ) ) {
			update_option( 'conf_manager_font_notice_dismissed', true );
		}

		return new \WP_REST_Response( [
			'success' => true,
			'message' => sprintf(
				/* translators: %s: font display name */
				__( 'Uploaded %s.', 'conferencemanager' ),
				self::FONTS[ $key ]['name']
			),
			'status'  => self::get_status(),
		], 200 );
	}
}
