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

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

use ConfManager\Traits\Singleton;
use ConfManager\Notification\TransactionalMailer;
use ConfManager\Security\AuditLog;

/**
 * 参加者の請求書・領収書を管理画面から発行する
 *
 * ## なぜ在るか
 *
 * これまで参加者の帳票に到達できるのは**参加者本人のマイページ**と
 * **入金確認メールの添付**だけだった。決済管理・参加者リストのどちらにも
 * 発行ボタンが無く、管理用の REST ルートも存在しなかった。事務局が代理で
 * 出す手段が無く、「領収書をもう一度」に応えられなかった。
 * ⚠️ 決済管理の画面には「領収書は入金確認後に発行可能」と書いてあるのに、
 * どこから発行するのかが書かれておらず同じ画面にボタンも無かったため、
 * 利用者からは「ボタンが消えた」と見えていた。
 *
 * ## 設計
 *
 * - **帳票行が無ければ作ってから描画する。** 作成は `TransactionalMailer` の
 *   自動生成メソッド（冪等）に委ね、ここでは複製しない。宛名・但し書きの
 *   組み立てが 2 箇所に分かれると、片方だけ直った帳票が出回る。
 * - **PDF はディスクに書かない。** `render_binary()` で都度描画して base64 で返す。
 *   マイページ側は uploads 配下に**連番のファイル名**で置いた PDF を URL で配る
 *   既存の作りだが、こちらで公開 URL を増やさない。
 * - 🔴 **申込から辿るとき、候補が 2 件以上なら発行しない。**
 *   1 つの申込に決済が複数ぶら下がることは実際にある。領収書は法的な意味を持つ
 *   書類なので、黙って新しい方を選ぶより「決済管理から選べ」と言う方が安全。
 *   決済管理は決済単位なので、そちらには必ず曖昧さの無い入口がある。
 * - **2 回押しても採番は増えない。**「同じ内容で番号違いの請求書」を作らないことを
 *   最優先にした（既に在る帳票を探してから作る）。
 */
class PaymentDocuments {

	use Singleton;

	public const TYPE_INVOICE = 'invoice';
	public const TYPE_RECEIPT = 'receipt';

	/**
	 * 発行元。監査ログの行為者を分けるためだけに使う
	 *
	 * 事務局の再発行と、参加者が自分でマイページから落としたのは別の出来事なので、
	 * 同じ名前で混ぜない（「誰が出したか」を後から追えなくなる）。
	 */
	public const SOURCE_ADMIN  = 'admin';
	public const SOURCE_MYPAGE = 'mypage';

	/**
	 * 🔴 帳票を**新しく発行してよい**決済ステータス。可否の正典はここ 1 箇所
	 *
	 * ⚠️ 「発行してよいか」と「既に在るものを再取得してよいか」は別。
	 * ここで見るのは**前者だけ**で、既に採番済みの帳票は状態が変わったあとでも
	 * `find_document()` が先に拾って再描画される（入金後に返金された領収書を
	 * 参加者が落とせなくなると困るため）。
	 *
	 * ⚠️ この 2 つは `src/admin/pages/Payments.jsx` の `BILLABLE_STATUSES` /
	 * `RECEIPT_STATUSES` と対になっている。**片方だけ変えるとボタンは出るのに
	 * サーバが断る**（この不一致こそが v1.6.7 で直した中身）。
	 */
	public const BILLABLE_STATUSES = [ 'pending', 'completed', 'refunded', 'partial_refund' ];
	public const RECEIPT_STATUSES  = [ 'completed' ];

	/**
	 * 領収書ボタンを出してよい決済ステータス（＝控えが既に在りうる状態）
	 *
	 * 領収書は `completed` でしか採番されないが、そのあと返金されて
	 * `refunded` / `partial_refund` に移る。**採番済みの控えはそこでも落とせる**
	 * ので、ボタンを出す条件は `RECEIPT_STATUSES` より広い。
	 *
	 * 🔴 「広い」を野放しにしない。ここに無い状態（`pending` / `failed`）で
	 * ボタンを出すと、押しても `payment_not_completed` で断られる＝E そのもの。
	 * `M-18` が JSX の `RECEIPT_STATUSES` とこの定数の**一致**を見る。
	 */
	public const RECEIPT_VISIBLE_STATUSES = [ 'completed', 'refunded', 'partial_refund' ];

