<?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).
 * Применяет dual-balance flow:
 *   1. tier-lookup по tenants.delivered_in_month + 1
 *   2. prepaid: balance_leads--, lead_charges (price=0)
 *   3. rub: balance_rub -= price/100 (bcmath), lead_charges (price=tier)
 *   4. INSERT supplier_lead_costs (gap-fix sharing-flow)
 *   5. INSERT balance_transactions (universal ledger движения баланса)
 *
 * Spec: docs/superpowers/specs/2026-05-11-plan4-billing-csv-admin-design.md §3
 */
final class LedgerService
{
    public function __construct(
        private readonly PricingTierResolver $resolver,
        private readonly PricingTierRepository $tiers,
    ) {}

    /**
     * @throws InsufficientBalanceException когда balance_leads=0 AND 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 {
        // 1. tier-resolution для (delivered_in_month + 1)-го лида
        $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;

        // 2. Decide chargeSource (bcmath — НЕ PHP float)
        $source = $this->decideSource($lockedTenant, $priceKopecks);

        // 3. Apply (bcmath для money; raw DB::update — Eloquent decrement() требует float|int,
        // что несовместимо с string-precision arithmetic для копеек/рублей).
        if ($source === 'prepaid') {
            $lockedTenant->decrement('balance_leads', 1);
        } else {
            $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();

        // 4. INSERT lead_charges (always)
        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' => $source === 'prepaid' ? 0 : $priceKopecks,
            'charge_source' => $source,
            'charged_at' => now(),
            'created_at' => now(),
        ]);

        // 5. INSERT balance_transactions (универсальный ledger)
        BalanceTransaction::create([
            'tenant_id' => $lockedTenant->id,
            'type' => BalanceTransaction::TYPE_LEAD_CHARGE,
            'amount_leads' => $source === 'prepaid' ? -1 : 0,
            'amount_rub' => $source === 'rub' ? '-'.bcdiv((string) $priceKopecks, '100', 2) : '0.00',
            'balance_leads_after' => (int) $lockedTenant->balance_leads,
            'balance_rub_after' => (string) $lockedTenant->balance_rub,
            'related_type' => Deal::class,
            'related_id' => $deal->id,
            'created_at' => now(),
        ]);

        // 6. INSERT supplier_lead_costs (gap-fix Plan 2/3 sharing-flow)
        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($source, $tier, $source === 'prepaid' ? 0 : $priceKopecks);
    }

    private function decideSource(Tenant $tenant, int $priceKopecks): string
    {
        if ((int) $tenant->balance_leads >= 1) {
            return 'prepaid';
        }

        // bcmath: balance_rub (DECIMAL string) * 100 ≥ priceKopecks → можем списать rub
        $balanceKopecks = bcmul((string) $tenant->balance_rub, '100', 0);
        if (bccomp($balanceKopecks, (string) $priceKopecks, 0) >= 0) {
            return 'rub';
        }

        throw new InsufficientBalanceException(
            priceKopecks: $priceKopecks,
            balanceRub: (string) $tenant->balance_rub,
            balanceLeads: (int) $tenant->balance_leads,
        );
    }

    /**
     * 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 && in_array($sp->platform, ['B1', 'B2', 'B3'], true)) {
                $supplier = Supplier::where('code', strtolower($sp->platform))->first();
                if ($supplier !== null) {
                    return (int) $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;
        }

        return null;
    }
}
