<?php

declare(strict_types=1);

namespace App\Services\Billing;

use App\Exceptions\Billing\InsufficientBalanceException;
use App\Models\BalanceTransaction;
use App\Models\Deal;
use App\Models\LeadCharge;
use App\Models\Supplier;
use App\Models\SupplierLead;
use App\Models\Tenant;
use App\Repositories\PricingTierRepository;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;

/**
 * Командный сервис биллинга на горячем пути доставки лида.
 *
 * Контракт: вызывается ВНУТРИ открытой DB-транзакции под lockForUpdate(Tenant).
 * Применяет always-rub flow (Billing v2 Spec A — prepaid-лиды ликвидированы):
 *   1. tier-lookup по tenants.delivered_in_month + 1
 *   2. bcmath проверка balance_rub × 100 ≥ priceKopecks; иначе throw
 *   3. balance_rub -= price/100 (bcmath)
 *   4. INSERT lead_charges (charge_source='rub')
 *   5. INSERT balance_transactions (amount_leads=null, amount_rub отрицательное)
 *   6. INSERT supplier_lead_costs (gap-fix sharing-flow)
 *
 * Spec: docs/superpowers/specs/2026-05-23-billing-v2-spec-a-balance-rub-design.md §4.2
 */
final class LedgerService
{
    public function __construct(
        private readonly PricingTierResolver $resolver,
        private readonly PricingTierRepository $tiers,
    ) {}

    /**
     * @throws InsufficientBalanceException когда balance_rub * 100 < priceKopecks.
     *                                      До throw НЕ модифицирует tenant/charges/transactions/costs.
     *
     * @precondition caller wraps in DB::transaction with lockForUpdate($lockedTenant).
     *               Атомарность всех INSERT'ов (lead_charges + balance_transactions + supplier_lead_costs)
     *               обеспечивается этой внешней транзакцией.
     */
    public function chargeForDelivery(
        Tenant $lockedTenant,
        Deal $deal,
        ?SupplierLead $lead = null,
    ): ChargeResult {
        $activeTiers = $this->tiers->activeAt(Carbon::now('Europe/Moscow'));
        $tier = $this->resolver->resolveForCount(
            $activeTiers,
            ($lockedTenant->delivered_in_month ?? 0) + 1
        );
        $priceKopecks = (int) $tier->price_per_lead_kopecks;

        // R-03 (Stage 3 §4.3.1): frozen tenant must not receive new charges even
        // if balance_rub > 0. Throwing here triggers the same auto-pause flow as
        // InsufficientBalance — RouteSupplierLeadJob::handleInsufficientBalance
        // flips projects.is_active=false and queues ZeroBalancePausedMail rate-limited.
        if ($lockedTenant->frozen_by_balance_at !== null) {
            throw new InsufficientBalanceException(
                priceKopecks: $priceKopecks,
                balanceRub: (string) $lockedTenant->balance_rub,
            );
        }

        // bcmath: balance_rub × 100 ≥ priceKopecks — единственный путь списания.
        // Billing v2 Spec A: prepaid-лиды убраны, balance_leads НЕ читается и НЕ изменяется.
        $balanceKopecks = bcmul((string) $lockedTenant->balance_rub, '100', 0);
        if (bccomp($balanceKopecks, (string) $priceKopecks, 0) < 0) {
            throw new InsufficientBalanceException(
                priceKopecks: $priceKopecks,
                balanceRub: (string) $lockedTenant->balance_rub,
            );
        }

        $amountRub = bcdiv((string) $priceKopecks, '100', 2);
        $newBalanceRub = bcsub((string) $lockedTenant->balance_rub, $amountRub, 2);
        DB::table('tenants')
            ->where('id', $lockedTenant->id)
            ->update(['balance_rub' => $newBalanceRub]);

        $lockedTenant->increment('delivered_in_month', 1);
        $lockedTenant->refresh();

        LeadCharge::create([
            'tenant_id' => $lockedTenant->id,
            'deal_id' => $deal->id,
            'deal_received_at' => $deal->received_at,
            'tier_no' => $tier->tier_no,
            'price_per_lead_kopecks' => $priceKopecks,
            'charge_source' => 'rub',
            'charged_at' => now(),
            'created_at' => now(),
        ]);

        BalanceTransaction::create([
            'tenant_id' => $lockedTenant->id,
            'type' => BalanceTransaction::TYPE_LEAD_CHARGE,
            'amount_leads' => null,
            'amount_rub' => '-'.$amountRub,
            'balance_leads_after' => null,
            'balance_rub_after' => (string) $lockedTenant->balance_rub,
            'related_type' => Deal::class,
            'related_id' => $deal->id,
            'created_at' => now(),
        ]);

        if ($lead !== null) {
            $supplierId = $this->resolveSupplierId($lead);
            if ($supplierId !== null) {
                /** @var Supplier $supplier */
                $supplier = Supplier::findOrFail($supplierId);
                DB::table('supplier_lead_costs')->insert([
                    'deal_id' => $deal->id,
                    'received_at' => $deal->received_at,
                    'supplier_id' => $supplierId,
                    'cost_rub' => $supplier->cost_rub,
                    'created_at' => now(),
                ]);
            }
        }

        return new ChargeResult($tier, $priceKopecks);
    }

    /**
     * supplier_id из $lead->supplier_project->platform → suppliers.code (lowercase).
     * supplier_projects не имеет колонки supplier_id (см. schema.sql §2.3, sharing-model);
     * supplier_id выводится из platform (B1/B2/B3) через лookup suppliers WHERE code='b1'/'b2'/'b3'.
     *
     * Fallback: парсим platform из raw_payload['project'] (B1_xxx → 'b1'), если у lead'а нет
     * supplier_project_id (legacy/orphan webhook).
     */
    private function resolveSupplierId(SupplierLead $lead): ?int
    {
        if ($lead->supplier_project_id !== null) {
            $sp = DB::table('supplier_projects')->where('id', $lead->supplier_project_id)->first();
            if ($sp !== null) {
                if (in_array($sp->platform, ['B1', 'B2', 'B3'], true)) {
                    $supplier = Supplier::where('code', strtolower($sp->platform))->first();
                    if ($supplier !== null) {
                        return (int) $supplier->id;
                    }
                }
                if ($sp->platform === 'DIRECT') {
                    $supplier = Supplier::where('code', 'direct')->first();

                    return $supplier?->id;
                }
            }
        }
        // Fallback: парсим platform из raw_payload['project'] (B1_xxx → 'b1')
        $project = trim((string) ($lead->raw_payload['project'] ?? ''));
        if (preg_match('/^(B[123])_/', $project, $m) === 1) {
            $code = strtolower($m[1]);
            $supplier = Supplier::where('code', $code)->first();

            return $supplier?->id;
        }
        // Phase 3: project без B-префикса (и не пустой) → DIRECT.
        if ($project !== '') {
            $supplier = Supplier::where('code', 'direct')->first();

            return $supplier?->id;
        }

        return null;
    }
}
