<?php

namespace App\Http\Controllers;

use App\Models\Currency;
use App\Models\Plan;
use App\Models\PlanPrice;
use App\Services\Billing\BillingManager;
use App\Services\Settings;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;

class PricingController extends Controller
{
    /**
     * Inertia /pricing surface. The frontend rewrite for the new currency-
     * first model is pending in a follow-up task; this controller already
     * ships data in the new shape (see `provider_prices`-mapped output —
     * back-compat with the existing Pricing.tsx during the transition).
     *
     * The new SPA Pricing page should fetch `/pricing.json` instead of
     * relying on this Inertia payload — it returns the canonical
     * (plans, prices, currencies, providers) shape.
     */
    public function show(Request $request, BillingManager $billing): Response
    {
        $payload = $this->buildPricingPayload($request, $billing);

        return Inertia::render('Pricing', [
            'plans' => $payload['plans'],
            'activeProviders' => $payload['activeProviders'],
            'defaultCurrency' => $payload['defaultCurrency'],
            'currentSubscription' => $payload['currentSubscription'],
            // Pre-serialized Product + AggregateOffer JSON-LD. PageSeo
            // emits this alongside the auto Organization + FAQPage
            // schemas. Built server-side because the data is already
            // here and Pricing.tsx fetches plans via /pricing.json on
            // mount — by then SEO crawlers may have already left.
            'pricingJsonLd' => $this->buildPricingJsonLd($payload['plans']->all(), $payload['defaultCurrency']),
        ]);
    }

    /**
     * Build Product + AggregateOffer schema for the /pricing page. Surfaces
     * price + currency range directly in Google SERP rich snippets.
     *
     * AggregateOffer is the right schema shape for "one product with
     * multiple plan offerings" (SaaS pricing tiers). lowPrice = cheapest
     * monthly across plans; highPrice = priciest yearly. Hands Google a
     * single trustable range without it having to interpret per-plan rows.
     *
     * Returns null when no plans are configured — no schema = no rich
     * snippet, which is correct (don't ship "Product with $0 to $0"
     * placeholder data that erodes trust signals).
     *
     * @param  array<int, array<string, mixed>>  $plans  Output of plansShaped from buildPricingPayload.
     */
    private function buildPricingJsonLd(array $plans, ?string $defaultCurrency): ?string
    {
        if (count($plans) === 0) return null;

        $prices = [];
        foreach ($plans as $p) {
            $monthly = (int) ($p['monthly_price_cents'] ?? 0);
            $yearly = (int) ($p['yearly_price_cents'] ?? 0);
            // Skip the Trial / Free plan from price min/max — listing $0
            // as lowPrice on AggregateOffer suggests there's a freemium
            // tier, which is true, but Google may discount Product trust
            // signals when lowPrice is 0. Keep $0 plans visible as
            // individual offers below; just don't anchor the aggregate
            // range at zero.
            if ($monthly > 0) $prices[] = $monthly / 100;
            if ($yearly > 0) $prices[] = $yearly / 100;
        }
        if ($prices === []) return null;

        $currency = $defaultCurrency ?: 'USD';
        $appUrl = rtrim(config('app.url', ''), '/');
        $siteName = (string) (\App\Services\Settings::get('site.name', config('app.name')) ?? config('app.name'));

        $individualOffers = [];
        foreach ($plans as $p) {
            $monthly = (int) ($p['monthly_price_cents'] ?? 0);
            if ($monthly === 0) continue; // Free/trial plans skipped — no Offer for "0 price" placeholder
            $individualOffers[] = [
                '@type' => 'Offer',
                'name' => (string) ($p['name'] ?? ''),
                'description' => (string) ($p['description'] ?? ''),
                'price' => number_format($monthly / 100, 2, '.', ''),
                'priceCurrency' => (string) ($p['currency'] ?? $currency),
                'url' => $appUrl.'/pricing',
                'priceSpecification' => [
                    '@type' => 'UnitPriceSpecification',
                    'price' => number_format($monthly / 100, 2, '.', ''),
                    'priceCurrency' => (string) ($p['currency'] ?? $currency),
                    'referenceQuantity' => [
                        '@type' => 'QuantitativeValue',
                        'value' => 1,
                        'unitCode' => 'MON',
                    ],
                ],
            ];
        }

        return json_encode([
            '@context' => 'https://schema.org',
            '@type' => 'Product',
            'name' => $siteName,
            'description' => 'Subscription plans for '.$siteName,
            'offers' => [
                '@type' => 'AggregateOffer',
                'priceCurrency' => $currency,
                'lowPrice' => number_format(min($prices), 2, '.', ''),
                'highPrice' => number_format(max($prices), 2, '.', ''),
                'offerCount' => count($plans),
                'offers' => $individualOffers,
            ],
        ], JSON_UNESCAPED_SLASHES);
    }

