<?php

namespace App\Http\Controllers;

use App\Models\Plan;
use App\Models\Subscription;
use App\Services\Billing\BillingManager;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Inertia\Inertia;
use Inertia\Response;

/**
 * Hosted-checkout entry point. POSTs to /checkout return JSON
 * (`{ checkout_url, session_id }`) so a future desktop wrapper can call the
 * same endpoint via Sanctum without a parallel API surface; the success +
 * cancel landings render Inertia pages because they're navigational.
 */
class CheckoutController extends Controller
{
    public function __construct(private readonly BillingManager $billing) {}

    public function store(Request $request): JsonResponse
    {
        $data = $request->validate([
            'plan_key' => ['required', 'string', Rule::exists('plans', 'key')->where('is_active', true)],
            'period' => ['required', Rule::in(['monthly', 'yearly'])],
            'provider' => ['required', 'string', Rule::exists('payment_providers', 'key')->where('is_active', true)],
            // currency_id selects which `plan_prices` row to use. Optional
            // for back-compat — when null, providers fall back to the
            // deployment's default currency. New callers (post-rebuild
            // SPA pricing page) always pass it.
            'currency_id' => ['nullable', 'integer', Rule::exists('currencies', 'id')],
        ]);

        $plan = Plan::query()
            ->where('key', $data['plan_key'])
            ->where('is_active', true)
            ->firstOrFail();

        // Free is the default tier — never a checkout target. Surfacing 422
        // here keeps the StripeProvider out of a no-price-id branch.
        if ($plan->key === 'free') {
            return response()->json([
                'error' => 'Free plan does not require checkout.',
            ], 422);
        }

        $user = $request->user();

        // Reject any new checkout when the user already has an active
        // subscription. The intended path for upgrades/downgrades is
        // change-plan (provider's `revise` / Stripe `update`) which
        // modifies the EXISTING subscription in place — keeping ONE sub
        // row per user. Allowing a fresh checkout here would create a
        // PARALLEL second subscription, and cancelling either of them
        // leaves the user on the other one (the "I upgraded from Pro
        // to Scale then cancelled Scale and got dropped back to Pro"
        // bug). Cross-provider switches are an explicit choice exposed
        // via the `?switch_provider=1` flag on /pricing — those callers
        // pass `switch_provider=true` here to bypass the guard, but the
        // OLD sub then has to be cancelled by the user (Pricing page UI
        // walks them through it).
        $existingActive = Subscription::query()
            ->where('user_id', $user->id)
            ->whereIn('status', ['active', 'trialing'])
            ->first();

        if ($existingActive) {
            // Same plan + same period → already subscribed (the existing
            // error message stays).
            if ($existingActive->plan_id === $plan->id && $existingActive->period === $data['period']) {
                return response()->json([
                    'error' => 'You are already subscribed to this plan and period.',
                ], 422);
            }

            // Same provider → route to change-plan (revise) instead.
            // Surface a pointer the UI can use to redirect the user.
            if ($existingActive->provider_key === $data['provider']) {
                return response()->json([
                    'error' => "You're already on the {$existingActive->plan?->name} plan. "
                        ."Use Change plan on your account page to switch instead — "
                        ."this avoids creating a parallel subscription.",
                    'change_plan_url' => route('account.subscription'),
                ], 422);
            }

            // Different provider → require explicit switch-provider intent.
            // /pricing surfaces a "Use a different payment method" link
            // that sets switch_provider=true in the body.
            if (! $request->boolean('switch_provider')) {
                return response()->json([
                    'error' => "You're already subscribed via "
                        .ucfirst($existingActive->provider_key)
                        .". To pay with a different provider, cancel the existing subscription first.",
                ], 422);
            }
        }

        try {
            $provider = $this->billing->provider($data['provider']);
            $session = $provider->createCheckoutSession(
                $user,
                $plan,
                $data['period'],
                $data['currency_id'] ?? null,
            );
        } catch (\Throwable $e) {
            // Catch \Throwable (not just RuntimeException) — Razorpay's SDK
            // throws Razorpay\Api\Errors\BadRequestError which extends
            // \Exception, Stripe throws \Stripe\Exception\ApiErrorException,
            // and we never want raw provider error text leaking to the user
            // (attacker fingerprinting signal + APP_DEBUG=true would render
            // the message verbatim on the page). Log full detail server-side.
            Log::error('Checkout session creation failed', [
                'provider' => $data['provider'],
                'plan_key' => $plan->key,
                'period' => $data['period'],
                'user_id' => $user->id,
                'message' => $e->getMessage(),
                'exception' => $e,
            ]);

            return response()->json([
                'error' => 'Checkout is temporarily unavailable. Please try again or contact support.',
            ], 500);
        }

        return response()->json([
            'checkout_url' => $session->url,
            'session_id' => $session->providerSessionId,
        ]);
    }

