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

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


use ConfManager\Traits\Singleton;
use ConfManager\Traits\HasHooks;
use ConfManager\Support\LocalTime;
use ConfManager\Registration\RegistrationManager;

/**
 * 決済 REST API
 *
 * @since 1.0.0
 */
class PaymentApi {

	use Singleton;
	use HasHooks;

	private const NAMESPACE = 'conf-manager/v1';

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

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

	/**
	 * WP_Error に HTTP ステータスを足す（元の data を保ったまま）
	 *
	 * 🔴 **素で `add_data( [ 'status' => 400 ] )` と書かないこと。**
	 * add_data() は同じコードの data を**置き換える**ので、ハンドラが入れた
	 * 理由（`reason` 等）が黙って消える。エラーコードは残るため、
	 * **「理由まで見ているつもりで見られていない」**状態になる。
	 *
	 * このファイルの add_data は**すべてこのヘルパを通す**（静的検査 M-17 が見張る）。
	 * ⚠️ 別のファイルを同じ形に直したら、M-17 の `M17_CONVERTED` に足すこと。
	 *
	 * @since 1.6.5
	 * @param \WP_Error $error  エラー
	 * @param int       $status HTTP ステータス
	 * @return \WP_Error
	 */
	private static function with_status( \WP_Error $error, int $status ): \WP_Error {
		$data = $error->get_error_data();
		$error->add_data( array_merge( is_array( $data ) ? $data : [], [ 'status' => $status ] ) );
		return $error;
	}

	public function register_routes(): void {
		register_rest_route( self::NAMESPACE, '/payments', [
			[
				'methods'             => \WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_items' ],
				'permission_callback' => fn() => current_user_can( 'conf_manager_view_payments' ),
			],
		] );

		register_rest_route( self::NAMESPACE, '/payments/(?P<id>[\d]+)/confirm', [
			[
				'methods'             => \WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'confirm_payment' ],
				'permission_callback' => fn() => current_user_can( 'conf_manager_edit_payments' ),
			],
		] );

