<?php

declare(strict_types=1);

namespace App\Services;

use App\Exceptions\PlanLimitExceededException;
use App\Models\Company;
use App\Models\Subscription;
use Closure;
use Illuminate\Support\Facades\DB;

/**
 * Aplica los límites del plan de una empresa en los puntos de creación
 * (panel /admin y API PWA). La lógica de "cuántos caben" vive en Company/
 * Subscription; este servicio la convierte en assert que lanza
 * PlanLimitExceededException con un mensaje accionable cuando se excede.
 *
 * NO se aplica a flujos internos/sistémicos (registro de trial, renovaciones,
 * seeders): esos crean registros por lógica de negocio, no por acción directa
 * del usuario contra su cupo.
 */
class PlanLimitService
{
    public function assertCanAddUser(Company $company): void
    {
        $subscription = $this->subscriptionOrFail($company);

        if (! $company->canAddUser()) {
            throw PlanLimitExceededException::for('usuarios', $subscription->getMaxUsers(), $subscription->plan?->name);
        }
    }

    public function assertCanAddCollector(Company $company): void
    {
        $subscription = $this->subscriptionOrFail($company);

        if (! $company->canAddCollector()) {
            throw PlanLimitExceededException::for('cobradores', $subscription->getMaxCollectors(), $subscription->plan?->name);
        }
    }

    public function assertCanAddClient(Company $company): void
    {
        $subscription = $this->subscriptionOrFail($company);

        if (! $company->canAddClient()) {
            throw PlanLimitExceededException::for('clientes', $subscription->getMaxClients(), $subscription->plan?->name);
        }
    }

    /**
     * Bloquea por conteo de créditos activos y, si se pasa el monto, por
     * volumen mensual desembolsado.
     */
    public function assertCanCreateCredit(Company $company, float $amount = 0.0): void
    {
        $subscription = $this->subscriptionOrFail($company);

        if (! $company->canCreateCredit()) {
            throw PlanLimitExceededException::for('créditos activos', $subscription->getMaxActiveCredits(), $subscription->plan?->name);
        }

        if ($amount > 0.0 && ! $company->canDisburse($amount)) {
            throw PlanLimitExceededException::forMonthlyVolume($subscription->getMaxMonthlyVolume(), $subscription->plan?->name);
        }
    }

    /**
     * Ejecuta "chequear cupo + crear" de forma ATÓMICA y serializada por empresa (#169).
     *
     * Sin esto, `assertCanX()` cuenta y luego el controlador crea en pasos separados:
     * dos requests concurrentes (p.ej. la sincronización offline de la PWA disparando
     * varias creaciones a la vez) pasan ambos el chequeo `count < max` y dejan a la
     * empresa 1+ sobre el tope. Aquí se bloquea la fila de la suscripción con
     * `lockForUpdate` dentro de una transacción: el segundo request espera a que el
     * primero haga commit y entonces vuelve a contar (ya viendo el registro nuevo).
     *
     * `$operation` debe hacer el assert Y la creación (para que ambos ocurran bajo el
     * lock). Cualquier `PlanLimitExceededException` que lance revierte la transacción.
     *
     * @template T
     *
     * @param  Closure(): T  $operation
     * @return T
     */
    public function runGuardedCreate(Company $company, Closure $operation): mixed
    {
        return DB::transaction(function () use ($company, $operation) {
            Subscription::query()
                ->where('company_id', $company->id)
                ->lockForUpdate()
                ->first();

            return $operation();
        });
    }

    private function subscriptionOrFail(Company $company): Subscription
    {
        $subscription = $company->currentSubscription;

        if (! $subscription) {
            throw PlanLimitExceededException::noActiveSubscription();
        }

        return $subscription;
    }
}
