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

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


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

/**
 * 決済管理（抽象レイヤー）
 *
 * 決済記録のCRUD・ステータス管理・入金消込を行う。
 * 銀行振込・Stripe等の決済手段はサブクラスで実装。
 *
 * @since 1.0.0
 */
class PaymentManager {

	use Singleton;

	private const TABLE = 'conf_payments';

	/**
	 * 有効なステータス
	 */
	private const VALID_STATUSES = [ 'pending', 'completed', 'refunded', 'partial_refund', 'failed' ];

	/**
	 * 有効な決済手段
	 */
	private const VALID_METHODS = [ 'bank', 'stripe' ];

	/**
	 * 初期化
	 */
	protected function init(): void {}

	/**
	 * テーブル名
	 */
	private function table(): string {
		global $wpdb;
		return $wpdb->prefix . self::TABLE;
	}

	/**
	 * 決済記録を作成する
	 *
	 * @param array $data {
	 *     @type int    $registration_id    登録ID（必須）
	 *     @type string $method             決済手段 bank|stripe（必須）
	 *     @type int    $amount             決済金額（必須）
	 *     @type int    $tax                消費税額（デフォルト: 0）
	 *     @type int    $fee                手数料（デフォルト: 0）
	 *     @type bool   $is_group_payment   団体一括決済フラグ
	 *     @type string $bank_transfer_name 振込名義
	 *     @type string $gateway_txn_id     外部決済トランザクションID
	 * }
	 * @return int|\WP_Error
	 */
	public function create( array $data ): int|\WP_Error {
		global $wpdb;

		$error = $this->validate( $data );
		if ( is_wp_error( $error ) ) {
			return $error;
		}

		$insert_data = [
			'registration_id'  => absint( $data['registration_id'] ),
			'method'           => sanitize_text_field( $data['method'] ),
			'amount'           => absint( $data['amount'] ),
			'tax'              => absint( $data['tax'] ?? 0 ),
			'fee'              => absint( $data['fee'] ?? 0 ),
			'is_group_payment' => ! empty( $data['is_group_payment'] ) ? 1 : 0,
			'status'           => 'pending',
		];

		if ( ! empty( $data['bank_transfer_name'] ) ) {
			$insert_data['bank_transfer_name'] = sanitize_text_field( $data['bank_transfer_name'] );
		}

		if ( ! empty( $data['gateway_txn_id'] ) ) {
			$insert_data['gateway_txn_id'] = sanitize_text_field( $data['gateway_txn_id'] );
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table query.
		$result = $wpdb->insert( $this->table(), $insert_data );

		if ( false === $result ) {
			return new \WP_Error(
				'db_insert_error',
				__( 'Failed to create payment record.', 'conferencemanager' ),
				$wpdb->last_error
			);
		}

		$payment_id = (int) $wpdb->insert_id;

		AuditLog::instance()->log( 'create_payment', 'payment', $payment_id, [
			'registration_id' => $data['registration_id'],
			'method'          => $data['method'],
			'amount'          => $data['amount'],
		] );

		return $payment_id;
	}

	/**
	 * 決済記録を取得する
	 *
	 * @param int $id 決済ID
	 * @return array|null
	 */
	public function get( int $id ): ?array {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
		$row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$this->table()} WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from safe internal method.
				$id
			),
			ARRAY_A
		);

		return $row ?: null;
	}

	/**
	 * 登録IDで決済記録を取得する
	 *
	 * @param int $registration_id 登録ID
	 * @return array 決済記録一覧
	 */
	public function get_by_registration( int $registration_id ): array {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$this->table()} WHERE registration_id = %d ORDER BY created_at DESC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from safe internal method.
				$registration_id
			),
			ARRAY_A
		);

		return $rows ?: [];
	}

	/**
	 * 未入金（pending）の決済金額を再計算結果に同期する
	 *
	 * 参加区分の管理者修正により申込金額が変わった際、未消込の決済レコードのみ
	 * amount/tax を更新する。completed/refunded 等は触れない（入金済は別運用）。
	 *
	 * @param int $registration_id 登録ID
	 * @param int $amount          新しい税抜金額
	 * @param int $tax             新しい消費税額
	 * @return int 更新した決済レコード件数
	 */
	public function sync_pending_amount( int $registration_id, int $amount, int $tax ): int {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; request-local write, no caching needed.
		$updated = $wpdb->update(
			$this->table(),
			[
				'amount' => $amount,
				'tax'    => $tax,
			],
			[
				'registration_id' => $registration_id,
				'status'          => 'pending',
			]
		);

		return false === $updated ? 0 : (int) $updated;
	}

	/**
	 * 入金を確認する（消込）
	 *
	 * @param int    $id                 決済ID
	 * @param string $bank_transfer_name 振込名義（銀行振込の場合）
	 * @param string $bank_transfer_date 振込日（Y-m-d）
	 * @return true|\WP_Error
	 */
	public function confirm_payment( int $id, string $bank_transfer_name = '', string $bank_transfer_date = '' ): true|\WP_Error {
		global $wpdb;

		$payment = $this->get( $id );
		if ( null === $payment ) {
			return new \WP_Error( 'not_found', __( 'Payment record not found.', 'conferencemanager' ) );
		}

		if ( 'pending' !== $payment['status'] ) {
			return new \WP_Error( 'invalid_status', __( 'This payment has already been reconciled or is invalid.', 'conferencemanager' ) );
		}

		$update_data = [
			'status'  => 'completed',
			'paid_at' => current_time( 'mysql', true ),
		];

		if ( '' !== $bank_transfer_name ) {
			$update_data['bank_transfer_name'] = sanitize_text_field( $bank_transfer_name );
		}

		if ( '' !== $bank_transfer_date ) {
			$update_data['bank_transfer_date'] = sanitize_text_field( $bank_transfer_date );
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; request-local write, no caching needed.
		$result = $wpdb->update( $this->table(), $update_data, [ 'id' => $id ] );

		if ( false === $result ) {
			return new \WP_Error(
				'db_update_error',
				__( 'Failed to update payment confirmation.', 'conferencemanager' ),
				$wpdb->last_error
			);
		}

		// 関連する申込のステータスも paid に更新
		$reg_mgr = \ConfManager\Registration\RegistrationManager::instance();
		$reg_mgr->update_status( (int) $payment['registration_id'], 'paid' );

		AuditLog::instance()->log( 'confirm_payment', 'payment', $id, [
			'registration_id'    => $payment['registration_id'],
			'amount'             => $payment['amount'],
			'bank_transfer_name' => $bank_transfer_name,
		] );

		/**
		 * 入金確認後に発火
		 *
		 * @since 1.2.0
		 * @param int $id              決済ID
		 * @param int $registration_id 申込ID
		 */
		do_action( 'conf_manager_payment_confirmed', $id, (int) $payment['registration_id'] );

		return true;
	}

	/**
	 * 返金処理
	 *
	 * @param int    $id     決済ID
	 * @param int    $amount 返金額（0=全額）
	 * @param string $reason 返金理由
	 * @return true|\WP_Error
	 */
	public function refund( int $id, int $amount = 0, string $reason = '' ): true|\WP_Error {
		global $wpdb;

		$payment = $this->get( $id );
		if ( null === $payment ) {
			return new \WP_Error( 'not_found', __( 'Payment record not found.', 'conferencemanager' ) );
		}

		if ( 'completed' !== $payment['status'] ) {
			return new \WP_Error( 'invalid_status', __( 'Only paid payments can be refunded.', 'conferencemanager' ) );
		}

		$payment_amount = (int) $payment['amount'];
		$refund_amount  = ( 0 === $amount || $amount >= $payment_amount ) ? $payment_amount : $amount;
		$new_status     = ( $refund_amount >= $payment_amount ) ? 'refunded' : 'partial_refund';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; request-local write, no caching needed.
		$result = $wpdb->update(
			$this->table(),
			[
				'status'        => $new_status,
				'refund_amount' => $refund_amount,
				'refund_reason' => sanitize_text_field( $reason ),
			],
			[ 'id' => $id ]
		);

		if ( false === $result ) {
			return new \WP_Error(
				'db_update_error',
				__( 'Refund processing failed.', 'conferencemanager' ),
				$wpdb->last_error
			);
		}

		// 全額返金の場合、申込もキャンセル
		if ( 'refunded' === $new_status ) {
			$reg_mgr = \ConfManager\Registration\RegistrationManager::instance();
			$reg_mgr->cancel( (int) $payment['registration_id'] );
		}

		AuditLog::instance()->log( 'refund_payment', 'payment', $id, [
			'refund_amount' => $refund_amount,
			'new_status'    => $new_status,
			'reason'        => $reason,
		] );

		return true;
	}

	/**
	 * 決済を検索する
	 *
	 * @param array $args {
	 *     @type int    $registration_id 登録IDで絞り込み
	 *     @type string $method          決済手段で絞り込み
	 *     @type string $status          ステータスで絞り込み
	 *     @type string $date_from       入金日の開始（Y-m-d）
	 *     @type string $date_to         入金日の終了（Y-m-d）
	 *     @type int    $per_page        件数
	 *     @type int    $page            ページ番号
	 * }
	 * @return array{items: array, total: int, page: int, per_page: int}
	 */
	public function search( array $args = [] ): array {
		global $wpdb;

		$table    = $this->table();
		$where    = [];
		$values   = [];
		$join_sql = '';
		$per_page = min( 100, max( 1, absint( $args['per_page'] ?? 20 ) ) );
		$page     = max( 1, absint( $args['page'] ?? 1 ) );

		// イベントIDフィルタ（registrations JOIN）
		if ( ! empty( $args['event_id'] ) ) {
			$reg_table = $wpdb->prefix . 'conf_registrations';
			$join_sql  = "INNER JOIN {$reg_table} r ON r.id = p.registration_id";
			$where[]   = 'r.event_id = %d';
			$values[]  = absint( $args['event_id'] );
		}

		if ( ! empty( $args['registration_id'] ) ) {
			$where[]  = 'p.registration_id = %d';
			$values[] = absint( $args['registration_id'] );
		}

		if ( ! empty( $args['method'] ) && in_array( $args['method'], self::VALID_METHODS, true ) ) {
			$where[]  = 'p.method = %s';
			$values[] = $args['method'];
		}

		if ( ! empty( $args['status'] ) && in_array( $args['status'], self::VALID_STATUSES, true ) ) {
			$where[]  = 'p.status = %s';
			$values[] = $args['status'];
		}

		if ( ! empty( $args['date_from'] ) ) {
			$where[]  = 'p.paid_at >= %s';
			$values[] = sanitize_text_field( $args['date_from'] ) . ' 00:00:00';
		}

		if ( ! empty( $args['date_to'] ) ) {
			$where[]  = 'p.paid_at <= %s';
			$values[] = sanitize_text_field( $args['date_to'] ) . ' 23:59:59';
		}

		$where_sql = ! empty( $where ) ? 'WHERE ' . implode( ' AND ', $where ) : '';

		$count_sql = "SELECT COUNT(*) FROM {$table} p {$join_sql} {$where_sql}";
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $where_sql uses %s placeholders; when $values is empty, no user input in query.
		$total     = ! empty( $values )
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Dynamic query built with safe values
			? (int) $wpdb->get_var( $wpdb->prepare( $count_sql, ...$values ) )
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Dynamic query built with safe values
			: (int) $wpdb->get_var( $count_sql );

		$offset       = ( $page - 1 ) * $per_page;
		$query_values = array_merge( $values, [ $per_page, $offset ] );
		$data_sql     = "SELECT p.* FROM {$table} p {$join_sql} {$where_sql} ORDER BY p.created_at DESC LIMIT %d OFFSET %d";
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Dynamic query built with safe values
		$rows         = $wpdb->get_results(
			$wpdb->prepare( $data_sql, ...$query_values ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			ARRAY_A
		);

		return [
			'items'    => $rows ?: [],
			'total'    => $total,
			'page'     => $page,
			'per_page' => $per_page,
		];
	}

	/**
	 * イベントの売上集計を取得
	 *
	 * @param int $event_id イベントID
	 * @return array{total_amount: int, total_tax: int, total_fee: int, count: int}
	 */
	public function get_event_summary( int $event_id ): array {
		global $wpdb;

		$reg_table = $wpdb->prefix . 'conf_registrations';
		$pay_table = $this->table();

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names from plugin-defined prefix; values bound via prepare().
		$row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT
					COALESCE(SUM(p.amount), 0) AS total_amount,
					COALESCE(SUM(p.tax), 0) AS total_tax,
					COALESCE(SUM(p.fee), 0) AS total_fee,
					COUNT(p.id) AS count
				FROM {$pay_table} p
				INNER JOIN {$reg_table} r ON p.registration_id = r.id
				WHERE r.event_id = %d AND p.status = 'completed'",
				$event_id
			),
			ARRAY_A
		);

		// 未入金の集計も取得
		$pending_row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT
					COALESCE(SUM(p.amount), 0) AS total_amount,
					COALESCE(SUM(p.tax), 0) AS total_tax,
					COUNT(p.id) AS count
				FROM {$pay_table} p
				INNER JOIN {$reg_table} r ON p.registration_id = r.id
				WHERE r.event_id = %d AND p.status = 'pending'",
				$event_id
			),
			ARRAY_A
		);
		// phpcs:enable

		return [
			'completed_amount' => (int) ( $row['total_amount'] ?? 0 ),
			'completed_tax'    => (int) ( $row['total_tax'] ?? 0 ),
			'completed_fee'    => (int) ( $row['total_fee'] ?? 0 ),
			'completed_count'  => (int) ( $row['count'] ?? 0 ),
			'pending_amount'   => (int) ( $pending_row['total_amount'] ?? 0 ),
			'pending_tax'      => (int) ( $pending_row['total_tax'] ?? 0 ),
			'pending_count'    => (int) ( $pending_row['count'] ?? 0 ),
		];
	}

	/**
	 * バリデーション
	 */
	private function validate( array $data ): true|\WP_Error {
		if ( empty( $data['registration_id'] ) ) {
			return new \WP_Error( 'missing_registration_id', __( 'Registration ID is required.', 'conferencemanager' ) );
		}

		if ( empty( $data['method'] ) || ! in_array( $data['method'], self::VALID_METHODS, true ) ) {
			return new \WP_Error( 'invalid_method', __( 'Invalid payment method.', 'conferencemanager' ) );
		}

		if ( empty( $data['amount'] ) || (int) $data['amount'] <= 0 ) {
			return new \WP_Error( 'invalid_amount', __( 'Payment amount must be 1 or more.', 'conferencemanager' ) );
		}

		return true;
	}

	/**
	 * 決済手段別の売上集計を取得
	 *
	 * @param int $event_id イベントID
	 * @return array<string, array{amount: int, tax: int, fee: int, count: int}>
	 */
	public function get_summary_by_method( int $event_id ): array {
		global $wpdb;

		$reg_table = $wpdb->prefix . 'conf_registrations';
		$pay_table = $this->table();

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names from plugin-defined prefix; values bound via prepare().
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT
					p.method,
					COALESCE(SUM(p.amount), 0) AS total_amount,
					COALESCE(SUM(p.tax), 0) AS total_tax,
					COALESCE(SUM(p.fee), 0) AS total_fee,
					COUNT(p.id) AS count
				FROM {$pay_table} p
				INNER JOIN {$reg_table} r ON p.registration_id = r.id
				WHERE r.event_id = %d AND p.status = 'completed'
				GROUP BY p.method",
				$event_id
			),
			ARRAY_A
		);
		// phpcs:enable

		$summary = [];
		foreach ( $rows ?: [] as $row ) {
			$summary[ $row['method'] ] = [
				'amount' => (int) $row['total_amount'],
				'tax'    => (int) $row['total_tax'],
				'fee'    => (int) $row['total_fee'],
				'count'  => (int) $row['count'],
			];
		}

		return $summary;
	}

	/**
	 * 期間別の売上集計を取得（日別）
	 *
	 * @param int    $event_id  イベントID
	 * @param string $date_from 開始日（Y-m-d）
	 * @param string $date_to   終了日（Y-m-d）
	 * @return array<string, array{amount: int, tax: int, count: int}>
	 */
	public function get_daily_summary( int $event_id, string $date_from = '', string $date_to = '' ): array {
		global $wpdb;

		$reg_table = $wpdb->prefix . 'conf_registrations';
		$pay_table = $this->table();

		$where  = "r.event_id = %d AND p.status = 'completed' AND p.paid_at IS NOT NULL";
		$values = [ $event_id ];

		if ( '' !== $date_from ) {
			$where   .= ' AND p.paid_at >= %s';
			$values[] = sanitize_text_field( $date_from ) . ' 00:00:00';
		}

		if ( '' !== $date_to ) {
			$where   .= ' AND p.paid_at <= %s';
			$values[] = sanitize_text_field( $date_to ) . ' 23:59:59';
		}

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names from plugin-defined prefix; dynamic WHERE built from whitelisted placeholders; values bound via prepare() spread.
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT
					DATE(p.paid_at) AS pay_date,
					COALESCE(SUM(p.amount), 0) AS total_amount,
					COALESCE(SUM(p.tax), 0) AS total_tax,
					COUNT(p.id) AS count
				FROM {$pay_table} p
				INNER JOIN {$reg_table} r ON p.registration_id = r.id
				WHERE {$where}
				GROUP BY DATE(p.paid_at)
				ORDER BY pay_date ASC",
				...$values
			),
			ARRAY_A
		);
		// phpcs:enable

		$summary = [];
		foreach ( $rows ?: [] as $row ) {
			$summary[ $row['pay_date'] ] = [
				'amount' => (int) $row['total_amount'],
				'tax'    => (int) $row['total_tax'],
				'count'  => (int) $row['count'],
			];
		}

		return $summary;
	}

	/**
	 * 売上レポートのCSVエクスポート用データを取得
	 *
	 * @param int $event_id イベントID
	 * @return array{header: array, rows: array}
	 */
	public function get_export_data( int $event_id ): array {
		global $wpdb;

		$reg_table = $wpdb->prefix . 'conf_registrations';
		$pay_table = $this->table();
		$mem_table = $wpdb->prefix . 'conf_members';

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names from plugin-defined prefix; values bound via prepare().
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT
					r.registration_no,
					m.name_sei,
					m.name_mei,
					m.member_type,
					r.participation_type,
					p.method,
					p.amount,
					p.tax,
					p.fee,
					p.status AS payment_status,
					p.bank_transfer_name,
					p.paid_at
				FROM {$pay_table} p
				INNER JOIN {$reg_table} r ON p.registration_id = r.id
				INNER JOIN {$mem_table} m ON r.member_id = m.id
				WHERE r.event_id = %d
				ORDER BY p.created_at ASC",
				$event_id
			),
			ARRAY_A
		);
		// phpcs:enable

		$header = [
			'registration_no', 'name_sei', 'name_mei', 'member_type',
			'participation_type', 'method', 'amount', 'tax', 'fee',
			'payment_status', 'bank_transfer_name', 'paid_at',
		];

		return [
			'header' => $header,
			'rows'   => $rows ?: [],
		];
	}

	/**
	 * 有効なステータス一覧
	 */
	public static function get_valid_statuses(): array {
		return self::VALID_STATUSES;
	}
}