		// 帳票の発行（PDF を base64 で返す）
		//
		// 🔴 メソッドは POST。帳票行が無ければ**発番して作る**＝状態を変える入口なので
		//    GET に置かない。cap も view ではなく edit。
		register_rest_route( self::NAMESPACE, '/payments/(?P<id>[\d]+)/invoice-pdf', [
			[
				'methods'             => \WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'issue_invoice_pdf' ],
				'permission_callback' => fn() => current_user_can( 'conf_manager_edit_payments' ),
			],
		] );

		register_rest_route( self::NAMESPACE, '/payments/(?P<id>[\d]+)/receipt-pdf', [
			[
				'methods'             => \WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'issue_receipt_pdf' ],
				'permission_callback' => fn() => current_user_can( 'conf_manager_edit_payments' ),
			],
		] );

		register_rest_route( self::NAMESPACE, '/payments/(?P<id>[\d]+)/refund', [
			[
				'methods'             => \WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'refund_payment' ],
				'permission_callback' => fn() => current_user_can( 'conf_manager_edit_payments' ),
			],
		] );

		// 入金額の訂正（記録の修正・金銭の授受なし）
		register_rest_route( self::NAMESPACE, '/payments/(?P<id>[\d]+)/paid-amount', [
			[
				'methods'             => \WP_REST_Server::EDITABLE,
				'callback'            => [ $this, 'update_paid_amount' ],
				'permission_callback' => fn() => current_user_can( 'conf_manager_edit_payments' ),
			],
		] );

		// 精算明細（返金・追加徴収）
		register_rest_route( self::NAMESPACE, '/payments/(?P<id>[\d]+)/adjustments', [
			[
				'methods'             => \WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_adjustments' ],
				'permission_callback' => fn() => current_user_can( 'conf_manager_view_payments' ),
			],
			[
				'methods'             => \WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'create_adjustment' ],
				'permission_callback' => fn() => current_user_can( 'conf_manager_edit_payments' ),
			],
		] );

		// 精算明細の取り消し（誤記入の訂正）
		//
		// 🔴 DELETE を置くのは「起きていない取引が帳簿に載った」ときのため。
		//    実際に返金した記録を取りやめる用途ではない（可否は delete_blocker が決める）。
		register_rest_route( self::NAMESPACE, '/payments/(?P<id>[\d]+)/adjustments/(?P<adjustment_id>[\d]+)', [
			[
				'methods'             => \WP_REST_Server::DELETABLE,
				'callback'            => [ $this, 'delete_adjustment' ],
				'permission_callback' => fn() => current_user_can( 'conf_manager_edit_payments' ),
			],
		] );

		register_rest_route( self::NAMESPACE, '/payments/summary/(?P<event_id>[\d]+)', [
			[
				'methods'             => \WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_summary' ],
				'permission_callback' => fn() => current_user_can( 'conf_manager_view_payments' ),
			],
		] );

		register_rest_route( self::NAMESPACE, '/payments/export/(?P<event_id>[\d]+)', [
			[
				'methods'             => \WP_REST_Server::READABLE,
				'callback'            => [ $this, 'export_csv' ],
				'permission_callback' => fn() => current_user_can( 'conf_manager_export_payments' ),
			],
		] );

	}

	public function get_items( \WP_REST_Request $request ): \WP_REST_Response {
		$result = PaymentManager::instance()->search( [
			'event_id'        => $request->get_param( 'event_id' ),
			'registration_id' => $request->get_param( 'registration_id' ),
			'method'          => $request->get_param( 'method' ),
			'status'          => $request->get_param( 'status' ),
			'date_from'       => $request->get_param( 'date_from' ),
			'date_to'         => $request->get_param( 'date_to' ),
			'per_page'        => $request->get_param( 'per_page' ) ?? 20,
			'page'            => $request->get_param( 'page' ) ?? 1,
		] );

		// 各決済に会員名を一括付加（N+1回避）
		global $wpdb;
		$reg_table    = $wpdb->prefix . 'conf_registrations';
		$member_table = $wpdb->prefix . 'conf_members';

		$reg_ids = array_filter( array_map( fn( $i ) => (int) $i['registration_id'], $result['items'] ) );
		$member_map = [];
		$transfer_note_map = [];
		if ( ! empty( $reg_ids ) ) {
			$placeholders = implode( ',', array_fill( 0, count( $reg_ids ), '%d' ) );
			// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names from plugin-defined prefix; IN() placeholders built from %d count and bound via spread.
			$rows = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT r.id AS registration_id, r.transfer_note, m.name_sei, m.name_mei FROM {$member_table} m
					 INNER JOIN {$reg_table} r ON r.member_id = m.id
					 WHERE r.id IN ({$placeholders})",
					...$reg_ids
				),
				ARRAY_A
			);
			// phpcs:enable
			foreach ( $rows as $row ) {
				$member_map[ (int) $row['registration_id'] ]        = $row['name_sei'] . ' ' . $row['name_mei'];
				$transfer_note_map[ (int) $row['registration_id'] ] = $row['transfer_note'] ?? '';
			}
		}
		foreach ( $result['items'] as &$item ) {
			$item['member_name']   = $member_map[ (int) $item['registration_id'] ] ?? '';
			$item['transfer_note'] = $transfer_note_map[ (int) $item['registration_id'] ] ?? '';
		}
		unset( $item );

		// 精算情報（返金／追加徴収の合計・純入金額・精算状態）を付与
		$result['items'] = PaymentManager::attach_settlement( $result['items'] );

		// 日時は GMT で保存しているので、画面に出す用の「このサイトの時間」を別キーで添える。
		// ⚠️ paid_at / created_at そのものは GMT のまま返す（絞り込みや他の計算が GMT 前提のため）。
		foreach ( $result['items'] as &$row ) {
			$row['paid_at_local']    = LocalTime::from_gmt( $row['paid_at'] ?? null );
			$row['created_at_local'] = LocalTime::from_gmt( $row['created_at'] ?? null );
		}
		unset( $row );

		$response = new \WP_REST_Response( $result['items'], 200 );
		$response->header( 'X-WP-Total', (string) $result['total'] );

		return $response;
	}

	public function confirm_payment( \WP_REST_Request $request ): \WP_REST_Response|\WP_Error {
		$id   = (int) $request['id'];
		$name = sanitize_text_field( $request->get_param( 'bank_transfer_name' ) ?? '' );
		$date = sanitize_text_field( $request->get_param( 'bank_transfer_date' ) ?? '' );

		// 未指定（null / 空文字）の場合は請求額どおりの入金として扱うため null を渡す。
		$paid_raw    = $request->get_param( 'paid_amount' );
		$paid_amount = ( null === $paid_raw || '' === $paid_raw ) ? null : (int) $paid_raw;

		$note = sanitize_textarea_field( $request->get_param( 'confirm_note' ) ?? '' );
		// 入金日（現地日付 Y-m-d）。振込日（bank_transfer_date）とは別物。
		$paid_at = sanitize_text_field( $request->get_param( 'paid_at' ) ?? '' );

		$result = PaymentManager::instance()->confirm_payment( $id, $name, $date, $paid_amount, $note, $paid_at );

		if ( is_wp_error( $result ) ) {
			return self::with_status( $result, 400 );
		}

		return new \WP_REST_Response( PaymentManager::instance()->get( $id ), 200 );
	}

	/**
	 * PUT /payments/{id}/paid-amount — 記録済み入金額の訂正
	 */
	public function update_paid_amount( \WP_REST_Request $request ): \WP_REST_Response|\WP_Error {
		$id = (int) $request['id'];

		$raw = $request->get_param( 'paid_amount' );
		if ( null === $raw || '' === $raw ) {
			return new \WP_Error( 'missing_param', __( 'The paid amount is required.', 'conferencemanager' ), [ 'status' => 400 ] );
		}

		$result = PaymentManager::instance()->update_paid_amount(
			$id,
			(int) $raw,
			sanitize_textarea_field( $request->get_param( 'reason' ) ?? '' )
		);

		if ( is_wp_error( $result ) ) {
			return self::with_status( $result, 400 );
		}

		$payment = PaymentManager::instance()->get( $id );

		return new \WP_REST_Response( [
			'payment'    => $payment,
			'settlement' => PaymentManager::resolve_settlement( $payment ),
		], 200 );
	}

	/**
	 * GET /payments/{id}/adjustments — 精算明細の一覧
	 */
	public function get_adjustments( \WP_REST_Request $request ): \WP_REST_Response|\WP_Error {
		$id      = (int) $request['id'];
		$payment = PaymentManager::instance()->get( $id );
		if ( null === $payment ) {
			return new \WP_Error( 'not_found', __( 'Payment record not found.', 'conferencemanager' ), [ 'status' => 404 ] );
		}

		return new \WP_REST_Response( [
			'items'      => PaymentAdjustmentManager::instance()->get_by_payment( $id ),
			'settlement' => PaymentManager::resolve_settlement( $payment ),
		], 200 );
	}

	/**
	 * POST /payments/{id}/adjustments — 返金・追加徴収の記録
	 */
	public function create_adjustment( \WP_REST_Request $request ): \WP_REST_Response|\WP_Error {
		$id     = (int) $request['id'];
		$type   = sanitize_text_field( $request->get_param( 'type' ) ?? '' );
		$amount = (int) $request->get_param( 'amount' );
		$method = sanitize_text_field( $request->get_param( 'method' ) ?? 'cash' );
		$reason = sanitize_textarea_field( $request->get_param( 'reason' ) ?? '' );
		$when   = sanitize_text_field( $request->get_param( 'occurred_at' ) ?? '' );

		// 参加取消を伴う全額返金は、決済ステータスの遷移と申込キャンセルを伴うため
		// 専用経路（refund）へ委譲する。
		$cancel = rest_sanitize_boolean( $request->get_param( 'cancel_registration' ) ?? false );

		if ( $cancel && PaymentAdjustmentManager::TYPE_REFUND === $type ) {
			$payment = PaymentManager::instance()->get( $id );
			if ( null === $payment ) {
				return new \WP_Error( 'not_found', __( 'Payment record not found.', 'conferencemanager' ), [ 'status' => 404 ] );
			}

			// ⚠️ v1.4.0 でカード決済（Stripe）は Pro 版の機能になった。過去の
			// method='stripe' の決済も、返金は**記録のみ**（カード会社への実返金は行わない）。
			$result = PaymentManager::instance()->refund( $id, $amount, $reason, $method, $when );
		} else {
			$result = PaymentAdjustmentManager::instance()->create( [
				'payment_id'  => $id,
				'type'        => $type,
				'amount'      => $amount,
				'method'      => $method,
				'occurred_at' => $when,
				'reason'      => $reason,
			] );
		}

		if ( is_wp_error( $result ) ) {
			return self::with_status( $result, 400 );
		}

		$payment = PaymentManager::instance()->get( $id );

		return new \WP_REST_Response( [
			'id'         => is_int( $result ) ? $result : 0,
			'cancelled'  => (bool) $cancel,
			'items'      => PaymentAdjustmentManager::instance()->get_by_payment( $id ),
			'settlement' => PaymentManager::resolve_settlement( $payment ),
		], 201 );
	}

	/**
	 * DELETE /payments/{id}/adjustments/{adjustment_id} — 誤って記録した精算の取り消し
	 *
	 * @since 1.6.5
	 */
	public function delete_adjustment( \WP_REST_Request $request ): \WP_REST_Response|\WP_Error {
		$id            = (int) $request['id'];
		$adjustment_id = (int) $request['adjustment_id'];

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

		$adjustment = PaymentAdjustmentManager::instance()->get( $adjustment_id );
		// ⚠️ 明細が**この決済のもの**であることまで見る。ID だけで消せると、
		//    URL を書き換えて別の決済の明細を消せてしまう。
		if ( null === $adjustment || (int) $adjustment['payment_id'] !== $id ) {
			return new \WP_Error( 'not_found', __( 'Settlement record not found.', 'conferencemanager' ), [ 'status' => 404 ] );
		}

		$result = PaymentAdjustmentManager::instance()->delete( $adjustment_id );

		if ( is_wp_error( $result ) ) {
			// 断った理由（reason）を保ったままステータスを足す（→ with_status）。
			return self::with_status( $result, 400 );
		}

		$payment = PaymentManager::instance()->get( $id );

		return new \WP_REST_Response( [
			'deleted'    => true,
			'items'      => PaymentAdjustmentManager::instance()->get_by_payment( $id ),
			'settlement' => PaymentManager::resolve_settlement( $payment ),
		], 200 );
	}

	/**
	 * 返金処理
	 */
	public function refund_payment( \WP_REST_Request $request ): \WP_REST_Response|\WP_Error {
		$id     = (int) $request['id'];
		$amount = absint( $request->get_param( 'amount' ) ?? 0 );
		$reason = sanitize_text_field( $request->get_param( 'reason' ) ?? '' );

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

		// ⚠️ v1.4.0 でカード決済（Stripe）は Pro 版の機能になった。
		// 過去の method='stripe' の決済も含め、返金は**記録のみ**。
		$result = PaymentManager::instance()->refund( $id, $amount, $reason );

		if ( is_wp_error( $result ) ) {
			return self::with_status( $result, 400 );
		}

		return new \WP_REST_Response( [
			'success' => true,
			'payment' => PaymentManager::instance()->get( $id ),
			// ⚠️ 実際に通った経路だけを言う。カード会社への返金は行っていない。
			'message' => __( 'Refund record saved.', 'conferencemanager' ),
		], 200 );
	}

	public function get_summary( \WP_REST_Request $request ): \WP_REST_Response {
		$event_id = (int) $request['event_id'];

		return new \WP_REST_Response( [
			'total'     => PaymentManager::instance()->get_event_summary( $event_id ),
			'by_method' => PaymentManager::instance()->get_summary_by_method( $event_id ),
		], 200 );
	}

	/**
	 * CSV出力（直接出力方式）
	 */
	public function export_csv( \WP_REST_Request $request ): void {
		$event_id = (int) $request['event_id'];
		$data     = PaymentManager::instance()->get_export_data( $event_id );

		$filename = 'payments-event-' . $event_id . '-' . gmdate( 'Ymd' ) . '.csv';

		header( 'Content-Type: text/csv; charset=UTF-8' );
		header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
		header( 'Cache-Control: no-cache, no-store, must-revalidate' );

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Writing to php://output stream for CSV download; WP_Filesystem does not support stream wrappers.
		$output = fopen( 'php://output', 'w' );
		// BOMでExcel UTF-8対応
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Writing to php://output stream (matches fopen above).
		fwrite( $output, "\xEF\xBB\xBF" );

		// 日本語ヘッダー
		// 🔴 ここに日本語を直接書かないこと。Free は**英語ソース**なので、
		// ベタ書きすると日本語以外のサイトでも日本語の見出しが出る（実際そうなっていた。
		// 値の側だけ直して見出しを見落としていたのを Tier 2 の T2-8 が捕まえた）。
		// 📌 msgid はすべて既存のものを使い回している（新しい msgid を増やさない）。
		$header_labels = [
			'registration_no'    => __( 'Reg. No.', 'conferencemanager' ),
			'name_sei'           => __( 'Last Name', 'conferencemanager' ),
			'name_mei'           => __( 'First Name', 'conferencemanager' ),
			'member_type'        => __( 'Member Type', 'conferencemanager' ),
			'participation_type' => __( 'Participation Type', 'conferencemanager' ),
			'method'             => __( 'Payment Method', 'conferencemanager' ),
			'amount'             => __( 'Amount (excl. tax)', 'conferencemanager' ),
			'tax'                => __( 'Tax', 'conferencemanager' ),
			'fee'                => __( 'Fees', 'conferencemanager' ),
			'paid_amount'        => __( 'Paid amount', 'conferencemanager' ),
			'refund_total'       => __( 'Refund Amount', 'conferencemanager' ),
			'surcharge_total'    => __( 'Surcharge', 'conferencemanager' ),
			'net_paid'           => __( 'Net paid', 'conferencemanager' ),
			'paid_diff'          => __( 'Difference', 'conferencemanager' ),
			'payment_status'     => __( 'Status', 'conferencemanager' ),
			'bank_transfer_name' => __( 'Transfer Name', 'conferencemanager' ),
			'confirm_note'       => __( 'Handover note', 'conferencemanager' ),
			'paid_at'            => __( 'Payment Date', 'conferencemanager' ),
		];
		fputcsv( $output, array_map( fn( $key ) => $header_labels[ $key ] ?? $key, $data['header'] ), ',', '"', '' );

		// キーを表示名に置き換える。
		//
		// 🔴 ここに日本語を直接書かないこと。Free は**英語ソース**なので、
		// ベタ書きすると日本語以外のサイトでも日本語の CSV が出る（実際そうなっていた）。
		// msgid はすべて既存のものを使い回している（新しい msgid を増やさない）。
		$status_labels = [
			'pending'        => __( 'Unpaid', 'conferencemanager' ),
			'completed'      => __( 'Paid', 'conferencemanager' ),
			'refunded'       => __( 'Refunded', 'conferencemanager' ),
			'partial_refund' => __( 'Partial Refund', 'conferencemanager' ),
			'failed'         => __( 'Failed', 'conferencemanager' ),
			// ⚠️ ここを忘れると **CSV にだけ生の `cancelled` が出る**。
			'cancelled'      => __( 'Withdrawn', 'conferencemanager' ),
		];
		$method_labels = [
			'bank'   => __( 'Bank Transfer', 'conferencemanager' ),
			'stripe' => 'Stripe',
		];
		// 🔴 会員種別の表示名を 2 箇所目に書かない。
		// ここには生の日本語配列（'regular' => '正会員'）があり、
		// 同じ人の同じ区分が経路によって別表記になりうる状態だった。
		// 正典は RegistrationManager::base_labels()。静的検査 labels-2 が再発を止める。
		$type_labels = RegistrationManager::base_labels();
		$participation_labels = [
			'onsite' => __( 'On-site', 'conferencemanager' ),
			'online' => __( 'Online', 'conferencemanager' ),
		];

		foreach ( $data['rows'] as $row ) {
			$row['payment_status']     = $status_labels[ $row['payment_status'] ] ?? $row['payment_status'];
			$row['method']             = $method_labels[ $row['method'] ] ?? $row['method'];
			$row['member_type']        = $type_labels[ $row['member_type'] ] ?? $row['member_type'];
			$row['participation_type'] = $participation_labels[ $row['participation_type'] ] ?? $row['participation_type'];
			fputcsv( $output, array_map( [ $this, 'sanitize_csv_value' ], $row ), ',', '"', '' );
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closes php://output stream (matches fopen above).
		fclose( $output );
		exit;
	}

	/**
	 * CSVセル値のインジェクション対策
	 *
	 * =, +, -, @ で始まる文字列はExcelで数式として解釈されるため、
	 * シングルクォートをプレフィックスして無害化する。
	 *
	 * @param string|int|float|null $value セル値
	 * @return string|int|float|null サニタイズ済みの値
	 */
	private function sanitize_csv_value( string|int|float|null $value ): string|int|float|null {
		if ( ! is_string( $value ) || '' === $value ) {
			return $value;
		}

		// 数値のみの文字列（差額のマイナス値など）は数式になり得ないため、
		// Excel で数値として扱えるようクォートしない。
		if ( is_numeric( $value ) ) {
			return $value;
		}

		$first = $value[0];
		if ( in_array( $first, [ '=', '+', '-', '@', "\t", "\r", "\n" ], true ) ) {
			return "'" . $value;
		}

		return $value;
	}


	/**
	 * POST /payments/{id}/invoice-pdf — 請求書を発行して PDF を返す
	 */
	public function issue_invoice_pdf( \WP_REST_Request $request ): \WP_REST_Response|\WP_Error {
		return $this->issue_document( (int) $request['id'], PaymentDocuments::TYPE_INVOICE );
	}

	/**
	 * POST /payments/{id}/receipt-pdf — 領収書を発行して PDF を返す
	 */
	public function issue_receipt_pdf( \WP_REST_Request $request ): \WP_REST_Response|\WP_Error {
		return $this->issue_document( (int) $request['id'], PaymentDocuments::TYPE_RECEIPT );
	}

	/**
	 * 帳票の発行を 1 箇所に寄せる
	 *
	 * @param int    $payment_id 決済ID
	 * @param string $type       帳票種別
	 */
	private function issue_document( int $payment_id, string $type ): \WP_REST_Response|\WP_Error {
		$result = PaymentDocuments::instance()->issue_for_payment( $payment_id, $type );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		return new \WP_REST_Response( $result, 200 );
	}
}
