<?php

namespace App\Services\Providers;

use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\Tenant;
use App\Models\User;
use App\Services\OperationRunService;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;
use ReflectionFunction;
use ReflectionMethod;

final class ProviderOperationStartGate
{
    public function __construct(
        private readonly OperationRunService $runs,
        private readonly ProviderOperationRegistry $registry,
    ) {}

    /**
     * @param  array<string, mixed>  $extraContext
     */
    public function start(
        Tenant $tenant,
        ProviderConnection $connection,
        string $operationType,
        callable $dispatcher,
        ?User $initiator = null,
        array $extraContext = [],
    ): ProviderOperationStartResult {
        if ((int) $connection->tenant_id !== (int) $tenant->getKey()) {
            throw new InvalidArgumentException('ProviderConnection does not belong to the given tenant.');
        }

        $definition = $this->registry->get($operationType);

        return DB::transaction(function () use ($tenant, $connection, $operationType, $dispatcher, $initiator, $extraContext, $definition): ProviderOperationStartResult {
            $lockedConnection = ProviderConnection::query()
                ->whereKey($connection->getKey())
                ->lockForUpdate()
                ->firstOrFail();

            $activeRun = OperationRun::query()
                ->where('tenant_id', $tenant->getKey())
                ->active()
                ->where('context->provider_connection_id', (int) $lockedConnection->getKey())
                ->orderByDesc('id')
                ->first();

            if ($activeRun instanceof OperationRun) {
                if ($activeRun->type === $operationType) {
                    return ProviderOperationStartResult::deduped($activeRun);
                }

                return ProviderOperationStartResult::scopeBusy($activeRun);
            }

            $context = array_merge($extraContext, [
                'provider' => $lockedConnection->provider,
                'module' => $definition['module'],
                'provider_connection_id' => (int) $lockedConnection->getKey(),
                'target_scope' => [
                    'entra_tenant_id' => $lockedConnection->entra_tenant_id,
                ],
            ]);

            $run = $this->runs->ensureRunWithIdentity(
                tenant: $tenant,
                type: $operationType,
                identityInputs: [
                    'provider_connection_id' => (int) $lockedConnection->getKey(),
                ],
                context: $context,
                initiator: $initiator,
            );

            $dispatched = false;

            if ($run->wasRecentlyCreated) {
                $this->invokeDispatcher($dispatcher, $run);
                $dispatched = true;
            }

            return ProviderOperationStartResult::started($run, $dispatched);
        });
    }

    private function invokeDispatcher(callable $dispatcher, OperationRun $run): void
    {
        $ref = null;

        if (is_array($dispatcher) && count($dispatcher) === 2) {
            $ref = new ReflectionMethod($dispatcher[0], (string) $dispatcher[1]);
        } elseif (is_string($dispatcher) && str_contains($dispatcher, '::')) {
            [$class, $method] = explode('::', $dispatcher, 2);
            $ref = new ReflectionMethod($class, $method);
        } elseif ($dispatcher instanceof \Closure) {
            $ref = new ReflectionFunction($dispatcher);
        } elseif (is_object($dispatcher) && method_exists($dispatcher, '__invoke')) {
            $ref = new ReflectionMethod($dispatcher, '__invoke');
        }

        if ($ref && $ref->getNumberOfParameters() >= 1) {
            $dispatcher($run);

            return;
        }

        $dispatcher();
    }
}
