<?php

if ( !defined( 'ABSPATH' ) ) {
    exit;
}
class BSPP_Stripe_Checkout {
    private $settings;

    public function __construct() {
        $this->settings = new BSPP_Settings_Repository();
    }

    public function create_subscription_checkout_session(
        $user_id,
        $user_email,
        $purchase_id,
        $amount_dollars,
        $interval,
        $post_id,
        $plan = 'monthly'
    ) {
        $stripe_api_key = $this->settings->get( 'stripe_api_key', '' );
        if ( empty( $stripe_api_key ) ) {
            return new WP_Error('missing_stripe_api_key', 'Missing stripe_api_key in settings.');
        }
        $amount_cents = (int) round( (float) $amount_dollars * 100 );
        $interval = sanitize_key( $interval );
        if ( $amount_cents <= 0 || empty( $interval ) ) {
            return new WP_Error('invalid_amount', 'Invalid subscription amount.');
        }
        $post_id = (int) $post_id;
        if ( $post_id <= 0 ) {
            return new WP_Error('invalid_post_id', 'Invalid post id.');
        }
        $success_url = add_query_arg( 'bspp_stripe', 'success', get_permalink( $post_id ) );
        $cancel_url = add_query_arg( 'bspp_stripe', 'cancel', get_permalink( $post_id ) );
        // Stripe expects application/x-www-form-urlencoded.
        $body = [
            'mode'                                           => 'subscription',
            'client_reference_id'                            => (string) (int) $purchase_id,
            'payment_method_types[0]'                        => 'card',
            'success_url'                                    => $success_url . '&session_id={CHECKOUT_SESSION_ID}',
            'cancel_url'                                     => $cancel_url,
            'line_items[0][price_data][currency]'            => 'usd',
            'line_items[0][price_data][product_data][name]'  => 'Bobasoftware Publisher Paywall Subscription',
            'line_items[0][price_data][unit_amount]'         => (string) $amount_cents,
            'line_items[0][quantity]'                        => 1,
            'line_items[0][price_data][recurring][interval]' => $interval,
            'metadata[user_id]'                              => (string) (int) $user_id,
            'metadata[purchase_id]'                          => (string) (int) $purchase_id,
            'metadata[plan]'                                 => sanitize_key( $plan ),
            'metadata[purchase_type]'                        => 'subscription',
            'metadata[post_id]'                              => (string) $post_id,
        ];
        $user_email = sanitize_email( (string) $user_email );
        if ( $user_email !== '' ) {
            $body['customer_email'] = $user_email;
        }
        $response = wp_remote_post( 'https://api.stripe.com/v1/checkout/sessions', [
            'method'  => 'POST',
            'headers' => [
                'Authorization' => 'Bearer ' . $stripe_api_key,
            ],
            'timeout' => 30,
            'body'    => $body,
        ] );
        if ( is_wp_error( $response ) ) {
            return $response;
        }
        $code = (int) wp_remote_retrieve_response_code( $response );
        $raw = wp_remote_retrieve_body( $response );
        $data = json_decode( $raw, true );
        if ( $code < 200 || $code >= 300 ) {
            $stripe_message = 'Stripe error';
            if ( is_array( $data ) && isset( $data['error']['message'] ) && is_string( $data['error']['message'] ) ) {
                $stripe_message = $data['error']['message'];
            }
            return new WP_Error('stripe_checkout_failed', $stripe_message, [
                'http_code' => $code,
                'body'      => ( is_array( $data ) ? $data : null ),
            ]);
        }
        if ( empty( $data['id'] ) || empty( $data['url'] ) ) {
            return new WP_Error('stripe_checkout_invalid_response', 'Missing session url or id from Stripe response.');
        }
        return [
            'stripe_session_id'  => (string) $data['id'],
            'stripe_session_url' => (string) $data['url'],
        ];
    }

    public function create_article_unlock_checkout_session(
        $user_id,
        $user_email,
        $purchase_id,
        $amount_dollars,
        $post_id
    ) {
        return new WP_Error('premium_only', 'Per-article checkout is not available in this build.');
    }

