<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\BalanceTransaction;
use App\Models\PricingTier;
use App\Models\Project;
use App\Models\Tenant;
use App\Models\User;
use App\Repositories\PricingTierRepository;
use App\Services\Billing\BalanceToLeadsConverter;
use App\Services\Billing\BillingTopupService;
use App\Services\Billing\Gateway\PaymentGatewayManager;
use App\Services\Billing\OnlineTopupService;
use App\Services\Billing\RunwayCalculator;
use App\Support\SystemSettings;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;

/**
 * Биллинг тенанта — кошелёк, транзакции, счета, пополнение (audit E1/E3).
 *
 * Все эндпоинты под middleware [auth:sanctum, tenant] (RLS-контекст).
 * Отдельно от TenantChargesController (lead_charges ledger) и
 * AdminBillingController (SaaS-уровневые агрегаты).
 *
 * E1: POST /api/billing/topup — MVP-stub пополнения (без платёжного шлюза).
 * E3: GET wallet/transactions/invoices — данные для BillingView Overview.
 */
class BillingController extends Controller
{
    public function __construct(
        private readonly BillingTopupService $topupService,
    ) {}

    /**
     * POST /api/billing/topup — пополнить рублёвый баланс.
     *
     * Развилка: если флаг billing_yookassa_enabled ВКЛ — создаём платёж через
     * шлюз и возвращаем confirmation_url (баланс не меняется до webhook).
     * Если ВЫКЛ — MVP-stub мгновенного зачисления (текущее прод-поведение до Б-1).
     */
    public function topup(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'amount_rub' => ['required', 'numeric', 'min:100', 'max:1000000', 'decimal:0,2'],
        ]);

        /** @var User $user */
        $user = $request->user();
        $amountRub = bcadd((string) $validated['amount_rub'], '0', 2);

        // Развилка: реальный шлюз (флаг ВКЛ) ИЛИ мгновенная заглушка (флаг ВЫКЛ).
        if (SystemSettings::bool('billing_yookassa_enabled')) {
            $manager = app(PaymentGatewayManager::class);
            $gateway = $manager->activeGateway();
            if ($gateway === null) {
                return response()->json(['message' => 'Платёжный шлюз не настроен.'], 503);
            }

            $returnUrl = rtrim((string) config('app.url'), '/').'/billing?topup=return';
            $result = app(OnlineTopupService::class)->start(
                (int) $user->tenant_id, $amountRub, $gateway, $returnUrl, (int) $user->id
            );

            return response()->json(['confirmation_url' => $result->confirmationUrl], 201);
        }

        // Заглушка (текущее прод-поведение до Б-1): мгновенное зачисление.
        $tx = $this->topupService->topup((int) $user->tenant_id, $amountRub, (int) $user->id);

        return response()->json([
            'transaction' => [
                'id' => $tx->id,
                'type' => $tx->type,
                'amount_rub' => $tx->amount_rub,
                'balance_rub_after' => $tx->balance_rub_after,
                'created_at' => $tx->created_at,
            ],
            'balance_rub' => $tx->balance_rub_after,
        ], 201);
    }

    /**
     * GET /api/billing/wallet — единый ₽-баланс + рассчитанные «≈ N лидов» + 7-ступенчатый превью.
     *
     * Billing v2 Spec A: `balance_leads` ушёл из ответа; конверсия ₽ → лиды
     * считается на лету через BalanceToLeadsConverter (точный расчёт по
     * ступеням, не «по текущей»). Тариф унифицирован до name+features.
     */
    public function wallet(Request $request): JsonResponse
    {
        /** @var User $user */
        $user = $request->user();
        /** @var Tenant $tenant */
        $tenant = Tenant::query()->with('tariff')->findOrFail((int) $user->tenant_id);

        $activeTiers = app(PricingTierRepository::class)->activeAt(Carbon::now('Europe/Moscow'));
        $conversion = app(BalanceToLeadsConverter::class)->convert(
            (string) $tenant->balance_rub,
            (int) ($tenant->delivered_in_month ?? 0),
            $activeTiers,
        );

        $tiersPreview = $activeTiers
            ->sortBy('tier_no')
            ->values()
            ->map(static fn ($t) => [
                'tier_no' => (int) $t->tier_no,
                'leads_in_tier' => $t->leads_in_tier === null ? null : (int) $t->leads_in_tier,
                'price_rub' => bcdiv((string) $t->price_per_lead_kopecks, '100', 2),
            ])
            ->all();

        return response()->json([
            'balance_rub' => $tenant->balance_rub,
            'affordable_leads' => $conversion['leads'],
            'current_tier' => $conversion['current_tier'],
            'next_tier' => $conversion['next_tier'],
            'delivered_in_month' => (int) ($tenant->delivered_in_month ?? 0),
            'runway_days' => $this->runwayDays($tenant, $conversion['leads']),
            'tiers_preview' => $tiersPreview,
            'tariff' => $tenant->tariff === null ? null : [
                'code' => $tenant->tariff->code,
                'name' => $tenant->tariff->name,
                'features' => $tenant->tariff->features ?? [],
            ],
        ]);
    }

    /**
     * GET /api/billing/balance-status — лёгкий статус баланса для UI префлайта
     * (Billing v2 Spec C §3.6). Питает глобальный баннер заморозки
     * (BalanceFrozenBanner: frozen_by_balance_at + дефицит) и индикатор ёмкости
     * (BalanceCapacityIndicator: balance / capacity / required). Грузится в
     * AppLayout на всех страницах, поэтому без tiers_preview и истории.
     */
    public function balanceStatus(Request $request): JsonResponse
    {
        /** @var User $user */
        $user = $request->user();
        /** @var Tenant $tenant */
        $tenant = Tenant::query()->findOrFail((int) $user->tenant_id);

        $activeTiers = app(PricingTierRepository::class)->activeAt(Carbon::now('Europe/Moscow'));
        $deliveredInMonth = (int) ($tenant->delivered_in_month ?? 0);

        $capacityLeads = (int) app(BalanceToLeadsConverter::class)->convert(
            (string) $tenant->balance_rub,
            $deliveredInMonth,
            $activeTiers,
        )['leads'];

        // Требуемые лиды/день — сумма лимитов активных не-заблокированных проектов
        // (та же выборка, что в ProjectController preflight).
        $requiredLeads = (int) Project::query()
            ->where('tenant_id', $tenant->id)
            ->where('is_active', true)
            ->whereNull('preflight_blocked_at')
            ->sum('daily_limit_target');

        $deficitLeads = max(0, $requiredLeads - $capacityLeads);
        $deficitRub = '0.00';
        if ($deficitLeads > 0) {
            $needed = $this->minBalanceForLeads($requiredLeads, $deliveredInMonth, $activeTiers);
            $deficitRub = bcsub($needed, (string) $tenant->balance_rub, 2);
            if (bccomp($deficitRub, '0', 2) < 0) {
                $deficitRub = '0.00';
            }
        }

        return response()->json([
            'frozen_by_balance_at' => $tenant->frozen_by_balance_at?->toISOString(),
            'balance_rub' => (string) $tenant->balance_rub,
            'capacity_leads' => $capacityLeads,
            'required_leads_per_day' => $requiredLeads,
            'deficit_leads' => $deficitLeads,
            'deficit_rub' => $deficitRub,
        ]);
    }

    /**
     * Минимальный баланс (₽, scale 2), чтобы позволить себе $leads лидов при уже
     * доставленных $deliveredInMonth в этом месяце — сумма цен ступеней по позициям
     * [delivered .. delivered+leads-1]. Зеркалит логику BalanceToLeadsConverter.
     *
     * @param  Collection<int, PricingTier>  $tiers
     */
    private function minBalanceForLeads(int $leads, int $deliveredInMonth, $tiers): string
    {
        if ($leads <= 0) {
            return '0.00';
        }

        $sorted = $tiers
            ->filter(fn ($t) => (bool) $t->is_active)
            ->sortBy('tier_no')
            ->values();

        $kopecks = '0';
        $remaining = $leads;
        $cumulative = 0;            // позиции [0..cumulative) пройдены предыдущими ступенями
        $position = $deliveredInMonth;

        foreach ($sorted as $tier) {
            $unlimited = $tier->leads_in_tier === null;
            $tierEnd = $unlimited ? PHP_INT_MAX : $cumulative + (int) $tier->leads_in_tier;

            $slotsInTier = max(0, $tierEnd - max($cumulative, $position));
            if ($slotsInTier > 0) {
                $take = min($remaining, $slotsInTier);
                $kopecks = bcadd($kopecks, bcmul((string) (int) $tier->price_per_lead_kopecks, (string) $take, 0), 0);
                $remaining -= $take;
                $position += $take;
            }

            if ($remaining <= 0 || $unlimited) {
                break;
            }
            $cumulative = $tierEnd;
        }

        return bcdiv($kopecks, '100', 2);
    }

    /**
     * GET /api/billing/transactions?type=topup|lead_charge|migration&page=N
     * — пагинированная история balance_transactions тенанта (20/страница).
     *
     * Billing v2 Spec A: 'refund' убран из whitelist (возвраты не реализуются);
     * 'migration' добавлен (тип одноразовой конвертации balance_leads → balance_rub).
     * Поле display_amount_rub в каждой строке — UI-показ суммы; для исторических
     * prepaid lead_charge (amount_rub='0.00') возвращается '0.00' для маркера
     * «бесплатное списание».
     */
    public function transactions(Request $request): JsonResponse
    {
        /** @var User $user */
        $user = $request->user();
        $tenantId = (int) $user->tenant_id;

        // Явный tenant_id фильтр — defense-in-depth поверх RLS (тесты идут
        // под superuser BYPASSRLS; паттерн TenantChargesController).
        $query = BalanceTransaction::query()
            ->where('tenant_id', $tenantId)
            ->orderBy('created_at', 'desc')
            ->orderBy('id', 'desc');

        $type = $request->query('type');
        if (is_string($type) && in_array($type, ['topup', 'lead_charge', 'migration'], true)) {
            $query->where('type', $type);
        }

        $page = $query->paginate(20);

        return response()->json([
            'data' => array_map(static function (BalanceTransaction $tx): array {
                // Historic prepaid rows: type=lead_charge AND amount_rub=='0.00' (deduction в leads).
                // display_amount_rub возвращает явное '0.00' для UI-маркера «бесплатное списание»,
                // несмотря на то что значение совпадает с amount_rub.
                $displayAmountRub = (string) $tx->amount_rub;
                if ($tx->type === BalanceTransaction::TYPE_LEAD_CHARGE
                    && bccomp((string) $tx->amount_rub, '0', 2) === 0) {
                    $displayAmountRub = '0.00';
                }

                return [
                    'id' => $tx->id,
                    'code' => 'TX-'.$tx->id,
                    'type' => $tx->type,
                    'description' => $tx->description,
                    'amount_rub' => $tx->amount_rub,
                    'amount_leads' => $tx->amount_leads,
                    'balance_rub_after' => $tx->balance_rub_after,
                    'display_amount_rub' => $displayAmountRub,
                    'created_at' => $tx->created_at,
                ];
            }, $page->items()),
            'meta' => [
                'current_page' => $page->currentPage(),
                'last_page' => $page->lastPage(),
                'total' => $page->total(),
                'per_page' => $page->perPage(),
            ],
        ]);
    }

    /**
     * GET /api/billing/invoices — счета тенанта (saas_invoices).
     *
     * Real-but-empty на MVP: saas_invoices.legal_entity_id NOT NULL требует
     * зарегистрированного юр-лица (блокируется Б-1). Read-only выборка через
     * DB::table — без Eloquent-модели (паттерн AdminBillingController).
     */
    public function invoices(Request $request): JsonResponse
    {
        /** @var User $user */
        $user = $request->user();
        $tenantId = (int) $user->tenant_id;

        $rows = DB::table('saas_invoices')
            ->where('tenant_id', $tenantId)
            ->orderBy('issued_at', 'desc')
            ->get(['id', 'invoice_number', 'amount_total', 'status', 'issued_at', 'pdf_path']);

        return response()->json([
            'data' => $rows->map(static fn (\stdClass $r): array => [
                'id' => $r->id,
                'invoice_number' => $r->invoice_number,
                'amount_total' => $r->amount_total,
                'status' => $r->status,
                'issued_at' => $r->issued_at,
                'has_pdf' => $r->pdf_path !== null,
            ])->all(),
        ]);
    }

    /**
     * Прогноз «на сколько дней хватит affordable_leads» — оценочный UX-показатель.
     *
     * Billing v2 Spec A: считаем по affordable_leads (выход BalanceToLeadsConverter)
     * делённому на среднюю скорость списания за 30 дней (count(lead_charges)/30).
     * Раньше формула была balance_rub / per-day-rub-spend — после унификации
     * единицы измерения «лиды» более показательны и устраняют дрейф между
     * рублёвой шапкой и тарифной ступенью.
     *
     * - affordable_leads ≤ 0 → 0 (тенант не может купить ни одного лида).
     * - leadsLast30Days = 0 → null (нет истории, не от чего считать).
     * - иначе → floor(affordable_leads / (leadsLast30Days / 30)).
     */
    private function runwayDays(Tenant $tenant, int $affordableLeads): ?int
    {
        // F3 (17.06.2026): единый источник расчёта — RunwayCalculator (общий с дашбордом),
        // чтобы прогноз «хватит на дни» не расходился между биллингом и дашбордом.
        return app(RunwayCalculator::class)->daysLeft((int) $tenant->id, $affordableLeads);
    }
}
