<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\AuditLog;
use App\Models\Invoice;
use App\Models\PaymentProvider;
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\Validation\Rule;
use Inertia\Inertia;
use Inertia\Response;
use Throwable;

class SubscriptionsController extends Controller
{
    public function index(Request $request): Response
    {
        $filters = [
            'q' => trim((string) $request->query('q', '')),
            'status' => (string) $request->query('status', 'all'),
            'plan_key' => $request->query('plan_key') ?: null,
            'provider_key' => $request->query('provider_key') ?: null,
        ];

        $query = Subscription::query()
            ->with(['user:id,email,name', 'plan:id,key,name'])
            ->orderByDesc('created_at');

        if ($filters['q'] !== '') {
            $needle = $filters['q'];
            $query->whereHas('user', fn ($q) => $q->where('email', 'like', "%{$needle}%"));
        }

        if (in_array($filters['status'], ['active', 'trialing', 'past_due', 'canceled'], true)) {
            $query->where('status', $filters['status']);
        }

        if ($filters['plan_key']) {
            $planId = Plan::query()->where('key', $filters['plan_key'])->value('id');
            // Forcing -1 when an unknown plan key was sent ensures the result
            // set is empty instead of silently returning every row.
            $query->where('plan_id', $planId ?: -1);
        }

        if ($filters['provider_key']) {
            $query->where('provider_key', $filters['provider_key']);
        }

        $paginator = $query->paginate(50)->withQueryString();

        $rows = $paginator->through(fn (Subscription $s) => [
            'id' => $s->id,
            'user' => [
                'id' => $s->user_id,
                // Live email when the user row is intact; snapshot when the
                // account was purged (user_id null) so the row stays
                // attributable on the admin view.
                'email' => $s->user?->email ?? $s->user_email_snapshot,
                'name' => $s->user?->name,
                'deleted' => $s->user_id === null,
            ],
            'plan' => [
                'id' => $s->plan_id,
                'key' => $s->plan?->key,
                'name' => $s->plan?->name,
            ],
            'provider_key' => $s->provider_key,
            'provider_subscription_id' => $s->provider_subscription_id,
            'period' => $s->period,
            'status' => $s->status,
            'current_period_ends_at' => $s->current_period_ends_at?->toIso8601String(),
            'canceled_at' => $s->canceled_at?->toIso8601String(),
            'cancels_at_period_end' => (bool) $s->cancels_at_period_end,
            'is_test' => (bool) $s->is_test,
            'created_at' => $s->created_at?->toIso8601String(),
        ]);

        return Inertia::render('Admin/Subscriptions/Index', [
            'subscriptions' => $rows,
            'filters' => $filters,
            'plans' => Plan::orderBy('position')->get(['id', 'key', 'name']),
            'providers' => PaymentProvider::orderBy('position')->get(['key', 'label']),
            // Drives the "Clear test data" danger-zone tile. Counts are
            // unfiltered (we never auto-hide test rows from the list — the
            // admin needs to see them while testing); the tile renders only
            // when at least one test row exists. is_test lives only on
            // subscriptions; invoices inherit via the FK relationship.
            'test_data' => [
                'subscription_count' => Subscription::query()->where('is_test', true)->count(),
                'invoice_count' => Invoice::query()
                    ->whereHas('subscription', fn ($q) => $q->where('is_test', true))
                    ->count(),
                'currently_logged_in_user_id' => $request->user()?->id,
                'currently_logged_in_user_has_test_sub' => $request->user()
                    ? Subscription::query()
                        ->where('user_id', $request->user()->id)
                        ->where('is_test', true)
                        ->exists()
                    : false,
            ],
        ]);
    }