    public function create_tip_unlock_checkout_session(
        $user_id,
        $user_email,
        $purchase_id,
        $amount_dollars,
        $post_id
    ) {
        return new WP_Error('premium_only', 'Tip unlock checkout is not available in this build.');
    }

    /**
     * Fetch a Checkout Session from Stripe (used after success redirect when webhooks are delayed or unavailable).
     *
     * @return array|WP_Error Decoded session object.
     */
    public function retrieve_checkout_session( $session_id ) {
        $stripe_api_key = $this->settings->get( 'stripe_api_key', '' );
        if ( empty( $stripe_api_key ) ) {
            return new WP_Error('missing_stripe_api_key', 'Missing stripe_api_key in settings.');
        }
        $session_id = ( is_string( $session_id ) ? sanitize_text_field( $session_id ) : '' );
        if ( $session_id === '' || strpos( $session_id, 'cs_' ) !== 0 ) {
            return new WP_Error('invalid_session_id', 'Invalid checkout session id.');
        }
        $url = 'https://api.stripe.com/v1/checkout/sessions/' . rawurlencode( $session_id );
        $response = wp_remote_get( $url, [
            'timeout' => 25,
            'headers' => [
                'Authorization' => 'Bearer ' . $stripe_api_key,
            ],
        ] );
        if ( is_wp_error( $response ) ) {
            return $response;
        }
        $code = (int) wp_remote_retrieve_response_code( $response );
        $raw = wp_remote_retrieve_body( $response );
        $data = json_decode( $raw, true );
        if ( $code < 200 || $code >= 300 ) {
            $stripe_message = 'Stripe error';
            if ( is_array( $data ) && isset( $data['error']['message'] ) && is_string( $data['error']['message'] ) ) {
                $stripe_message = $data['error']['message'];
            }
            return new WP_Error('stripe_session_retrieve_failed', $stripe_message, [
                'http_code' => $code,
            ]);
        }
        if ( !is_array( $data ) || empty( $data['id'] ) ) {
            return new WP_Error('stripe_session_invalid_response', 'Invalid session response from Stripe.');
        }
        return $data;
    }

    /**
     * Fetch a Stripe Invoice (subscription refunds; read-only).
     *
     * @param string $invoice_id in_…
     * @return array|WP_Error
     */
    public function retrieve_invoice( $invoice_id ) {
        $stripe_api_key = $this->settings->get( 'stripe_api_key', '' );
        if ( empty( $stripe_api_key ) ) {
            return new WP_Error('missing_stripe_api_key', 'Missing stripe_api_key in settings.');
        }
        $invoice_id = ( is_string( $invoice_id ) ? sanitize_text_field( $invoice_id ) : '' );
        if ( $invoice_id === '' || strpos( $invoice_id, 'in_' ) !== 0 ) {
            return new WP_Error('invalid_invoice_id', 'Invalid Stripe invoice id.');
        }
        $url = 'https://api.stripe.com/v1/invoices/' . rawurlencode( $invoice_id );
        $response = wp_remote_get( $url, [
            'timeout' => 25,
            'headers' => [
                'Authorization' => 'Bearer ' . $stripe_api_key,
            ],
        ] );
        if ( is_wp_error( $response ) ) {
            return $response;
        }
        $code = (int) wp_remote_retrieve_response_code( $response );
        $raw = wp_remote_retrieve_body( $response );
        $data = json_decode( $raw, true );
        if ( $code < 200 || $code >= 300 ) {
            $stripe_message = 'Stripe error';
            if ( is_array( $data ) && isset( $data['error']['message'] ) && is_string( $data['error']['message'] ) ) {
                $stripe_message = $data['error']['message'];
            }
            return new WP_Error('stripe_invoice_retrieve_failed', $stripe_message, [
                'http_code' => $code,
            ]);
        }
        if ( !is_array( $data ) || empty( $data['id'] ) ) {
            return new WP_Error('stripe_invoice_invalid_response', 'Invalid invoice response from Stripe.');
        }
        return $data;
    }