    public function success(Request $request): Response
    {
        $sessionId = $request->query('session_id');

        // Stripe's success_url is a plain GET redirect — provisioning
        // normally happens via the `checkout.session.completed` webhook.
        // Front-run that webhook by retrieving the session synchronously
        // and applying the same upsert logic. Two reasons:
        //
        //  1. Local dev where Stripe webhooks can't reach localhost —
        //     happy-path provisioning works without `stripe listen`.
        //  2. Production race — customer's browser typically lands here
        //     a few hundred ms before the webhook arrives; sync-verify
        //     means they see the new plan on next page load instead of
        //     "still on Free."
        //
        // Webhook stays canonical — this just front-runs it. Idempotent.
        // Razorpay's success path doesn't need this — its checkout-modal
        // verification runs synchronously in `razorpayVerify` before
        // landing here.
        if (is_string($sessionId) && str_starts_with($sessionId, 'cs_')) {
            try {
                $provider = $this->billing->provider('stripe');
                if (method_exists($provider, 'verifyAndProvisionCheckoutSession')) {
                    $provider->verifyAndProvisionCheckoutSession($sessionId);
                }
            } catch (\Throwable $e) {
                // Verification failed — log + render the success page
                // anyway. The webhook will catch up; user sees their sub
                // appear shortly via a page refresh.
                Log::warning('checkout.success sync-verify failed', [
                    'session_id' => $sessionId,
                    'error' => $e->getMessage(),
                ]);
            }
        }

        return Inertia::render('Checkout/Success', [
            'sessionId' => $sessionId,
        ]);
    }

    public function cancel(): Response
    {
        return Inertia::render('Checkout/Cancel');
    }

    /**
     * Renders the Razorpay Standard Checkout modal page. The user lands here
     * via redirect from `POST /checkout`'s `checkout_url` response when the
     * active provider is Razorpay; the page loads `checkout.js` and pops
     * the modal pre-configured with the subscription_id created server-side.
     */
    public function razorpay(Request $request, string $subscription): Response
    {
        $user = $request->user();

        $sub = Subscription::query()
            ->where('user_id', $user->id)
            ->where('provider_key', 'razorpay')
            ->where('provider_subscription_id', $subscription)
            ->firstOrFail();

        $plan = Plan::query()->find($sub->plan_id);

        $provider = $this->billing->provider('razorpay');
        if (! method_exists($provider, 'publicKey')) {
            abort(500, 'Razorpay provider missing publicKey() helper.');
        }
        $keyId = $provider->publicKey();
        if (! is_string($keyId) || $keyId === '') {
            abort(500, 'Razorpay provider not configured.');
        }

        return Inertia::render('Checkout/Razorpay', [
            'subscription_id' => $sub->provider_subscription_id,
            'key_id' => $keyId,
            'plan' => [
                'key' => $plan?->key,
                'name' => $plan?->name,
                'period' => $sub->period,
            ],
            'prefill' => [
                'name' => (string) $user->name,
                'email' => (string) $user->email,
                // Phone — placeholder until we add a phone-collection step.
                'contact' => '+919999999999',
            ],
            'success_url' => route('checkout.success'),
            'cancel_url' => route('checkout.cancel'),
        ]);
    }

    /**
     * Validates the signature Razorpay's checkout.js returns after a
     * successful payment. Marks the local subscription active immediately;
     * the `subscription.activated` webhook will reaffirm idempotently.
     */
    public function razorpayVerify(Request $request): JsonResponse
    {
        $data = $request->validate([
            'razorpay_payment_id' => ['required', 'string'],
            'razorpay_subscription_id' => ['required', 'string'],
            'razorpay_signature' => ['required', 'string'],
        ]);

        $provider = $this->billing->provider('razorpay');
        if (! method_exists($provider, 'verifyCheckoutSignature')) {
            return response()->json(['error' => 'verifier missing'], 500);
        }

        $ok = $provider->verifyCheckoutSignature(
            $data['razorpay_payment_id'],
            $data['razorpay_subscription_id'],
            $data['razorpay_signature'],
        );

        if (! $ok) {
            return response()->json(['error' => 'signature mismatch'], 400);
        }

        // Local row may not yet exist if the user lands here before the
        // webhook fires (race). Update if present; otherwise the webhook
        // handler will create it from scratch.
        $sub = Subscription::query()
            ->where('user_id', $request->user()->id)
            ->where('provider_key', 'razorpay')
            ->where('provider_subscription_id', $data['razorpay_subscription_id'])
            ->first();
        if ($sub && $sub->status !== 'active') {
            $sub->update(['status' => 'active']);
        }

        return response()->json([
            'ok' => true,
            'redirect' => route('checkout.success', ['session_id' => $data['razorpay_subscription_id']]),
        ]);
    }
}