    /**
     * Bulk-delete every is_test=true subscription + invoice. Used after
     * the admin finishes their pre-launch test checkout pass; the production
     * dashboard then shows real revenue, not test cruft.
     *
     * For each test subscription:
     *   1. Best-effort cancel on the provider side via BillingProvider::cancelSubscription
     *      (so Stripe/Razorpay's record matches local — even though their
     *      test data auto-prunes, leaving orphans is sloppy).
     *   2. Delete the local sub + its invoices.
     *
     * Provider cancel failures are collected and reported but don't block
     * the local delete — the alternative is leaving rows that admin
     * specifically asked to nuke. The audit log entry captures the full
     * outcome.
     *
     * NOTE: this DELETES the row entirely (not a soft delete). Subscriptions
     * + invoices are intentionally hard-deleted here because the rows have
     * no real-world counterpart — they came from sandbox test mode and
     * exist only to pollute production metrics if left around. The
     * `subscriptions.user_id=nullOnDelete` cascade on user purge does
     * preserve LIVE rows for accounting.
     */
    public function clearTestData(Request $request, BillingManager $manager): JsonResponse
    {
        $testSubs = Subscription::query()->where('is_test', true)->get();

        $providerCanceledCount = 0;
        $providerCancelErrors = [];

        foreach ($testSubs as $sub) {
            $provider = $manager->provider($sub->provider_key);
            if (! $provider) {
                continue;
            }
            try {
                $provider->cancelSubscription($sub, atPeriodEnd: false);
                $providerCanceledCount++;
            } catch (Throwable $e) {
                $providerCancelErrors[] = [
                    'subscription_id' => $sub->id,
                    'provider_key' => $sub->provider_key,
                    'message' => $e->getMessage(),
                ];
            }
        }

        // Delete invoices first via FK (is_test lives only on subscriptions
        // — invoices inherit). Doing it in this order keeps the foreign
        // keys consistent at every step.
        $testSubIds = $testSubs->pluck('id')->all();
        $deletedInvoices = $testSubIds === []
            ? 0
            : Invoice::query()->whereIn('subscription_id', $testSubIds)->delete();
        $deletedSubs = Subscription::query()->where('is_test', true)->delete();

        AuditLogger::record(
            'subscriptions.clearTestData',
            null,
            "Deleted {$deletedSubs} test subscription(s) and {$deletedInvoices} test invoice(s)",
            [
                'deleted_subscriptions' => $deletedSubs,
                'deleted_invoices' => $deletedInvoices,
                'provider_canceled' => $providerCanceledCount,
                'provider_cancel_errors' => count($providerCancelErrors),
            ],
        );

        return response()->json([
            'ok' => true,
            'deleted_subscriptions' => $deletedSubs,
            'deleted_invoices' => $deletedInvoices,
            'provider_canceled' => $providerCanceledCount,
            'provider_cancel_errors' => $providerCancelErrors,
        ]);
    }