    /**
     * @param mixed $value Stripe id string or expanded object with id.
     */
    private static function stripe_expandable_id( $value ) {
        if ( is_string( $value ) && $value !== '' ) {
            return $value;
        }
        if ( is_array( $value ) && !empty( $value['id'] ) && is_string( $value['id'] ) ) {
            return $value['id'];
        }
        return '';
    }

    /**
     * Find a refundable PaymentIntent id or Charge id on a paid invoice (top-level fields and InvoicePayment rows).
     *
     * @return array{pi: string, charge: string} One of pi or charge set when found.
     */
    private static function resolve_refund_target_from_invoice_object( array $inv ) {
        $pi = self::stripe_expandable_id( $inv['payment_intent'] ?? '' );
        if ( $pi !== '' && strpos( $pi, 'pi_' ) === 0 ) {
            return [
                'pi'     => $pi,
                'charge' => '',
            ];
        }
        $charge = self::stripe_expandable_id( $inv['charge'] ?? '' );
        if ( $charge !== '' && strpos( $charge, 'ch_' ) === 0 ) {
            return [
                'pi'     => '',
                'charge' => $charge,
            ];
        }
        $payment_rows = [];
        if ( !empty( $inv['payments']['data'] ) && is_array( $inv['payments']['data'] ) ) {
            $payment_rows = $inv['payments']['data'];
        }
        return self::resolve_refund_target_from_invoice_payment_rows( $payment_rows );
    }

    /**
     * @param array<int, mixed> $invoice_payment_rows Stripe InvoicePayment objects (from invoice.payments.data or invoice_payments list).
     * @return array{pi: string, charge: string}
     */
    private static function resolve_refund_target_from_invoice_payment_rows( array $invoice_payment_rows ) {
        foreach ( $invoice_payment_rows as $row ) {
            if ( !is_array( $row ) ) {
                continue;
            }
            $status = ( isset( $row['status'] ) ? (string) $row['status'] : '' );
            if ( $status !== 'paid' ) {
                continue;
            }
            $payment = ( isset( $row['payment'] ) && is_array( $row['payment'] ) ? $row['payment'] : [] );
            $ptype = ( isset( $payment['type'] ) ? (string) $payment['type'] : '' );
            if ( $ptype === 'payment_intent' ) {
                $pid = self::stripe_expandable_id( $payment['payment_intent'] ?? '' );
                if ( $pid !== '' && strpos( $pid, 'pi_' ) === 0 ) {
                    return [
                        'pi'     => $pid,
                        'charge' => '',
                    ];
                }
            }
            if ( $ptype === 'charge' ) {
                $cid = self::stripe_expandable_id( $payment['charge'] ?? '' );
                if ( $cid !== '' && strpos( $cid, 'ch_' ) === 0 ) {
                    return [
                        'pi'     => '',
                        'charge' => $cid,
                    ];
                }
            }
        }
        // Fallback: Checkout / test mode may omit "paid" on the row while the PI is still refundable.
        foreach ( $invoice_payment_rows as $row ) {
            if ( !is_array( $row ) ) {
                continue;
            }
            $status = ( isset( $row['status'] ) ? (string) $row['status'] : '' );
            if ( $status === 'canceled' ) {
                continue;
            }
            $payment = ( isset( $row['payment'] ) && is_array( $row['payment'] ) ? $row['payment'] : [] );
            $ptype = ( isset( $payment['type'] ) ? (string) $payment['type'] : '' );
            if ( $ptype === 'payment_intent' ) {
                $pid = self::stripe_expandable_id( $payment['payment_intent'] ?? '' );
                if ( $pid !== '' && strpos( $pid, 'pi_' ) === 0 ) {
                    return [
                        'pi'     => $pid,
                        'charge' => '',
                    ];
                }
            }
            if ( $ptype === 'charge' ) {
                $cid = self::stripe_expandable_id( $payment['charge'] ?? '' );
                if ( $cid !== '' && strpos( $cid, 'ch_' ) === 0 ) {
                    return [
                        'pi'     => '',
                        'charge' => $cid,
                    ];
                }
            }
        }
        return [
            'pi'     => '',
            'charge' => '',
        ];
    }

