<?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 App\Support\Providers\ProviderNextStepsRegistry;
use App\Support\Providers\ProviderReasonCodes;
use App\Support\Verification\BlockedVerificationReportFactory;
use App\Support\Verification\StaleQueuedVerificationReportFactory;
use App\Support\Verification\VerificationReportWriter;
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,
        private readonly ProviderConnectionResolver $resolver,
        private readonly ProviderNextStepsRegistry $nextStepsRegistry,
    ) {}

    /**
     * @param  array<string, mixed>  $extraContext
     */
    public function start(
        Tenant $tenant,
        ?ProviderConnection $connection,
        string $operationType,
        callable $dispatcher,
        ?User $initiator = null,
        array $extraContext = [],
    ): ProviderOperationStartResult {
        $definition = $this->registry->get($operationType);
        $resolution = $connection instanceof ProviderConnection
            ? $this->resolver->validateConnection($tenant, (string) $definition['provider'], $connection)
            : $this->resolver->resolveDefault($tenant, (string) $definition['provider']);

        if (! $resolution->resolved || ! $resolution->connection instanceof ProviderConnection) {
            return $this->startBlocked(
                tenant: $tenant,
                operationType: $operationType,
                provider: (string) $definition['provider'],
                module: (string) $definition['module'],
                reasonCode: $resolution->effectiveReasonCode(),
                extensionReasonCode: $resolution->extensionReasonCode,
                reasonMessage: $resolution->message,
                connection: $resolution->connection ?? $connection,
                initiator: $initiator,
                extraContext: $extraContext,
            );
        }

        return DB::transaction(function () use ($tenant, $operationType, $dispatcher, $initiator, $extraContext, $definition, $resolution): ProviderOperationStartResult {
            $connection = $resolution->connection;

            if (! $connection instanceof ProviderConnection) {
                throw new InvalidArgumentException('Resolved provider connection is missing.');
            }

            $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')
                ->lockForUpdate()
                ->first();

            if ($activeRun instanceof OperationRun) {
                if ($this->runs->isStaleQueuedRun($activeRun)) {
                    $this->runs->failStaleQueuedRun($activeRun);

                    if ($activeRun->type === 'provider.connection.check') {
                        VerificationReportWriter::write(
                            run: $activeRun,
                            checks: StaleQueuedVerificationReportFactory::checks($activeRun),
                            identity: StaleQueuedVerificationReportFactory::identity($activeRun),
                        );

                        $activeRun->refresh();
                    }

                    $activeRun = null;
                }
            }

            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);
        });
    }

    /**
     * @param  array<string, mixed>  $extraContext
     */
    private function startBlocked(
        Tenant $tenant,
        string $operationType,
        string $provider,
        string $module,
        string $reasonCode,
        ?string $extensionReasonCode = null,
        ?string $reasonMessage = null,
        ?ProviderConnection $connection = null,
        ?User $initiator = null,
        array $extraContext = [],
    ): ProviderOperationStartResult {
        $context = array_merge($extraContext, [
            'provider' => $provider,
            'module' => $module,
            'target_scope' => [
                'entra_tenant_id' => $tenant->graphTenantId(),
            ],
        ]);

        $identityInputs = [
            'provider' => $provider,
            'reason_code' => $reasonCode,
        ];

        if (is_string($extensionReasonCode) && $extensionReasonCode !== '') {
            $context['reason_code_extension'] = $extensionReasonCode;
            $identityInputs['reason_code_extension'] = $extensionReasonCode;
        }

        if ($connection instanceof ProviderConnection) {
            $context['provider_connection_id'] = (int) $connection->getKey();
            $identityInputs['provider_connection_id'] = (int) $connection->getKey();
        }

        $run = $this->runs->ensureRunWithIdentity(
            tenant: $tenant,
            type: $operationType,
            identityInputs: $identityInputs,
            context: $context,
            initiator: $initiator,
        );

        $run = $this->runs->finalizeBlockedRun(
            $run,
            reasonCode: ProviderReasonCodes::isKnown($reasonCode) ? $reasonCode : ProviderReasonCodes::UnknownError,
            nextSteps: $this->nextStepsRegistry->forReason($tenant, $reasonCode, $connection),
            message: $reasonMessage,
        );

        if ($operationType === 'provider.connection.check') {
            VerificationReportWriter::write(
                run: $run,
                checks: BlockedVerificationReportFactory::checks($run),
                identity: BlockedVerificationReportFactory::identity($run),
            );

            $run->refresh();
        }

        return ProviderOperationStartResult::blocked($run);
    }

    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();
    }
}