	/**
	 * 決済 ID から帳票を発行する（決済管理の操作列）
	 *
	 * @param int    $payment_id 決済ID
	 * @param string $type       self::TYPE_INVOICE | self::TYPE_RECEIPT
	 * @return array{filename:string,pdf_b64:string,document_no:string}|\WP_Error
	 */
	public function issue_for_payment( int $payment_id, string $type, string $source = self::SOURCE_ADMIN ): array|\WP_Error {
		if ( ! in_array( $type, [ self::TYPE_INVOICE, self::TYPE_RECEIPT ], true ) ) {
			return new \WP_Error( 'invalid_document_type', __( 'Invalid document type.', 'conferencemanager' ), [ 'status' => 400 ] );
		}

		$payment = PaymentManager::instance()->get( $payment_id );
		if ( null === $payment ) {
			return new \WP_Error( 'payment_not_found', __( 'Payment record not found.', 'conferencemanager' ), [ 'status' => 404 ] );
		}

		$document = $this->find_document( $payment_id, $type );

		if ( null === $document ) {
			$created = $this->create_document( $payment, $type );
			if ( is_wp_error( $created ) ) {
				return $created;
			}
			$document = $this->find_document( $payment_id, $type );
		}

		if ( null === $document ) {
			return new \WP_Error(
				'document_unavailable',
				self::TYPE_INVOICE === $type
					? __( 'Could not issue the invoice. The registration or member record was not found.', 'conferencemanager' )
					: __( 'Could not issue the receipt. The registration or member record was not found.', 'conferencemanager' ),
				[ 'status' => 400 ]
			);
		}

		$rendered = $this->render( (int) $document['id'], $type );
		if ( is_wp_error( $rendered ) ) {
			return $rendered;
		}

		/*
		 * 誰がいつ帳票を出したかを残す。
		 *
		 * create_invoice / create_receipt は**採番したときにしか残らない**ので、
		 * 「事務局が再発行した」記録はこれが唯一の手がかりになる。
		 */
		$action = self::SOURCE_MYPAGE === $source
			? ( self::TYPE_RECEIPT === $type ? 'mypage_receipt_download' : 'mypage_invoice_download' )
			: ( self::TYPE_RECEIPT === $type ? 'issue_receipt_pdf' : 'issue_invoice_pdf' );

		AuditLog::instance()->log(
			$action,
			$type,
			(int) $document['id'],
			[
				'payment_id'  => $payment_id,
				'document_no' => $rendered['document_no'],
			]
		);

		return $rendered;
	}

	/**
	 * 申込 ID から帳票を発行する（参加者リストの操作列）
	 *
	 * @param int    $registration_id 申込ID
	 * @param string $type            self::TYPE_INVOICE | self::TYPE_RECEIPT
	 * @return array{filename:string,pdf_b64:string,document_no:string}|\WP_Error
	 */
	public function issue_for_registration( int $registration_id, string $type, string $source = self::SOURCE_ADMIN ): array|\WP_Error {
		$payment_id = $this->resolve_payment_id( $registration_id, $type );
		if ( is_wp_error( $payment_id ) ) {
			return $payment_id;
		}

		return $this->issue_for_payment( $payment_id, $type, $source );
	}

	/**
	 * 申込にぶら下がる決済のうち、帳票を出すべき 1 件を決める
	 *
	 * 手順は 2 段。**既に帳票が在るものを優先**し、無ければ状態で絞る。
	 * どちらの段でも **2 件以上なら選ばずにエラーを返す**（docblock 冒頭の理由）。
	 *
	 * @param int    $registration_id 申込ID
	 * @param string $type            帳票種別
	 * @return int|\WP_Error
	 */
	public function resolve_payment_id( int $registration_id, string $type ): int|\WP_Error {
		$payments = PaymentManager::instance()->get_by_registration( $registration_id );

		if ( empty( $payments ) ) {
			return new \WP_Error(
				'no_payment',
				__( 'This registration has no payment record. Invoices and receipts cannot be issued for free (0 yen) registrations.', 'conferencemanager' ),
				[ 'status' => 404 ]
			);
		}

		// 段 1: 既に発行済みの帳票を持つ決済
		$issued = array_values( array_filter(
			$payments,
			fn( $p ) => null !== $this->find_document( (int) $p['id'], $type )
		) );

		if ( 1 === count( $issued ) ) {
			return (int) $issued[0]['id'];
		}
		if ( count( $issued ) > 1 ) {
			return $this->ambiguous_error( $type, count( $issued ) );
		}

		// 段 2: これから発行できる決済
		$eligible = array_values( array_filter(
			$payments,
			fn( $p ) => in_array(
				$p['status'],
				self::TYPE_RECEIPT === $type ? self::RECEIPT_STATUSES : self::BILLABLE_STATUSES,
				true
			)
		) );

		if ( 0 === count( $eligible ) ) {
			return self::TYPE_RECEIPT === $type
				? new \WP_Error(
					'payment_not_completed',
					__( 'This registration has no confirmed payment. Confirm the payment before issuing a receipt.', 'conferencemanager' ),
					[ 'status' => 400 ]
				)
				: new \WP_Error(
					'no_billable_payment',
					__( 'This registration has no payment that an invoice can be issued for.', 'conferencemanager' ),
					[ 'status' => 400 ]
				);
		}
		if ( count( $eligible ) > 1 ) {
			return $this->ambiguous_error( $type, count( $eligible ) );
		}

		return (int) $eligible[0]['id'];
	}

