<?php

namespace App\Http\Controllers;

use App\Models\Invoice;
use App\Models\Plan;
use App\Models\Subscription;
use App\Services\AuditLogger;
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;
use Throwable;

/**
 * Account-scoped endpoints. Phase 5B shipped only the subscription status
 * poller (used by Checkout/Success to detect the webhook landing); 5E adds
 * the user-facing dashboard + self-serve cancel/resume/changePlan. The
 * mutation endpoints intentionally mirror the admin shape but scope to the
 * caller — actor in the audit log is the user, not an admin.
 */
class AccountController extends Controller
{
    public function subscriptionStatus(Request $request): JsonResponse
    {
        $user = $request->user();
        $sub = $user->activeSubscription();

        return response()->json([
            'has_active_subscription' => $sub !== null,
            'plan_key' => $sub?->plan?->key,
            'status' => $sub?->status,
            'current_period_ends_at' => $sub?->current_period_ends_at?->toIso8601String(),
        ]);
    }

    public function subscription(Request $request): Response
    {
        $user = $request->user();
        $sub = $user->activeSubscription();

        // Free is real data — features live on the seeded `free` plan row,
        // not as a hardcoded snippet. This keeps a single source of truth
        // for what the Free tier currently offers.
        $currentPlan = $sub?->plan ?? Plan::query()->where('key', 'free')->firstOrFail();

        // Razorpay change-plan can land in a "pending payment" state when
        // the PATCH-first attempt falls back to cancel-then-create — the
        // new sub is `pending` until the user authorizes via the checkout
        // modal. Surface this so the UI can show a "Complete payment"
        // CTA instead of leaving the user stranded.
        //
        // Filter:
        //  1. Only "recent" pending subs (last 24 hours). Older rows are
        //     almost certainly abandoned; the weekly cleanup command
        //     handles them and surfacing them would be noise.
        //  2. Suppress when the active sub already covers the same plan
        //     (the pending row is leftover from an aborted retry).
        $pendingSub = Subscription::query()
            ->where('user_id', $user->id)
            ->where('status', 'pending')
            ->where('provider_key', 'razorpay')
            ->where('created_at', '>=', now()->subDay())
            ->latest('id')
            ->first();
        if ($pendingSub && $sub && $sub->plan_id === $pendingSub->plan_id) {
            $pendingSub = null;
        }

        $invoices = Invoice::query()
            ->whereIn(
                'subscription_id',
                Subscription::query()->where('user_id', $user->id)->pluck('id'),
            )
            ->with(['subscription:id,plan_id', 'subscription.plan:id,key,name'])
            ->orderByDesc('created_at')
            ->limit(10)
            ->get()
            ->map(fn (Invoice $i) => [
                'id' => $i->id,
                'provider_key' => $i->provider_key,
                'provider_invoice_id' => $i->provider_invoice_id,
                'amount_cents' => (int) $i->amount_cents,
                'tax_cents' => (int) $i->tax_cents,
                'tax_rate_bps' => $i->tax_rate_bps !== null ? (int) $i->tax_rate_bps : null,
                'tax_label' => $i->tax_label,
                'tax_country' => $i->tax_country,
                'currency' => $i->currency,
                'status' => $i->status,
                'paid_at' => $i->paid_at?->toIso8601String(),
                'hosted_url' => $i->hosted_url,
                'created_at' => $i->created_at?->toIso8601String(),
                'plan_name' => $i->subscription?->plan?->name,
            ]);

        return Inertia::render('Account/Subscription', [
            'currentPlan' => [
                'id' => $currentPlan->id,
                'key' => $currentPlan->key,
                'name' => $currentPlan->name,
                'description' => $currentPlan->description,
                'features' => $currentPlan->features ?? [],
                'monthly_price_cents' => (int) $currentPlan->monthly_price_cents,
                'yearly_price_cents' => (int) $currentPlan->yearly_price_cents,
                'currency' => $currentPlan->currency,
            ],
            'subscription' => $sub ? [
                'id' => $sub->id,
                'status' => $sub->status,
                'period' => $sub->period,
                'provider_key' => $sub->provider_key,
                'current_period_starts_at' => $sub->current_period_starts_at?->toIso8601String(),
                'current_period_ends_at' => $sub->current_period_ends_at?->toIso8601String(),
                'canceled_at' => $sub->canceled_at?->toIso8601String(),
                'cancels_at_period_end' => (bool) $sub->cancels_at_period_end,
            ] : null,
            'invoices' => $invoices,
            'pendingSub' => $pendingSub ? [
                'id' => $pendingSub->id,
                'provider_key' => $pendingSub->provider_key,
                'provider_subscription_id' => $pendingSub->provider_subscription_id,
                'plan_name' => $pendingSub->plan?->name,
                'plan_key' => $pendingSub->plan?->key,
                'period' => $pendingSub->period,
                'created_at' => $pendingSub->created_at?->toIso8601String(),
                'checkout_url' => route('checkout.razorpay.modal', [
                    'subscription' => $pendingSub->provider_subscription_id,
                ]),
            ] : null,
            'pricing_url' => route('pricing'),
            'switch_provider_url' => route('pricing', ['switch_provider' => 1]),
            'aiEnabled' => (bool) \App\Services\Settings::get('ai.enabled', false)
                && filled(\App\Services\Settings::get('ai.api_key'))
                && $request->user()->canUseAi(),
        ]);
    }

