<?php
/**
 * Checkout REST API Controller.
 *
 * Endpoints:
 *   GET    /titancart/v1/checkout             Get checkout data (cart + payment + shipping)
 *   POST   /titancart/v1/checkout/validate    Validate address fields (inline, no order)
 *   POST   /titancart/v1/checkout/place-order Place the order
 *   GET    /titancart/v1/orders/{number}      Get order by order number (thank-you page)
 *
 * @package TitanCart\REST\Checkout
 */

namespace TitanCart\REST\Checkout;

use TitanCart\Modules\Checkout\CheckoutManager;
use TitanCart\Modules\Checkout\OrderRepository;
use TitanCart\Modules\Shipping\ShippingManager;
use TitanCart\Modules\Cart\CartManager;
use TitanCart\Services\SettingsService;
use TitanCart\Core\LogService;
use WP_REST_Controller;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

defined( 'ABSPATH' ) || exit;

class CheckoutController extends WP_REST_Controller {

    protected $namespace = 'titancart/v1';
    protected $rest_base = 'checkout';

    private CheckoutManager $manager;
    private OrderRepository $order_repo;

    public function __construct() {
        $this->manager    = new CheckoutManager();
        $this->order_repo = new OrderRepository();
    }

    public function register_routes(): void {
        // WP.org R1 🅱4 — permission_callback audit: every checkout route
        // below is public BY DESIGN — guest checkout is a core storefront
        // capability. All operations act on the CALLER'S OWN cart session
        // (CartSession cookie); place-order creates an order from that cart
        // only. Server-side price/stock/coupon validation happens in
        // CheckoutManager regardless of caller. Per-route rationale:
        // docs/WPORG-REVIEWER-NOTES.md §REST.

        // GET /checkout — return cart + payment + shipping options.
        register_rest_route( $this->namespace, '/' . $this->rest_base, [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_checkout_data' ],
                'permission_callback' => '__return_true', // Public by design: guest checkout — returns the caller's own cart-session totals the checkout page renders.
            ],
        ] );