    /**
     * List InvoicePayment objects for an invoice (Checkout / newer invoices may only expose PI here).
     *
     * @return array<int, array<string, mixed>>|WP_Error
     */
    public function list_invoice_payments( $invoice_id ) {
        $stripe_api_key = $this->settings->get( 'stripe_api_key', '' );
        if ( empty( $stripe_api_key ) ) {
            return new WP_Error('missing_stripe_api_key', 'Missing stripe_api_key in settings.');
        }
        $invoice_id = ( is_string( $invoice_id ) ? sanitize_text_field( $invoice_id ) : '' );
        if ( $invoice_id === '' || strpos( $invoice_id, 'in_' ) !== 0 ) {
            return new WP_Error('invalid_invoice_id', 'Invalid Stripe invoice id.');
        }
        $url = 'https://api.stripe.com/v1/invoice_payments?' . rawurlencode( 'invoice' ) . '=' . rawurlencode( $invoice_id ) . '&' . rawurlencode( 'limit' ) . '=20';
        $response = wp_remote_get( $url, [
            'timeout' => 25,
            'headers' => [
                'Authorization' => 'Bearer ' . $stripe_api_key,
            ],
        ] );
        if ( is_wp_error( $response ) ) {
            return $response;
        }
        $code = (int) wp_remote_retrieve_response_code( $response );
        $raw = wp_remote_retrieve_body( $response );
        $data = json_decode( $raw, true );
        if ( $code < 200 || $code >= 300 ) {
            $stripe_message = 'Stripe error';
            if ( is_array( $data ) && isset( $data['error']['message'] ) && is_string( $data['error']['message'] ) ) {
                $stripe_message = $data['error']['message'];
            }
            return new WP_Error('stripe_invoice_payments_list_failed', $stripe_message, [
                'http_code' => $code,
            ]);
        }
        if ( !is_array( $data ) || !isset( $data['data'] ) || !is_array( $data['data'] ) ) {
            return new WP_Error('stripe_invoice_payments_invalid', 'Invalid invoice_payments list response from Stripe.');
        }
        $out = [];
        foreach ( $data['data'] as $item ) {
            if ( is_array( $item ) ) {
                $out[] = $item;
            }
        }
        return $out;
    }

    /**
     * POST /v1/refunds (internal).
     *
     * @return array|WP_Error
     */
    private function post_stripe_refund( array $body, $idempotency_key ) {
        $stripe_api_key = $this->settings->get( 'stripe_api_key', '' );
        if ( empty( $stripe_api_key ) ) {
            return new WP_Error('missing_stripe_api_key', 'Missing stripe_api_key in settings.');
        }
        $idempotency_key = ( is_string( $idempotency_key ) ? sanitize_text_field( $idempotency_key ) : '' );
        if ( $idempotency_key === '' ) {
            return new WP_Error('invalid_idempotency', 'Missing idempotency key.');
        }
        if ( empty( $body ) ) {
            return new WP_Error('invalid_refund_body', 'Missing refund parameters.');
        }
        $response = wp_remote_post( 'https://api.stripe.com/v1/refunds', [
            'timeout' => 25,
            'headers' => [
                'Authorization'   => 'Bearer ' . $stripe_api_key,
                'Idempotency-Key' => $idempotency_key,
            ],
            'body'    => $body,
        ] );
        if ( is_wp_error( $response ) ) {
            return $response;
        }
        $code = (int) wp_remote_retrieve_response_code( $response );
        $raw = wp_remote_retrieve_body( $response );
        $data = json_decode( $raw, true );
        if ( $code < 200 || $code >= 300 ) {
            $stripe_message = 'Stripe error';
            if ( is_array( $data ) && isset( $data['error']['message'] ) && is_string( $data['error']['message'] ) ) {
                $stripe_message = $data['error']['message'];
            }
            return new WP_Error('stripe_refund_failed', $stripe_message, [
                'http_code' => $code,
            ]);
        }
        if ( !is_array( $data ) || empty( $data['id'] ) ) {
            return new WP_Error('stripe_refund_invalid', 'Invalid refund response from Stripe.');
        }
        return $data;
    }

