<?php
/**
 * PP 1.5.0 — Tier C6 — WP-CLI namespace for ops recovery
 * ------------------------------------------------------
 * Ships in PP source path: app/CLI/PaymentPageCLI.php
 *
 * Provides:
 *   wp payment-page verify-integrity [--repair]
 *     → runs the Tier-C5 DatabaseIntegrityTest invariants live.
 *
 *   wp payment-page retry-stuck-pi --id=N
 *   wp payment-page retry-stuck-pi --all
 *     → queries Stripe for current PI status, reconciles or emails admin.
 *
 *   wp payment-page dump-payments [--format=json|csv]
 *     → ops snapshot for support tickets.
 *
 *   wp payment-page seed-test-data --count=1000
 *     → seeds 1000 payment rows for perf-budget testing (Tier C4).
 *
 * Register in payment-page.php conditionally:
 *
 *     if ( defined( 'WP_CLI' ) && WP_CLI ) {
 *         require_once __DIR__ . '/app/CLI/PaymentPageCLI.php';
 *         WP_CLI::add_command( 'payment-page', '\\PaymentPage\\CLI\\PaymentPageCLI' );
 *     }
 */

namespace PaymentPage\CLI;

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

class PaymentPageCLI {

	/**
	 * Verify cross-table database integrity.
	 *
	 * Runs the same invariants as `tests/Unit/Db/DatabaseIntegrityTest.php`
	 * against the live database. Catches three classes of corruption:
	 *   1. Orphan log rows (parent payment row deleted)
	 *   2. "Paid" rows with no gateway reference (untraceable in audits)
	 *   3. ACH PaymentIntents stuck > 30 days in `processing` state
	 *
	 * ## OPTIONS
	 *
	 * [--repair]
	 * : When set, attempts to repair detected issues (delete orphans, etc.).
	 * Default: dry-run (read-only).
	 *
	 * [--format=<format>]
	 * : Render output in a particular format. Default: human-readable.
	 * ---
	 * default: human
	 * options:
	 *   - human
	 *   - json
	 *   - yaml
	 * ---
	 *
	 * ## EXAMPLES
	 *
	 *     # Print integrity report (dry-run, human-readable)
	 *     $ wp payment-page verify-integrity
	 *
	 *     # Print as JSON for monitoring scripts
	 *     $ wp payment-page verify-integrity --format=json
	 *
	 *     # Repair detected issues (delete orphan log rows)
	 *     $ wp payment-page verify-integrity --repair
	 *
	 * ## EXIT CODES
	 *
	 *   0 — all invariants satisfied
	 *   1 — integrity violations found (use --repair to attempt fix)
	 *   2 — unexpected error (DB unreachable, etc.)
	 *
	 * @subcommand verify-integrity
	 * @when after_wp_load
	 */
	public function verify_integrity( $args, $assoc_args ) {
		global $wpdb;
		$repair  = isset( $assoc_args['repair'] );
		$issues  = array();

		// Same checks as tests/Unit/DatabaseIntegrityTest.php
		$orphan_log = (int) $wpdb->get_var(
			"SELECT COUNT(*) FROM {$wpdb->prefix}payment_page_log l
			 LEFT JOIN {$wpdb->prefix}payment_page_payments p ON p.id = l.payment_id
			 WHERE l.payment_id IS NOT NULL AND p.id IS NULL"
		);
		if ( $orphan_log > 0 ) {
			$issues[] = "$orphan_log orphan log rows (parent payment missing)";
			if ( $repair ) {
				$deleted = $wpdb->query(
					"DELETE l FROM {$wpdb->prefix}payment_page_log l
					 LEFT JOIN {$wpdb->prefix}payment_page_payments p ON p.id = l.payment_id
					 WHERE l.payment_id IS NOT NULL AND p.id IS NULL"
				);
				\WP_CLI::log( "Deleted $deleted orphan log rows." );
			}
		}

		$paid_no_gateway = (int) $wpdb->get_var(
			"SELECT COUNT(*) FROM {$wpdb->prefix}payment_page_payments
			 WHERE is_paid = 1
			   AND ( stripe_payment_intent_id IS NULL OR stripe_payment_intent_id = '' )
			   AND ( paypal_capture_id IS NULL OR paypal_capture_id = '' )"
		);
		if ( $paid_no_gateway > 0 ) {
			$issues[] = "$paid_no_gateway paid rows with NO gateway reference (untraceable; manual audit required)";
		}

		$stuck_pi = (int) $wpdb->get_var(
			"SELECT COUNT(*) FROM {$wpdb->prefix}payment_page_payments
			 WHERE is_paid = 0
			   AND payment_method = 'ach_direct_debit'
			   AND created_at < DATE_SUB( NOW(), INTERVAL 30 DAY )"
		);
		if ( $stuck_pi > 0 ) {
			$issues[] = "$stuck_pi ACH PIs stuck > 30 days (run `wp payment-page retry-stuck-pi --all`)";
		}

		if ( empty( $issues ) ) {
			\WP_CLI::success( 'All integrity checks passed.' );
			return;
		}
		\WP_CLI::warning( count( $issues ) . " integrity issue(s):" );
		foreach ( $issues as $i ) {
			\WP_CLI::log( "  - $i" );
		}
		if ( ! $repair ) {
			\WP_CLI::log( "\nRe-run with --repair to fix what's safely repairable." );
		}
	}