    public function cancel(Request $request, BillingManager $manager): JsonResponse
    {
        $data = $request->validate([
            'mode' => ['required', Rule::in(['at_period_end', 'immediate'])],
        ]);

        $sub = $this->resolveOwnedSubscription($request);

        // Already-canceled subs have nothing to do — block at 422 so the UI
        // can show a stale-state message without inventing a fake provider call.
        if ($sub->status === 'canceled' || $sub->canceled_at !== null) {
            return response()->json([
                'message' => 'Subscription is already canceled.',
            ], 422);
        }

        try {
            $manager->provider($sub->provider_key)
                ->cancelSubscription($sub, $data['mode'] === 'at_period_end');
        } catch (Throwable $e) {
            Log::error('account: cancel failed', [
                'sub_id' => $sub->id,
                'provider' => $sub->provider_key,
                'message' => $e->getMessage(),
                'exception' => $e,
            ]);

            return response()->json([
                'message' => 'Could not cancel right now. Please try again or contact support.',
            ], 422);
        }

        AuditLogger::record(
            'account.subscription.cancel',
            $sub,
            "Self-serve cancel (mode: {$data['mode']})",
            ['mode' => $data['mode']],
        );

        return response()->json(['ok' => true]);
    }

    public function resume(Request $request, BillingManager $manager): JsonResponse
    {
        $sub = $this->resolveOwnedSubscription($request);

        // Resume only makes sense on a still-active sub flagged to drop at
        // period end. After period_ends_at the row is `canceled` and the
        // path forward is a fresh checkout, not resume.
        if (! $sub->cancels_at_period_end) {
            return response()->json([
                'message' => 'Subscription is not pending cancellation.',
            ], 422);
        }

        try {
            $manager->provider($sub->provider_key)->resumeSubscription($sub);
        } catch (Throwable $e) {
            Log::error('account: resume failed', [
                'sub_id' => $sub->id,
                'provider' => $sub->provider_key,
                'message' => $e->getMessage(),
                'exception' => $e,
            ]);

            return response()->json([
                'message' => 'Could not resume right now. Please try again or contact support.',
            ], 422);
        }

        AuditLogger::record('account.subscription.resume', $sub, 'Self-serve resume');

        return response()->json(['ok' => true]);
    }

    public function changePlan(Request $request, BillingManager $manager): JsonResponse
    {
        $data = $request->validate([
            'plan_key' => ['required', 'string'],
            'period' => ['sometimes', Rule::in(['monthly', 'yearly'])],
            // Optional — defaults to the existing sub's currency_id (subs
            // rarely change currency mid-flight; that's a cancel-then-
            // resubscribe). The new SPA pricing page passes it explicitly
            // when the user picks a new currency.
            'currency_id' => ['sometimes', 'nullable', 'integer', Rule::exists('currencies', 'id')],
        ]);

        $sub = $this->resolveOwnedSubscription($request);

        // Free isn't a target for self-serve change-plan — dropping to Free
        // is a cancel (covered by /cancel), and rising from Free is a fresh
        // checkout (covered by /pricing). Refuse here so users get a clear
        // pointer instead of a confusing provider-side error.
        if ($data['plan_key'] === 'free' || $sub->plan?->key === 'free') {
            return response()->json([
                'message' => 'Use Cancel to drop to Free, or visit Pricing to subscribe.',
            ], 422);
        }

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

        if ($newPlan === null) {
            return response()->json([
                'message' => 'Unknown plan.',
            ], 422);
        }

        $newPeriod = $data['period'] ?? $sub->period;

        if ($newPlan->id === $sub->plan_id && $newPeriod === $sub->period) {
            return response()->json([
                'message' => 'Already on this plan.',
            ], 422);
        }

        $oldPlan = $sub->plan;
        $oldPeriod = $sub->period;

        $newCurrencyId = $data['currency_id'] ?? $sub->currency_id;

        try {
            $redirectUrl = $manager->provider($sub->provider_key)
                ->changePlan($sub, $newPlan, $newPeriod, $newCurrencyId);
        } catch (Throwable $e) {
            Log::error('account: change_plan failed', [
                'sub_id' => $sub->id,
                'provider' => $sub->provider_key,
                'from_plan' => $oldPlan?->key,
                'to_plan' => $newPlan->key,
                'message' => $e->getMessage(),
                'exception' => $e,
            ]);

            return response()->json([
                'message' => 'Could not switch plan right now. Please try again or contact support.',
            ], 422);
        }

        AuditLogger::record(
            'account.subscription.change_plan',
            $sub,
            "Self-serve plan change: {$oldPlan?->key} ({$oldPeriod}) → {$newPlan->key} ({$newPeriod})",
            [
                'from_plan' => $oldPlan?->key,
                'to_plan' => $newPlan->key,
                'from_period' => $oldPeriod,
                'to_period' => $newPeriod,
                'requires_action' => $redirectUrl !== null,
            ],
        );

        // If the provider returned a redirect URL, the user needs to
        // complete an additional step (3DS challenge, Razorpay re-checkout
        // after PATCH rejection, etc.) before the new plan activates.
        // Email so they can finish the flow even if they close the tab.
        if ($redirectUrl !== null) {
            \App\Services\NotificationDispatcher::paymentActionRequired(
                $sub->fresh(['plan', 'user', 'currency']),
                $redirectUrl,
            );
        }

        return response()->json([
            'ok' => true,
            'redirect_url' => $redirectUrl,
        ]);
    }

    /**
     * Resolves the caller's currently-active subscription. Returning a 404
     * (rather than 403) when none exists doubles as an ownership gate — a
     * user never sees signal that someone else's subscription id exists.
     */
    private function resolveOwnedSubscription(Request $request): Subscription
    {
        $sub = $request->user()->activeSubscription();
        abort_if($sub === null, 404);

        return $sub;
    }
}