    public function show(Request $request, Subscription $subscription): Response
    {
        $subscription->load(['user:id,email,name', 'plan:id,key,name']);

        $invoices = Invoice::query()
            ->where('subscription_id', $subscription->id)
            ->orderByDesc('created_at')
            ->limit(20)
            ->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,
                'currency' => $i->currency,
                'status' => $i->status,
                'paid_at' => $i->paid_at?->toIso8601String(),
                'hosted_url' => $i->hosted_url,
                'created_at' => $i->created_at?->toIso8601String(),
            ]);

        $auditLogsTotal = (int) AuditLog::query()
            ->where('target_type', 'Subscription')
            ->where('target_id', $subscription->id)
            ->count();
        $auditLogs = AuditLog::query()
            ->where('target_type', 'Subscription')
            ->where('target_id', $subscription->id)
            ->orderByDesc('created_at')
            ->with('actor:id,name,email')
            ->limit(20)
            ->get()
            ->map(fn (AuditLog $log) => [
                'id' => $log->id,
                'action' => $log->action,
                'summary' => $log->summary,
                'context' => $log->context,
                'created_at' => $log->created_at?->toIso8601String(),
                'actor' => $log->actor ? [
                    'id' => $log->actor->id,
                    'name' => $log->actor->name,
                    'email' => $log->actor->email,
                ] : null,
            ]);

        return Inertia::render('Admin/Subscriptions/Show', [
            'subscription' => [
                'id' => $subscription->id,
                'user' => [
                    'id' => $subscription->user_id,
                    'email' => $subscription->user?->email ?? $subscription->user_email_snapshot,
                    'name' => $subscription->user?->name,
                    'deleted' => $subscription->user_id === null,
                ],
                'plan' => [
                    'id' => $subscription->plan_id,
                    'key' => $subscription->plan?->key,
                    'name' => $subscription->plan?->name,
                ],
                'provider_key' => $subscription->provider_key,
                'provider_subscription_id' => $subscription->provider_subscription_id,
                'period' => $subscription->period,
                'status' => $subscription->status,
                'trial_ends_at' => $subscription->trial_ends_at?->toIso8601String(),
                'current_period_starts_at' => $subscription->current_period_starts_at?->toIso8601String(),
                'current_period_ends_at' => $subscription->current_period_ends_at?->toIso8601String(),
                'canceled_at' => $subscription->canceled_at?->toIso8601String(),
                'cancels_at_period_end' => (bool) $subscription->cancels_at_period_end,
                'created_at' => $subscription->created_at?->toIso8601String(),
                'seats' => (int) $subscription->seats,
                // Persistent recovery URL surfaced for pending Razorpay
                // subs — admin sees the checkout link the customer needs
                // to complete payment at, always (not just transiently
                // after a change-plan action). null otherwise.
                'pending_checkout_url' => (
                    $subscription->status === 'pending'
                    && $subscription->provider_key === 'razorpay'
                    && $subscription->provider_subscription_id
                )
                    ? route('checkout.razorpay.modal', [
                        'subscription' => $subscription->provider_subscription_id,
                    ])
                    : null,
            ],
            'invoices' => $invoices,
            'auditLogs' => $auditLogs,
            'auditLogsTotal' => $auditLogsTotal,
            'plans' => Plan::orderBy('position')->get(['id', 'key', 'name']),
        ]);
    }

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

        try {
            $manager->provider($subscription->provider_key)
                ->cancelSubscription($subscription, $data['mode'] === 'at_period_end');
        } catch (Throwable $e) {
            return response()->json([
                'message' => 'Provider call failed: '.$e->getMessage(),
            ], 422);
        }

        AuditLogger::record(
            'subscription.cancel',
            $subscription,
            "Cancelled (mode: {$data['mode']})",
            ['mode' => $data['mode']],
        );

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

    public function resume(Request $request, Subscription $subscription, BillingManager $manager): JsonResponse
    {
        try {
            $manager->provider($subscription->provider_key)
                ->resumeSubscription($subscription);
        } catch (Throwable $e) {
            return response()->json([
                'message' => 'Provider call failed: '.$e->getMessage(),
            ], 422);
        }

        AuditLogger::record('subscription.resume', $subscription, 'Resumed');

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

    public function changePlan(Request $request, Subscription $subscription, BillingManager $manager): JsonResponse
    {
        $data = $request->validate([
            'plan_key' => ['required', 'string', Rule::in(Plan::query()->pluck('key')->all())],
        ]);

        // Pre-flight: change-plan only makes sense on a sub that's actually
        // running. On a pending/canceled/incomplete sub, providers reject
        // the update — Stripe says "subscription is incomplete," Razorpay
        // says "Can't update Subscription when Subscription is not in
        // Authenticated or Active state" then our fallback would helpfully
        // (destructively) create yet another pending sub. Refuse here so
        // admin sees a clear error instead of stacking ghost subs.
        $changeable = ['active', 'trialing', 'past_due', 'authenticated'];
        if (! in_array($subscription->status, $changeable, true)) {
            return response()->json([
                'message' => "Can't change plan on a [{$subscription->status}] subscription. "
                    .'Cancel it if no longer needed and have the customer subscribe afresh.',
            ], 422);
        }

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

        try {
            $redirectUrl = $manager->provider($subscription->provider_key)
                ->changePlan($subscription, $newPlan, $subscription->period);
        } catch (Throwable $e) {
            return response()->json([
                'message' => 'Provider call failed: '.$e->getMessage(),
            ], 422);
        }

        AuditLogger::record(
            'subscription.change_plan',
            $subscription,
            "Plan changed: {$oldPlan?->key} → {$newPlan->key}"
                .($redirectUrl ? ' (customer auth required)' : ''),
            [
                'from_plan' => $oldPlan?->key,
                'to_plan' => $newPlan->key,
                'requires_action' => $redirectUrl !== null,
            ],
        );

        return response()->json([
            'ok' => true,
            'subscription' => $subscription->fresh(['plan']),
            // Provider returned a hosted-page URL — Stripe 3DS / Razorpay
            // re-checkout. Admin can't authenticate on the customer's
            // behalf; surface the URL so they can copy/email it. Local sub
            // is in pending state until the customer completes auth.
            'redirect_url' => $redirectUrl,
        ]);
    }
}