        // POST /checkout/validate — validate address only.
        register_rest_route( $this->namespace, '/' . $this->rest_base . '/validate', [
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'validate_address' ],
                'permission_callback' => '__return_true', // Public by design: guest checkout — stateless address validation; writes nothing.
            ],
        ] );

        // POST /checkout/place-order — submit the order.
        register_rest_route( $this->namespace, '/' . $this->rest_base . '/place-order', [
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'place_order' ],
                'permission_callback' => '__return_true', // Public by design: guest checkout — creates an order from the caller's OWN cart session only; all pricing/stock/coupon validation is server-side; reCAPTCHA-gated when configured.
            ],
        ] );

        // POST /checkout/finalize/{order_id} — commit a deferred-payment order.
        // Used by Stripe Card Elements after stripe.confirmCardPayment() succeeds
        // in the browser. Re-queries the gateway's API to verify the payment
        // actually settled BEFORE decrementing stock or destroying the cart, so
        // the endpoint is safe to expose publicly. See CheckoutManager::finalize_order
        // for the full security model.
        register_rest_route( $this->namespace, '/' . $this->rest_base . '/finalize/(?P<order_id>\d+)', [
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'finalize_order' ],
                'permission_callback' => '__return_true', // Public by design: gateway return leg — the handler verifies the order belongs to the caller's own cart session / order key before finalizing.
                'args'                => [
                    'order_id' => [
                        'type'              => 'integer',
                        'required'          => true,
                        'sanitize_callback' => 'absint',
                    ],
                ],
            ],
        ] );

        // POST /checkout/shipping-methods — get available shipping for an address.
        register_rest_route( $this->namespace, '/' . $this->rest_base . '/shipping-methods', [
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'get_shipping_methods' ],
                'permission_callback' => '__return_true', // Public by design: guest checkout — shipping methods for the caller's own cart session; writes nothing.
            ],
        ] );

        // GET /orders/{number} — retrieve order by number (thank-you page & admin).
        register_rest_route( $this->namespace, '/orders/(?P<number>[A-Z0-9]+-[A-Z0-9-]+)', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_order' ],
                'permission_callback' => [ $this, 'get_order_permissions_check' ],
            ],
        ] );
    }

    // ── Handlers ──────────────────────────────────────────────────────────────

    /** GET /checkout */
    public function get_checkout_data( WP_REST_Request $request ): WP_REST_Response {
        $lang = sanitize_text_field( $request->get_param( 'lang' ) ?? 'en' );
        $data = $this->manager->get_checkout_data( $lang );
        return new WP_REST_Response( $data );
    }

    /** POST /checkout/validate */
    public function validate_address( WP_REST_Request $request ): WP_REST_Response {
        $body   = $request->get_json_params() ?: [];
        $result = $this->manager->validate_address( $body );
        return new WP_REST_Response( $result );
    }

    /** POST /checkout/place-order */
    public function place_order( WP_REST_Request $request ): WP_REST_Response|WP_Error {
        $lang = sanitize_text_field( $request->get_param( 'lang' ) ?? 'en' );
        $body = $request->get_json_params() ?: [];

        // reCAPTCHA gate — guests only (the toggle is "Checkout (guest)"). Token
        // is read from the request body by the extension. Logged-in customers and
        // unprotected installs pass straight through.
        if ( ! is_user_logged_in()
            && ! \TitanCart\Extensions\RecaptchaExtension::passes( 'checkout', (string) ( $body['recaptcha_token'] ?? '' ) ) ) {
            return new WP_Error(
                'tc_recaptcha_failed',
                __( 'reCAPTCHA verification failed. Please try again.', 'titancart-lite' ),
                [ 'status' => 422 ]
            );
        }

        $result = $this->manager->place_order( $body, $lang );

        if ( ! $result['success'] ) {
            LogService::warning( 'checkout', 'failed', 'Checkout failed: ' . ( $result['message'] ?? 'unknown' ), [
                'errors'  => $result['errors'] ?? [],
                'email'   => $body['billing']['email'] ?? '',
                'gateway' => $body['payment_method_id'] ?? '',
            ] );
            return new WP_Error(
                'tc_checkout_failed',
                $result['message'],
                [
                    'status' => 422,
                    'errors' => $result['errors'],
                ]
            );
        }

        LogService::info( 'orders', 'create', 'Order placed: #' . ( $result['order_number'] ?? $result['order_id'] ?? '?' ) . ' — $' . number_format( (float) ( $result['total'] ?? 0 ), 2 ), [
            'object_type'  => 'order',
            'object_id'    => (int) ( $result['order_id'] ?? 0 ),
            'order_number' => $result['order_number'] ?? '',
            'total'        => $result['total'] ?? 0,
            'email'        => $body['billing']['email'] ?? '',
            'items_count'  => count( $result['items'] ?? [] ),
        ] );

        // Newsletter opt-in (S241 / Phase 5 block B) — fire only AFTER a confirmed
        // successful order, never on the failure path. Best-effort: a subscribe
        // hiccup must never affect the order response the customer is waiting on.
        $this->maybe_subscribe_checkout( $body );

        return new WP_REST_Response( $result, 201 );
    }

    /**
     * Enrol a checkout customer into the newsletter lists they opted into.
     *
     * Server-side companion to the per-list "Subscribe" checkboxes rendered on
     * the checkout review step. The browser posts a `newsletter_optin` array of
     * list IDs alongside the order; here we (a) honour the global
     * newsletters.show_subscribe_checkout master toggle, (b) validate every
     * submitted ID against the set of ACTIVE lists actually flagged
     * show_on_checkout = 1 (so a spoofed/stale ID can't enrol elsewhere), then
     * (c) create the subscriber tagged source 'checkout'. Double opt-in still
     * applies — NewsletterManager mints an activation token + fires the list
     * autoresponder. Wrapped defensively so nothing here can break checkout.
     *
     * @since S241
     */
    private function maybe_subscribe_checkout( array $body ): void {
        try {
            // class_exists: Lite strips the Newsletters module (R1 §🅰) — the
            // checkout opt-in surface doesn't render there either; no-op.
            if ( ! class_exists( \TitanCart\Modules\Newsletters\NewsletterManager::class ) ) {
                return;
            }
            $requested = $body['newsletter_optin'] ?? [];
            if ( ! is_array( $requested ) ) {
                $requested = [ $requested ];
            }
            $requested = array_values( array_unique( array_filter( array_map( 'intval', $requested ) ) ) );
            if ( empty( $requested ) ) {
                return;
            }

            // Global master toggle (matches the schema default of ON).
            if ( ! SettingsService::bool( 'newsletters', 'show_subscribe_checkout', true ) ) {
                return;
            }

            // The place-order payload arrives FLAT (checkout.js collectFormData()
            // keys every field by its raw `name` attr → billing_email, not a
            // nested billing.email). Read the flat shape first, falling back to a
            // nested billing[] for forward-compat / non-browser callers.
            $billing = is_array( $body['billing'] ?? null ) ? $body['billing'] : [];
            $email   = sanitize_email( $body['billing_email'] ?? $billing['email'] ?? '' );
            if ( ! is_email( $email ) ) {
                return;
            }

            global $wpdb;
            $eligible = array_map( 'intval', (array) $wpdb->get_col(
                "SELECT id FROM {$wpdb->prefix}tc_subscriber_lists
                 WHERE status = 'active' AND show_on_checkout = 1"
            ) );

            $target = array_values( array_intersect( $requested, $eligible ) );
            if ( empty( $target ) ) {
                return;
            }

            ( new \TitanCart\Modules\Newsletters\NewsletterManager() )->create_subscriber( [
                'email'      => $email,
                'first_name' => sanitize_text_field( $body['billing_first_name'] ?? $billing['first_name'] ?? '' ),
                'last_name'  => sanitize_text_field( $body['billing_last_name']  ?? $billing['last_name']  ?? '' ),
                'status'     => 'active',
                'source'     => 'checkout',
                'ip_address' => titancart_client_ip(), // WP.org R1 🅱2: validated capture.
                'list_ids'   => $target,
            ] );
        } catch ( \Throwable $e ) {
            // Swallow — the order already succeeded; a newsletter hiccup is non-fatal.
            LogService::warning( 'newsletters', 'checkout_optin', 'Checkout newsletter opt-in failed: ' . $e->getMessage() );
        }
    }

    /**
     * POST /checkout/finalize/{order_id}
     *
     * Commit an order whose payment was deferred to a JS-driven confirmation step
     * (currently only Stripe Card Elements / PaymentIntents). The handler delegates
     * all security and idempotency logic to CheckoutManager::finalize_order().
     *
     * Returns 200 on success (including idempotent already-finalized hits) and
     * 400 on any failure — bad order_id, missing transaction, gateway not
     * registered, or payment not yet confirmed by the gateway. Unlike a 401/403,
     * a 400 keeps the JS error path simple: any non-2xx means "don't trust the
     * commit happened, but redirect anyway and let the webhook reconcile."
     */
    public function finalize_order( WP_REST_Request $request ): WP_REST_Response {
        $order_id = (int) $request->get_param( 'order_id' );

        $result = $this->manager->finalize_order( $order_id );

        if ( ! $result['success'] ) {
            LogService::warning( 'checkout', 'finalize-failed', 'Finalize failed for order #' . $order_id . ': ' . ( $result['message'] ?? 'unknown' ), [
                'object_type' => 'order',
                'object_id'   => $order_id,
            ] );
            return new WP_REST_Response( $result, 400 );
        }

        // Only log the first commit — idempotent no-ops are not worth a row.
        if ( empty( $result['already_finalized'] ) ) {
            LogService::info( 'checkout', 'finalize', 'Order #' . $order_id . ' finalized after async payment confirmation', [
                'object_type' => 'order',
                'object_id'   => $order_id,
            ] );
        }

        return new WP_REST_Response( $result, 200 );
    }

    /**
     * POST /checkout/shipping-methods
     *
     * Returns available shipping methods + calculated prices for the
     * given address.  Called via AJAX after the customer fills in their
     * country / state / postcode on the checkout form.
     */
    public function get_shipping_methods( WP_REST_Request $request ): WP_REST_Response {
        $body = $request->get_json_params() ?: [];

        $address = [
            'country'  => strtoupper( sanitize_text_field( $body['country']  ?? '' ) ),
            'state'    => strtoupper( sanitize_text_field( $body['state']    ?? '' ) ),
            'postcode' => sanitize_text_field( $body['postcode'] ?? '' ),
        ];

        if ( empty( $address['country'] ) ) {
            return new WP_REST_Response( [ 'methods' => [] ] );
        }

        $cart_manager     = new CartManager();
        $cart             = $cart_manager->get_cart();
        $shipping_manager = new ShippingManager();
        $rates            = $shipping_manager->get_available_rates( $cart, $address );

        $methods = [];
        // class_exists: Lite strips the Promotions module (R1 §🅰) — no
        // free-shipping waiver exists there, so rates pass through unchanged.
        $promotions = class_exists( \TitanCart\Modules\Promotions\PromotionsModule::class )
            ? new \TitanCart\Modules\Promotions\PromotionsModule()
            : null;
        foreach ( $rates as $rate ) {
            $method_id = (int) ( $rate['method_id'] ?? 0 );
            $price     = round( (float) ( $rate['cost'] ?? 0 ), 2 );

            // Apply the same free-shipping waiver the final charge uses, so a
            // qualifying promotion (e.g. "free shipping over $200") shows $0 in
            // the method picker and order summary — not only on the placed order.
            // Only the waiver runs here (not a full rate recompute), so ordinary
            // method prices are unchanged.
            if ( $promotions ) {
                $price = $promotions->maybe_free_shipping( $price, $method_id, $cart, $address );
            }

            $methods[] = [
                'id'            => $method_id,
                // service_code disambiguates realtime carrier services that share one
                // base method_id (KI-32). Empty for core (zone) methods. The picker
                // must echo this back as shipping_service_code at place-order so the
                // charged rate matches the picked service.
                'service_code'  => (string) ( $rate['service_code'] ?? '' ),
                'carrier'       => (string) ( $rate['carrier'] ?? '' ),
                'rate_type'     => (string) ( $rate['rate_type'] ?? 'zone' ),
                'name'          => $rate['name'] ?? '',
                'price'         => round( (float) $price, 2 ),
                'delivery_days' => $rate['delivery_days'] ?? null,
            ];
        }

        return new WP_REST_Response( [ 'methods' => $methods ] );
    }

    /** GET /orders/{number} */
    public function get_order( WP_REST_Request $request ): WP_REST_Response|WP_Error {
        $number = strtoupper( sanitize_text_field( $request->get_param( 'number' ) ) );
        $order  = $this->order_repo->get_by_number( $number );

        if ( ! $order ) {
            return new WP_Error( 'tc_order_not_found', __( 'Order not found.', 'titancart-lite' ), [ 'status' => 404 ] );
        }

        // ── SECURITY: Real ownership/key check ────────────────────────────────
        // order_number is enumerable (TC-YYYYMMDD-NNNN). Without this gate,
        // anyone could iterate numbers and harvest billing/shipping addresses,
        // emails, phones, totals, and items. Three valid access paths:
        //
        //   1. Admin (manage_options).
        //   2. Authenticated WP user whose tc_customers.id matches the order.
        //   3. Anyone supplying a ?key= matching the row's order_key
        //      (CSPRNG-generated 32-char hex; ~128 bits of entropy).
        //
        // On failure we return 404 — NOT 403 — so the endpoint can't be used
        // to confirm whether a given order_number exists. hash_equals() is
        // constant-time, defending against timing-side-channel guessing.
        $is_admin = current_user_can( 'manage_options' );

        $is_owner = false;
        if ( ! $is_admin && is_user_logged_in() ) {
            $repo         = new \TitanCart\Modules\Accounts\CustomerRepository();
            $customer_row = $repo->find_by_wp_user( get_current_user_id() );
            if ( $customer_row && (int) $customer_row['id'] === (int) $order['customer_id'] ) {
                $is_owner = true;
            }
        }

        $supplied_key = (string) ( $request->get_param( 'key' ) ?? '' );
        $row_key      = (string) ( $order['order_key'] ?? '' );
        $key_ok       = ( $supplied_key !== '' && $row_key !== '' && hash_equals( $row_key, $supplied_key ) );

        if ( ! $is_admin && ! $is_owner && ! $key_ok ) {
            // Deliberate 404 — do not leak existence.
            return new WP_Error( 'tc_order_not_found', __( 'Order not found.', 'titancart-lite' ), [ 'status' => 404 ] );
        }

        // Strip server-only fields before returning to non-admin callers.
        // ip_address / user_agent are forensic columns, not customer data.
        if ( ! $is_admin ) {
            unset( $order['ip_address'], $order['user_agent'] );
        }

        $order['items'] = $this->order_repo->get_items( (int) $order['id'] );
        return new WP_REST_Response( $order );
    }

    // ── Permissions ───────────────────────────────────────────────────────────

    /**
     * Order endpoint is publicly callable, but the handler enforces auth via
     * one of: admin capability, authenticated ownership, or a matching key
     * query param. See get_order() for the full check.
     */
    public function get_order_permissions_check( WP_REST_Request $request ): bool {
        return true;
    }
}
