<?php

declare(strict_types=1);

namespace App\Services\Onboarding;

use App\Exceptions\Onboarding\OnboardingDraftConflictException;
use App\Exceptions\Onboarding\OnboardingDraftImmutableException;
use App\Models\ManagedEnvironmentOnboardingSession;
use App\Models\User;
use App\Models\Workspace;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class OnboardingDraftMutationService
{
    public function __construct(
        private readonly OnboardingLifecycleService $lifecycleService,
    ) {}

    /**
     * @param  callable(ManagedEnvironmentOnboardingSession):void  $mutator
     */
    public function createOrResume(
        Workspace $workspace,
        User $actor,
        string $entraTenantId,
        callable $mutator,
        ?ManagedEnvironmentOnboardingSession $preferredDraft = null,
        ?int $expectedVersion = null,
        bool $incrementVersion = true,
        ?bool &$wasCreated = null,
    ): ManagedEnvironmentOnboardingSession {
        return DB::transaction(function () use ($workspace, $actor, $entraTenantId, $mutator, $preferredDraft, $expectedVersion, $incrementVersion, &$wasCreated): ManagedEnvironmentOnboardingSession {
            $draft = $this->resolveDraftForIdentity($workspace, $entraTenantId, $preferredDraft);
            $isNew = ! $draft instanceof ManagedEnvironmentOnboardingSession;
            $wasCreated = $isNew;

            if ($isNew) {
                $draft = new ManagedEnvironmentOnboardingSession;
                $draft->workspace_id = (int) $workspace->getKey();
                $draft->entra_tenant_id = $entraTenantId;
                $draft->started_by_user_id = (int) $actor->getKey();
                $draft->version = 0;
            } elseif ($expectedVersion !== null) {
                $this->assertExpectedVersion($draft, $expectedVersion);
            }

            $draft->entra_tenant_id = $entraTenantId;
            $draft->updated_by_user_id = (int) $actor->getKey();

            $mutator($draft);

            $this->persistDraft(
                draft: $draft,
                incrementVersion: $incrementVersion || $isNew,
            );

            return $draft->refresh();
        });
    }

    /**
     * @param  callable(ManagedEnvironmentOnboardingSession):void  $mutator
     */
    public function mutate(
        ManagedEnvironmentOnboardingSession $draft,
        User $actor,
        callable $mutator,
        ?int $expectedVersion = null,
        bool $incrementVersion = true,
        bool $allowTerminal = false,
    ): ManagedEnvironmentOnboardingSession {
        return DB::transaction(function () use ($draft, $actor, $mutator, $expectedVersion, $incrementVersion, $allowTerminal): ManagedEnvironmentOnboardingSession {
            $lockedDraft = ManagedEnvironmentOnboardingSession::query()
                ->whereKey($draft->getKey())
                ->lockForUpdate()
                ->firstOrFail();

            if (! $allowTerminal && $lockedDraft->lifecycleState()->isTerminal()) {
                throw new OnboardingDraftImmutableException(
                    draftId: (int) $lockedDraft->getKey(),
                    lifecycleState: $lockedDraft->lifecycleState(),
                );
            }

            if ($expectedVersion !== null) {
                $this->assertExpectedVersion($lockedDraft, $expectedVersion);
            }

            $lockedDraft->updated_by_user_id = (int) $actor->getKey();

            $mutator($lockedDraft);

            $this->persistDraft(
                draft: $lockedDraft,
                incrementVersion: $incrementVersion,
            );

            return $lockedDraft->refresh();
        });
    }

    public function lockForTrustedMutation(ManagedEnvironmentOnboardingSession|int|string $draft, Workspace $workspace): ManagedEnvironmentOnboardingSession
    {
        $draftId = $draft instanceof ManagedEnvironmentOnboardingSession
            ? (int) $draft->getKey()
            : (int) $draft;

        $lockedDraft = ManagedEnvironmentOnboardingSession::query()
            ->whereKey($draftId)
            ->where('workspace_id', (int) $workspace->getKey())
            ->lockForUpdate()
            ->first();

        if (! $lockedDraft instanceof ManagedEnvironmentOnboardingSession) {
            throw new NotFoundHttpException;
        }

        return $lockedDraft;
    }

    private function resolveDraftForIdentity(
        Workspace $workspace,
        string $entraTenantId,
        ?ManagedEnvironmentOnboardingSession $preferredDraft = null,
    ): ?ManagedEnvironmentOnboardingSession {
        if ($preferredDraft instanceof ManagedEnvironmentOnboardingSession) {
            $lockedPreferredDraft = ManagedEnvironmentOnboardingSession::query()
                ->whereKey($preferredDraft->getKey())
                ->where('workspace_id', (int) $workspace->getKey())
                ->lockForUpdate()
                ->first();

            if ($lockedPreferredDraft instanceof ManagedEnvironmentOnboardingSession && $lockedPreferredDraft->entra_tenant_id === $entraTenantId) {
                if ($lockedPreferredDraft->lifecycleState()->isTerminal()) {
                    throw new OnboardingDraftImmutableException(
                        draftId: (int) $lockedPreferredDraft->getKey(),
                        lifecycleState: $lockedPreferredDraft->lifecycleState(),
                    );
                }

                return $lockedPreferredDraft;
            }
        }

        return ManagedEnvironmentOnboardingSession::query()
            ->where('workspace_id', (int) $workspace->getKey())
            ->where('entra_tenant_id', $entraTenantId)
            ->resumable()
            ->orderByDesc('updated_at')
            ->lockForUpdate()
            ->first();
    }

    private function assertExpectedVersion(ManagedEnvironmentOnboardingSession $draft, int $expectedVersion): void
    {
        $actualVersion = max(1, (int) ($draft->version ?? 1));

        if ($expectedVersion === $actualVersion) {
            return;
        }

        throw new OnboardingDraftConflictException(
            draftId: (int) $draft->getKey(),
            expectedVersion: $expectedVersion,
            actualVersion: $actualVersion,
        );
    }

    private function persistDraft(ManagedEnvironmentOnboardingSession $draft, bool $incrementVersion): void
    {
        $currentVersion = max(0, (int) ($draft->version ?? 0));

        $draft->version = $incrementVersion
            ? $currentVersion + 1
            : max(1, $currentVersion);

        $this->lifecycleService->applySnapshot($draft, false);

        $draft->save();

        $this->lifecycleService->recordCompletedCheckpointTelemetryIfNeeded($draft);
    }
}