    /**
     * Full refund for a paid Invoice (subscription checkout).
     *
     * Uses top-level invoice.payment_intent / charge when present; otherwise reads InvoicePayment rows
     * (embedded invoice.payments.data or GET /v1/invoice_payments) so Checkout subscriptions work in test mode.
     *
     * @param string $invoice_id     in_…
     * @param string $idempotency_key Unique per refund attempt.
     * @return array|WP_Error
     */
    public function create_refund_for_invoice( $invoice_id, $idempotency_key ) {
        $invoice_id = ( is_string( $invoice_id ) ? sanitize_text_field( $invoice_id ) : '' );
        if ( $invoice_id === '' || strpos( $invoice_id, 'in_' ) !== 0 ) {
            return new WP_Error('invalid_invoice_id', 'Invalid Stripe invoice id.');
        }
        $inv = $this->retrieve_invoice( $invoice_id );
        if ( is_wp_error( $inv ) ) {
            return $inv;
        }
        $targets = self::resolve_refund_target_from_invoice_object( $inv );
        if ( $targets['pi'] !== '' ) {
            return $this->post_stripe_refund( [
                'payment_intent' => $targets['pi'],
            ], $idempotency_key );
        }
        if ( $targets['charge'] !== '' ) {
            return $this->post_stripe_refund( [
                'charge' => $targets['charge'],
            ], $idempotency_key );
        }
        $listed = $this->list_invoice_payments( $invoice_id );
        if ( is_wp_error( $listed ) ) {
            return $listed;
        }
        $targets = self::resolve_refund_target_from_invoice_payment_rows( $listed );
        if ( $targets['pi'] !== '' ) {
            return $this->post_stripe_refund( [
                'payment_intent' => $targets['pi'],
            ], $idempotency_key );
        }
        if ( $targets['charge'] !== '' ) {
            return $this->post_stripe_refund( [
                'charge' => $targets['charge'],
            ], $idempotency_key );
        }
        return new WP_Error('invoice_no_refundable_payment', 'This invoice has no refundable payment intent or charge.');
    }

    /**
     * Stripe Customer Billing Portal — readers manage/cancel subscription, cards, invoices.
     *
     * Publishers must enable the Customer Portal in Stripe Dashboard:
     * Settings → Billing → Customer portal (configure products/features and return URLs whitelist).
     *
     * @param string $stripe_customer_id Stripe customer id (cus_…).
     * @param string $return_url        HTTPS URL to send the reader after the portal session.
     * @return array{url:string}|WP_Error
     */
    public function create_billing_portal_session( $stripe_customer_id, $return_url ) {
        $stripe_api_key = $this->settings->get( 'stripe_api_key', '' );
        if ( empty( $stripe_api_key ) ) {
            return new WP_Error('missing_stripe_api_key', 'Missing stripe_api_key in settings.');
        }
        $stripe_customer_id = ( is_string( $stripe_customer_id ) ? sanitize_text_field( $stripe_customer_id ) : '' );
        $return_url = ( is_string( $return_url ) ? esc_url_raw( $return_url ) : '' );
        if ( $stripe_customer_id === '' || strpos( $stripe_customer_id, 'cus_' ) !== 0 ) {
            return new WP_Error('invalid_customer_id', 'Invalid Stripe customer id.');
        }
        if ( $return_url === '' ) {
            return new WP_Error('invalid_return_url', 'Invalid return URL.');
        }
        $response = wp_remote_post( 'https://api.stripe.com/v1/billing_portal/sessions', [
            'timeout' => 25,
            'headers' => [
                'Authorization' => 'Bearer ' . $stripe_api_key,
            ],
            'body'    => [
                'customer'   => $stripe_customer_id,
                'return_url' => $return_url,
            ],
        ] );
        if ( is_wp_error( $response ) ) {
            return $response;
        }
        $code = (int) wp_remote_retrieve_response_code( $response );
        $raw = wp_remote_retrieve_body( $response );
        $data = json_decode( $raw, true );
        if ( $code < 200 || $code >= 300 ) {
            $stripe_message = 'Stripe error';
            if ( is_array( $data ) && isset( $data['error']['message'] ) && is_string( $data['error']['message'] ) ) {
                $stripe_message = $data['error']['message'];
            }
            return new WP_Error('stripe_portal_failed', $stripe_message, [
                'http_code' => $code,
            ]);
        }
        if ( empty( $data['url'] ) || !is_string( $data['url'] ) ) {
            return new WP_Error('stripe_portal_invalid', 'Missing portal URL from Stripe.');
        }
        return [
            'url' => (string) $data['url'],
        ];
    }