    /**
     * Public JSON endpoint for the new currency-first Pricing SPA. Returns
     * everything the page needs to filter plans by currency client-side.
     *
     * `provider_price_id` is intentionally stripped from the public payload
     * — it's a backend-only concern. Anyone who needs it (admin Billing
     * surface) goes through the gated admin endpoints.
     */
    public function showJson(Request $request, BillingManager $billing): JsonResponse
    {
        $payload = $this->buildPricingPayload($request, $billing, jsonShape: true);

        return response()->json([
            'currencies' => $payload['currencies'],
            'plans' => $payload['plansBare'],
            'prices' => $payload['prices'],
            'activeProviders' => $payload['activeProviders'],
            'defaultCurrency' => $payload['defaultCurrency'],
            'currentSubscription' => $payload['currentSubscription'],
        ]);
    }

    /**
     * Builds the shared payload between the Inertia render path and the JSON
     * API endpoint. Keeping one query graph means Pricing pages can switch
     * between Inertia + JSON without drift in plan/price visibility rules.
     *
     * @return array{
     *   plans: \Illuminate\Support\Collection<int, array<string, mixed>>,
     *   plansBare: array<int, array<string, mixed>>,
     *   prices: array<int, array<string, mixed>>,
     *   currencies: array<int, array<string, mixed>>,
     *   activeProviders: array<int, array<string, string>>,
     *   defaultCurrency: string,
     *   currentSubscription: array{plan_key: ?string, provider_key: string, period: string, currency_id: ?int}|null,
     * }
     */
    private function buildPricingPayload(
        Request $request,
        BillingManager $billing,
        bool $jsonShape = false,
    ): array {
        $allPlans = Plan::query()
            ->where('is_active', true)
            ->orderBy('position')
            ->get([
                'id', 'key', 'name', 'description', 'currency',
                'monthly_price_cents', 'yearly_price_cents', 'features', 'max_seats',
            ]);

        $activeProviders = $billing->activeProviders()
            ->map(fn ($p) => ['key' => $p->key(), 'label' => $p->label()])
            ->values()
            ->all();
        $activeProviderKeys = array_column($activeProviders, 'key');

        // Pull the full price catalog for active providers. Filtering by
        // is_active + non-empty provider_price_id mirrors what
        // createCheckoutSession requires to actually charge — anything
        // missing those is unsubscribable and the Pricing page hides it.
        $priceRowsCollection = PlanPrice::query()
            ->whereIn('plan_id', $allPlans->pluck('id'))
            ->whereIn('gateway_key', $activeProviderKeys ?: [''])
            ->where('is_active', true)
            ->whereNotNull('provider_price_id')
            ->where('provider_price_id', '!=', '')
            ->get();

        // Plans visible on /pricing: free is always shown; paid plans require
        // at least one active price row. Exception: when no billing providers
        // are configured at all, show all plans so fresh installs and the
        // comparison grid render a useful page rather than just the free tier.
        $subscribablePlanIds = $priceRowsCollection->pluck('plan_id')->unique()->all();
        $plans = empty($activeProviderKeys)
            ? $allPlans
            : $allPlans->filter(
                fn (Plan $p) => $p->key === 'free' || in_array($p->id, $subscribablePlanIds, true),
            )->values();

        // Trim providers to those that actually have at least one
        // subscribable price — phantom buttons (Razorpay button when every
        // Razorpay row is inactive) confuse users.
        $providersWithActivePrices = $priceRowsCollection->pluck('gateway_key')->unique()->all();
        $activeProviders = array_values(array_filter(
            $activeProviders,
            fn ($p) => in_array($p['key'], $providersWithActivePrices, true),
        ));

        // Build provider_prices map for back-compat with the existing
        // Pricing.tsx prop shape: { [planId]: { [gateway]: { monthly?: {amount, currency}, yearly?: {amount, currency} } } }
        $currencyCodeById = Currency::query()->pluck('code', 'id')->all();
        $providerPrices = [];
        foreach ($priceRowsCollection as $row) {
            $code = $currencyCodeById[$row->currency_id] ?? null;
            if ($code === null) {
                continue;
            }
            $providerPrices[$row->plan_id][$row->gateway_key][$row->period] = [
                'amount_minor' => (int) $row->amount_minor,
                'currency' => (string) $code,
            ];
        }

        // Currencies that have at least one active subscribable price.
        // Free plan is currency-agnostic so it doesn't pull any currency
        // into the visible set; we add the deployment default last as a
        // fallback so the page is never empty when only free exists.
        $visibleCurrencyIds = $priceRowsCollection->pluck('currency_id')->unique()->all();
        $currencies = Currency::query()
            ->whereIn('id', $visibleCurrencyIds)
            ->orderBy('id')
            ->get(['id', 'code', 'symbol', 'name', 'is_default']);

        $default = Currency::default();
        $defaultCurrency = $default?->code ?? strtoupper((string) (Settings::get('site.default_currency') ?? 'USD'));

        // currentSubscription drives the in-page upgrade/downgrade flow.
        // `?switch_provider=1` opt-out: when set, currentSubscription is
        // suppressed so the page behaves as if the user has no active sub.
        $currentSubscription = null;
        $unlockProvider = $request->boolean('switch_provider');
        $user = $request->user();
        if ($user && ! $unlockProvider) {
            $sub = $user->activeSubscription();
            if ($sub) {
                $currentSubscription = [
                    'plan_key' => $sub->plan?->key,
                    'provider_key' => $sub->provider_key,
                    'period' => $sub->period,
                    'currency_id' => $sub->currency_id,
                ];
            }
        }

        $plansShaped = $plans->map(fn (Plan $p) => [
            'id' => $p->id,
            'key' => $p->key,
            'name' => $p->name,
            'description' => $p->description,
            'currency' => $p->currency,
            'monthly_price_cents' => $p->monthly_price_cents,
            'yearly_price_cents' => $p->yearly_price_cents,
            'features' => $p->features,
            'provider_prices' => $providerPrices[$p->id] ?? new \stdClass(),
        ])->values();

        // Bare plan rows for the JSON endpoint — currency-agnostic. Pricing
        // SPA filters via the prices array.
        $plansBare = $plans->map(fn (Plan $p) => [
            'id' => $p->id,
            'key' => $p->key,
            'name' => $p->name,
            'description' => $p->description,
            'features' => $p->features,
            'project_limit' => is_array($p->features) ? ($p->features['project_limit'] ?? null) : null,
            'can_export' => is_array($p->features) ? ($p->features['can_export'] ?? false) : false,
            'storage_bytes_limit' => is_array($p->features)
                ? (is_int($p->features['storage_bytes_limit'] ?? null)
                    ? $p->features['storage_bytes_limit']
                    : null)
                : null,
            // Team seat cap (owner + admins + editors; viewers are free +
            // unlimited). null = unlimited seats.
            'max_seats' => $p->max_seats === null ? null : (int) $p->max_seats,
        ])->values()->all();

        // Strip provider_price_id — admin-only concern. Public callers
        // route through the checkout endpoint to actually charge.
        $pricesPublic = $priceRowsCollection->map(fn (PlanPrice $row) => [
            'id' => $row->id,
            'plan_id' => $row->plan_id,
            'currency_id' => $row->currency_id,
            'gateway_key' => $row->gateway_key,
            'period' => $row->period,
            'amount_minor' => (int) $row->amount_minor,
            'is_active' => (bool) $row->is_active,
        ])->values()->all();

        return [
            'plans' => $plansShaped,
            'plansBare' => $plansBare,
            'prices' => $pricesPublic,
            'currencies' => $currencies->map(fn (Currency $c) => [
                'id' => $c->id,
                'code' => $c->code,
                'symbol' => $c->symbol,
                'name' => $c->name,
                'is_default' => (bool) $c->is_default,
            ])->values()->all(),
            'activeProviders' => $activeProviders,
            'defaultCurrency' => (string) $defaultCurrency,
            'currentSubscription' => $currentSubscription,
        ];
    }
}