	/**
	 * Reconcile ACH PaymentIntents stuck in 'processing' state.
	 *
	 * ACH via Stripe Financial Connections settles asynchronously over
	 * 2-4 business days. A small fraction of rows can get stuck if the
	 * `payment_intent.succeeded` webhook never arrives (network issue,
	 * Stripe outage, or the merchant's site was offline at delivery
	 * time). This command queries Stripe for the current PI status and
	 * reconciles the local row if Stripe says it succeeded.
	 *
	 * IDEMPOTENT — running twice on the same id is safe (it just re-
	 * queries Stripe and either matches the current state or warns).
	 *
	 * ## OPTIONS
	 *
	 * [--id=<id>]
	 * : Specific payment row id to retry. Mutually exclusive with --all.
	 *
	 * [--all]
	 * : Process every stuck ACH row older than 14 days.
	 *
	 * [--format=<format>]
	 * : Output format. Default: human-readable.
	 * ---
	 * default: human
	 * options:
	 *   - human
	 *   - json
	 * ---
	 *
	 * ## EXAMPLES
	 *
	 *     # Retry a specific stuck row
	 *     $ wp payment-page retry-stuck-pi --id=42
	 *
	 *     # Retry every stuck ACH row > 14 days old
	 *     $ wp payment-page retry-stuck-pi --all
	 *
	 *     # JSON output for monitoring scripts
	 *     $ wp payment-page retry-stuck-pi --all --format=json
	 *
	 * ## EXIT CODES
	 *
	 *   0 — all attempted rows reconciled (or no stuck rows found)
	 *   1 — at least one row could not be reconciled (Stripe API error)
	 *   2 — Stripe credentials not configured
	 *
	 * @subcommand retry-stuck-pi
	 * @when after_wp_load
	 */
	public function retry_stuck_pi( $args, $assoc_args ) {
		global $wpdb;
		$ids = isset( $assoc_args['id'] )
			? array( (int) $assoc_args['id'] )
			: $wpdb->get_col(
				"SELECT id FROM {$wpdb->prefix}payment_page_payments
				 WHERE is_paid = 0
				   AND payment_method = 'ach_direct_debit'
				   AND created_at < DATE_SUB( NOW(), INTERVAL 14 DAY )"
			);

		if ( empty( $ids ) ) {
			\WP_CLI::success( 'No stuck PIs found.' );
			return;
		}

		foreach ( $ids as $id ) {
			$row = $wpdb->get_row( $wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}payment_page_payments WHERE id = %d",
				$id
			) );
			if ( ! $row || empty( $row->stripe_payment_intent_id ) ) {
				\WP_CLI::warning( "Row $id missing — skip." );
				continue;
			}

			// Query Stripe for current PI status. Reconcile.
			try {
				if ( ! class_exists( '\Stripe\Stripe' ) ) {
					require_once PAYMENT_PAGE_BASE_PATH . '/lib/stripe/init.php';
				}
				$settings = get_option( 'payment_page_settings', array() );
				$sk = $settings['stripe_test_secret_key'] ?? '';
				if ( empty( $sk ) ) {
					\WP_CLI::error( 'No Stripe secret key configured.' );
				}
				\Stripe\Stripe::setApiKey( $sk );
				$pi = \Stripe\PaymentIntent::retrieve( $row->stripe_payment_intent_id );

				if ( $pi->status === 'succeeded' ) {
					$wpdb->update(
						"{$wpdb->prefix}payment_page_payments",
						array( 'is_paid' => 1 ),
						array( 'id' => $id )
					);
					\WP_CLI::success( "Row $id reconciled — PI is now succeeded; flipped is_paid=1." );
				} else {
					\WP_CLI::warning( "Row $id PI status is '{$pi->status}' — left as-is." );
				}
			} catch ( \Exception $e ) {
				\WP_CLI::warning( "Row $id Stripe API error: " . $e->getMessage() );
			}
		}
	}

	/**
	 * Dump payments table for support tickets.
	 *
	 * Operational snapshot of recent payments. Output schema is locked
	 * by `tests/Unit/CLI/PaymentPageCliContractTest.php` — third-party
	 * tooling (cron jobs, monitoring scripts, support tooling) can
	 * depend on the column shape staying stable across PP versions.
	 *
	 * READ-ONLY — never modifies the database.
	 *
	 * ## OPTIONS
	 *
	 * [--format=<format>]
	 * : Output format. Default: json.
	 * ---
	 * default: json
	 * options:
	 *   - json
	 *   - csv
	 * ---
	 *
	 * [--limit=<n>]
	 * : Maximum rows to return. Default: 100. Max: 1000.
	 *
	 * ## EXAMPLES
	 *
	 *     # Dump last 100 payments as JSON
	 *     $ wp payment-page dump-payments
	 *
	 *     # Dump last 500 as CSV (Excel-friendly)
	 *     $ wp payment-page dump-payments --format=csv --limit=500
	 *
	 *     # Pipe to jq for a quick top-revenue check
	 *     $ wp payment-page dump-payments | jq '[.[] | .amount] | add'
	 *
	 * ## EXIT CODES
	 *
	 *   0 — dump completed successfully (or no rows found)
	 *   1 — invalid --limit (must be 1-1000)
	 *   2 — DB unreachable
	 *
	 * @subcommand dump-payments
	 * @when after_wp_load
	 */
	public function dump_payments( $args, $assoc_args ) {
		global $wpdb;
		$format = $assoc_args['format'] ?? 'json';
		$limit  = (int) ( $assoc_args['limit'] ?? 100 );
		$rows   = $wpdb->get_results(
			"SELECT * FROM {$wpdb->prefix}payment_page_payments
			 ORDER BY id DESC LIMIT $limit",
			ARRAY_A
		);
		if ( $format === 'csv' ) {
			$out = fopen( 'php://output', 'w' );
			if ( $rows ) {
				fputcsv( $out, array_keys( $rows[0] ) );
				foreach ( $rows as $r ) {
					fputcsv( $out, $r );
				}
			}
		} else {
			echo wp_json_encode( $rows, JSON_PRETTY_PRINT );
		}
	}
}