    /**
     * Schedule subscription cancellation at end of current billing period (reader keeps access until then).
     *
     * @param string $stripe_subscription_id sub_…
     * @return array|WP_Error Decoded subscription object on success.
     */
    public function cancel_subscription_at_period_end( $stripe_subscription_id ) {
        $stripe_api_key = $this->settings->get( 'stripe_api_key', '' );
        if ( empty( $stripe_api_key ) ) {
            return new WP_Error('missing_stripe_api_key', 'Missing stripe_api_key in settings.');
        }
        $stripe_subscription_id = ( is_string( $stripe_subscription_id ) ? sanitize_text_field( $stripe_subscription_id ) : '' );
        if ( $stripe_subscription_id === '' || strpos( $stripe_subscription_id, 'sub_' ) !== 0 ) {
            return new WP_Error('invalid_subscription_id', 'Invalid Stripe subscription id.');
        }
        $url = 'https://api.stripe.com/v1/subscriptions/' . rawurlencode( $stripe_subscription_id );
        $response = wp_remote_post( $url, [
            'timeout' => 25,
            'headers' => [
                'Authorization' => 'Bearer ' . $stripe_api_key,
            ],
            'body'    => [
                'cancel_at_period_end' => 'true',
            ],
        ] );
        if ( is_wp_error( $response ) ) {
            return $response;
        }
        $code = (int) wp_remote_retrieve_response_code( $response );
        $raw = wp_remote_retrieve_body( $response );
        $data = json_decode( $raw, true );
        if ( $code < 200 || $code >= 300 ) {
            $stripe_message = 'Stripe error';
            if ( is_array( $data ) && isset( $data['error']['message'] ) && is_string( $data['error']['message'] ) ) {
                $stripe_message = $data['error']['message'];
            }
            return new WP_Error('stripe_subscription_cancel_failed', $stripe_message, [
                'http_code' => $code,
            ]);
        }
        if ( !is_array( $data ) || empty( $data['id'] ) ) {
            return new WP_Error('stripe_subscription_invalid', 'Invalid subscription response from Stripe.');
        }
        return $data;
    }

    /**
     * Full refund for a completed PaymentIntent (per-article purchase or tip).
     *
     * @param string $payment_intent_id pi_…
     * @param string $idempotency_key    Unique per refund attempt (Stripe idempotency).
     * @return array|WP_Error Decoded refund object.
     */
    public function create_refund_for_payment_intent( $payment_intent_id, $idempotency_key ) {
        $payment_intent_id = ( is_string( $payment_intent_id ) ? sanitize_text_field( $payment_intent_id ) : '' );
        if ( $payment_intent_id === '' || strpos( $payment_intent_id, 'pi_' ) !== 0 ) {
            return new WP_Error('invalid_payment_intent', 'Invalid payment intent id.');
        }
        return $this->post_stripe_refund( [
            'payment_intent' => $payment_intent_id,
        ], $idempotency_key );
    }

}