	/**
	 * 候補が複数あるときのエラー
	 *
	 * @param string $type  帳票種別
	 * @param int    $count 候補件数
	 */
	private function ambiguous_error( string $type, int $count ): \WP_Error {
		return new \WP_Error(
			'ambiguous_payment',
			sprintf(
				/* translators: 1: number of payment records, 2: document name (invoice or receipt) */
				__( 'This registration has %1$d payment records, so it is not clear which one the %2$s is for. Choose the payment in Payment Management and issue it there.', 'conferencemanager' ),
				$count,
				self::TYPE_RECEIPT === $type
					? __( 'receipt', 'conferencemanager' )
					: __( 'invoice', 'conferencemanager' )
			),
			[ 'status' => 409 ]
		);
	}

	/**
	 * 決済に紐づく帳票行を取得する
	 *
	 * @param int    $payment_id 決済ID
	 * @param string $type       帳票種別
	 */
	private function find_document( int $payment_id, string $type ): ?array {
		return self::TYPE_RECEIPT === $type
			? ReceiptGenerator::instance()->get_by_payment( $payment_id )
			: InvoiceGenerator::instance()->get_by_payment( $payment_id );
	}

	/**
	 * 帳票行を作る（自動生成と同じ経路を通す）
	 *
	 * ⚠️ TransactionalMailer の自動生成は**戻り値を返さない**ので、
	 * ここで先に前提を検査して理由の分かるエラーにする。呼んだ後に
	 * find_document() で作れたかどうかを見るのは呼び出し元の責任。
	 *
	 * @param array  $payment 決済行
	 * @param string $type    帳票種別
	 * @return true|\WP_Error
	 */
	private function create_document( array $payment, string $type ): true|\WP_Error {
		$registration_id = (int) $payment['registration_id'];
		if ( 0 === $registration_id ) {
			return new \WP_Error(
				'no_registration',
				__( 'This payment is not linked to a registration, so no document can be issued.', 'conferencemanager' ),
				[ 'status' => 400 ]
			);
		}

		if ( self::TYPE_RECEIPT === $type ) {
			// ReceiptGenerator::create() と同じ条件。ここで弾くのは理由を伝えるため。
			if ( ! in_array( $payment['status'], self::RECEIPT_STATUSES, true ) ) {
				return new \WP_Error(
					'payment_not_completed',
					__( 'Receipts can only be issued for payments that have been confirmed.', 'conferencemanager' ),
					[ 'status' => 400 ]
				);
			}
			TransactionalMailer::instance()->auto_generate_receipt( (int) $payment['id'], $registration_id );
			return true;
		}

		/*
		 * 🔴 請求書にはここに条件が無く、**どの状態でも採番まで通っていた**。
		 *
		 * 申込から辿る `resolve_payment_id()` には同じ絞り込みがあったので
		 * 「サーバは守っている」と読めてしまうが、決済管理のボタンが叩くのは
		 * `issue_for_payment()` ＝**こちらの経路**で、そこは素通りだった。
		 * 実測: status を failed にした決済に INV-202608-00001 が発行された。
		 *
		 * ⚠️ `auto_generate_invoice()` は状態を一切見ない（見るのは申込と会員の
		 * 存在だけ）ので、呼ぶ前に弾く必要がある。
		 */
		if ( ! in_array( $payment['status'], self::BILLABLE_STATUSES, true ) ) {
			return new \WP_Error(
				'no_billable_payment',
				__( 'An invoice cannot be issued for this payment.', 'conferencemanager' ),
				[ 'status' => 400 ]
			);
		}

		TransactionalMailer::instance()->auto_generate_invoice( $registration_id, (int) $payment['id'] );
		return true;
	}

	/**
	 * 帳票を PDF に描画して base64 で返す
	 *
	 * @param int    $document_id 帳票ID
	 * @param string $type        帳票種別
	 * @return array{filename:string,pdf_b64:string,document_no:string}|\WP_Error
	 */
	private function render( int $document_id, string $type ): array|\WP_Error {
		if ( self::TYPE_RECEIPT === $type ) {
			$generator = ReceiptGenerator::instance();
			$row       = $generator->get( $document_id );
			$no        = (string) ( $row['receipt_no'] ?? '' );
		} else {
			$generator = InvoiceGenerator::instance();
			$row       = $generator->get( $document_id );
			$no        = (string) ( $row['invoice_no'] ?? '' );
		}

		$binary = $generator->render_binary( $document_id );
		if ( is_wp_error( $binary ) ) {
			$binary->add_data( [ 'status' => 400 ] );
			return $binary;
		}

		return [
			'filename'    => $generator->filename( $document_id ),
			'pdf_b64'     => base64_encode( $binary ),
			'document_no' => $no,
		];
	}
}
