<?php

declare(strict_types=1);

namespace App\Services\Tenants;

use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentOnboardingSession;
use App\Models\User;
use App\Services\Auth\CapabilityResolver;
use App\Support\ReasonTranslation\ReasonResolutionEnvelope;
use App\Support\Tenants\TenantInteractionLane;
use App\Support\Tenants\TenantLifecycle;
use App\Support\Tenants\TenantOperabilityContext;
use App\Support\Tenants\TenantOperabilityDecision;
use App\Support\Tenants\TenantOperabilityOutcome;
use App\Support\Tenants\TenantOperabilityQuestion;
use App\Support\Tenants\TenantOperabilityReasonCode;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;

class TenantOperabilityService
{
    public function __construct(
        private readonly CapabilityResolver $capabilityResolver,
    ) {}

    public function decisionFor(ManagedEnvironment $tenant): TenantOperabilityDecision
    {
        return TenantOperabilityDecision::fromOutcomes([
            TenantOperabilityQuestion::SelectorEligibility->value => $this->evaluate(
                TenantOperabilityContext::forTenant(
                    tenant: $tenant,
                    lane: TenantInteractionLane::StandardActiveOperating,
                ),
                TenantOperabilityQuestion::SelectorEligibility,
            ),
            TenantOperabilityQuestion::AdministrativeDiscoverability->value => $this->evaluate(
                TenantOperabilityContext::forTenant(
                    tenant: $tenant,
                    lane: TenantInteractionLane::AdministrativeManagement,
                ),
                TenantOperabilityQuestion::AdministrativeDiscoverability,
            ),
            TenantOperabilityQuestion::ArchiveEligibility->value => $this->evaluate(
                TenantOperabilityContext::forTenant(
                    tenant: $tenant,
                    lane: TenantInteractionLane::AdministrativeManagement,
                ),
                TenantOperabilityQuestion::ArchiveEligibility,
            ),
            TenantOperabilityQuestion::RestoreEligibility->value => $this->evaluate(
                TenantOperabilityContext::forTenant(
                    tenant: $tenant,
                    lane: TenantInteractionLane::AdministrativeManagement,
                ),
                TenantOperabilityQuestion::RestoreEligibility,
            ),
            TenantOperabilityQuestion::ResumeOnboardingEligibility->value => $this->evaluate(
                TenantOperabilityContext::forTenant(
                    tenant: $tenant,
                    lane: TenantInteractionLane::AdministrativeManagement,
                ),
                TenantOperabilityQuestion::ResumeOnboardingEligibility,
            ),
            TenantOperabilityQuestion::CanonicalLinkedRecordViewability->value => $this->evaluate(
                TenantOperabilityContext::forTenant(
                    tenant: $tenant,
                    lane: TenantInteractionLane::CanonicalWorkspaceRecord,
                ),
                TenantOperabilityQuestion::CanonicalLinkedRecordViewability,
            ),
        ]);
    }

    public function evaluate(TenantOperabilityContext $context, TenantOperabilityQuestion $question): TenantOperabilityOutcome
    {
        $lifecycle = TenantLifecycle::fromTenant($context->tenant);

        if ($context->workspaceId !== null && (int) $context->tenant->workspace_id !== $context->workspaceId) {
            return TenantOperabilityOutcome::deny(
                question: $question,
                lifecycle: $lifecycle,
                lane: $context->lane,
                reasonCode: $question === TenantOperabilityQuestion::RememberedContextValidity
                    ? TenantOperabilityReasonCode::RememberedContextStale
                    : TenantOperabilityReasonCode::WorkspaceMismatch,
                discoverable: false,
            );
        }

        if ($context->actor instanceof User && ! $this->capabilityResolver->isMember($context->actor, $context->tenant)) {
            return TenantOperabilityOutcome::deny(
                question: $question,
                lifecycle: $lifecycle,
                lane: $context->lane,
                reasonCode: TenantOperabilityReasonCode::TenantNotEntitled,
                discoverable: false,
                metadata: $this->metadata($context),
            );
        }

        if ($context->requiredCapability !== null && $context->actor instanceof User && ! $this->capabilityResolver->can($context->actor, $context->tenant, $context->requiredCapability)) {
            return TenantOperabilityOutcome::deny(
                question: $question,
                lifecycle: $lifecycle,
                lane: $context->lane,
                reasonCode: TenantOperabilityReasonCode::MissingCapability,
                discoverable: $question === TenantOperabilityQuestion::AdministrativeDiscoverability || $question === TenantOperabilityQuestion::EnvironmentBoundViewability,
                requiredCapability: $context->requiredCapability,
                metadata: $this->metadata($context),
            );
        }

        if (
            $context->tenant->workspace?->isClosed()
            && in_array($question, [
                TenantOperabilityQuestion::SelectorEligibility,
                TenantOperabilityQuestion::RememberedContextValidity,
                TenantOperabilityQuestion::ArchiveEligibility,
                TenantOperabilityQuestion::RestoreEligibility,
                TenantOperabilityQuestion::ResumeOnboardingEligibility,
                TenantOperabilityQuestion::OnboardingCompletionEligibility,
                TenantOperabilityQuestion::VerificationReadinessEligibility,
            ], true)
        ) {
            return TenantOperabilityOutcome::deny(
                question: $question,
                lifecycle: $lifecycle,
                lane: $context->lane,
                reasonCode: TenantOperabilityReasonCode::WorkspaceClosed,
                discoverable: in_array($question, [
                    TenantOperabilityQuestion::ArchiveEligibility,
                    TenantOperabilityQuestion::RestoreEligibility,
                    TenantOperabilityQuestion::ResumeOnboardingEligibility,
                    TenantOperabilityQuestion::VerificationReadinessEligibility,
                ], true),
                requiredCapability: $context->requiredCapability,
                metadata: $this->metadata($context),
            );
        }

        if (
            $context->tenant->isRemovedFromWorkspace()
            && in_array($question, [
                TenantOperabilityQuestion::SelectorEligibility,
                TenantOperabilityQuestion::RememberedContextValidity,
                TenantOperabilityQuestion::ArchiveEligibility,
                TenantOperabilityQuestion::ResumeOnboardingEligibility,
                TenantOperabilityQuestion::OnboardingCompletionEligibility,
                TenantOperabilityQuestion::VerificationReadinessEligibility,
            ], true)
        ) {
            return TenantOperabilityOutcome::deny(
                question: $question,
                lifecycle: $lifecycle,
                lane: $context->lane,
                reasonCode: TenantOperabilityReasonCode::TenantRemovedFromWorkspace,
                discoverable: in_array($question, [
                    TenantOperabilityQuestion::ArchiveEligibility,
                    TenantOperabilityQuestion::ResumeOnboardingEligibility,
                    TenantOperabilityQuestion::VerificationReadinessEligibility,
                ], true),
                requiredCapability: $context->requiredCapability,
                metadata: $this->metadata($context),
            );
        }

        return match ($question) {
            TenantOperabilityQuestion::SelectorEligibility => $this->selectorEligibilityOutcome($context, $lifecycle),
            TenantOperabilityQuestion::RememberedContextValidity => $this->rememberedContextOutcome($context, $lifecycle),
            TenantOperabilityQuestion::EnvironmentBoundViewability => $this->tenantBoundViewabilityOutcome($context, $lifecycle),
            TenantOperabilityQuestion::CanonicalLinkedRecordViewability => $this->canonicalViewabilityOutcome($context, $lifecycle),
            TenantOperabilityQuestion::ArchiveEligibility => $this->archiveEligibilityOutcome($context, $lifecycle),
            TenantOperabilityQuestion::RestoreEligibility => $this->restoreEligibilityOutcome($context, $lifecycle),
            TenantOperabilityQuestion::ResumeOnboardingEligibility => $this->resumeOnboardingOutcome($context, $lifecycle),
            TenantOperabilityQuestion::OnboardingCompletionEligibility => $this->onboardingCompletionOutcome($context, $lifecycle),
            TenantOperabilityQuestion::VerificationReadinessEligibility => $this->verificationReadinessOutcome($context, $lifecycle),
            TenantOperabilityQuestion::AdministrativeDiscoverability => $this->administrativeDiscoverabilityOutcome($context, $lifecycle),
        };
    }

    public function outcomeFor(
        ManagedEnvironment $tenant,
        TenantOperabilityQuestion $question,
        ?User $actor = null,
        ?int $workspaceId = null,
        TenantInteractionLane $lane = TenantInteractionLane::AdministrativeManagement,
        ?ManagedEnvironmentOnboardingSession $onboardingDraft = null,
        ?string $requiredCapability = null,
        ?ManagedEnvironment $selectedTenant = null,
        ?string $linkedRecordType = null,
        ?int $linkedRecordId = null,
    ): TenantOperabilityOutcome {
        return $this->evaluate(
            TenantOperabilityContext::forTenant(
                tenant: $tenant,
                actor: $actor,
                workspaceId: $workspaceId,
                lane: $lane,
                onboardingDraft: $onboardingDraft,
                requiredCapability: $requiredCapability,
                selectedTenant: $selectedTenant,
                linkedRecordType: $linkedRecordType,
                linkedRecordId: $linkedRecordId,
            ),
            $question,
        );
    }

    public function lifecycleFor(ManagedEnvironment $tenant): TenantLifecycle
    {
        return $this->decisionFor($tenant)->lifecycle;
    }

    public function canSelectAsContext(ManagedEnvironment $tenant): bool
    {
        return $this->outcomeFor(
            tenant: $tenant,
            question: TenantOperabilityQuestion::SelectorEligibility,
            lane: TenantInteractionLane::StandardActiveOperating,
        )->allowed;
    }

    public function canViewTenantSurface(ManagedEnvironment $tenant): bool
    {
        return $this->outcomeFor(
            tenant: $tenant,
            question: TenantOperabilityQuestion::AdministrativeDiscoverability,
            lane: TenantInteractionLane::AdministrativeManagement,
        )->allowed;
    }

    public function canResumeOnboarding(ManagedEnvironment $tenant): bool
    {
        return $this->outcomeFor(
            tenant: $tenant,
            question: TenantOperabilityQuestion::ResumeOnboardingEligibility,
            lane: TenantInteractionLane::AdministrativeManagement,
        )->allowed;
    }

    public function canArchive(ManagedEnvironment $tenant): bool
    {
        return $this->outcomeFor(
            tenant: $tenant,
            question: TenantOperabilityQuestion::ArchiveEligibility,
            lane: TenantInteractionLane::AdministrativeManagement,
        )->allowed;
    }

    public function canRestore(ManagedEnvironment $tenant): bool
    {
        return $this->outcomeFor(
            tenant: $tenant,
            question: TenantOperabilityQuestion::RestoreEligibility,
            lane: TenantInteractionLane::AdministrativeManagement,
        )->allowed;
    }

    public function primaryManagementActionKey(ManagedEnvironment $tenant, bool $preferOnboarding = false): ?string
    {
        return $this->decisionFor($tenant)->primaryManagementActionKey($preferOnboarding);
    }

    public function canReferenceInWorkspaceMonitoring(ManagedEnvironment $tenant): bool
    {
        return $this->outcomeFor(
            tenant: $tenant,
            question: TenantOperabilityQuestion::CanonicalLinkedRecordViewability,
            lane: TenantInteractionLane::CanonicalWorkspaceRecord,
        )->allowed;
    }

    public function presentReason(TenantOperabilityOutcome $outcome): ?ReasonResolutionEnvelope
    {
        return $outcome->reasonCode?->toReasonResolutionEnvelope('detail');
    }

    /**
     * @param  Collection<int, ManagedEnvironment>  $tenants
     * @return Collection<int, ManagedEnvironment>
     */
    public function filterSelectable(Collection $tenants): Collection
    {
        return $tenants
            ->filter(fn (mixed $tenant): bool => $tenant instanceof ManagedEnvironment && $this->canSelectAsContext($tenant))
            ->values();
    }

    public function applySelectableScope(Builder $query, ?string $table = null): Builder
    {
        $prefix = $table !== null && $table !== '' ? "{$table}." : '';

        return $query
            ->whereNull("{$prefix}deleted_at")
            ->whereNull("{$prefix}removed_from_workspace_at")
            ->where("{$prefix}lifecycle_status", TenantLifecycle::Active->value)
            ->whereHas('workspace', fn (Builder $workspaceQuery): Builder => $workspaceQuery
                ->whereNull('archived_at')
                ->whereNull('closed_at'));
    }

    public function applyAdministrativeDiscoverabilityScope(Builder $query, ?string $table = null): Builder
    {
        $prefix = $table !== null && $table !== '' ? "{$table}." : '';

        return $query
            ->withTrashed()
            ->where(function (Builder $builder) use ($prefix): void {
                $builder->whereIn("{$prefix}lifecycle_status", TenantLifecycle::values())
                    ->orWhereNotNull("{$prefix}deleted_at");
            });
    }

    private function selectorEligibilityOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome
    {
        if ($context->lane !== TenantInteractionLane::StandardActiveOperating) {
            return $this->wrongLaneOutcome($context, TenantOperabilityQuestion::SelectorEligibility, $lifecycle);
        }

        if (! $lifecycle->isSelectableInStandardLane()) {
            return TenantOperabilityOutcome::deny(
                question: TenantOperabilityQuestion::SelectorEligibility,
                lifecycle: $lifecycle,
                lane: $context->lane,
                reasonCode: TenantOperabilityReasonCode::SelectorIneligibleLifecycle,
                discoverable: false,
                metadata: $this->metadata($context),
            );
        }

        return TenantOperabilityOutcome::allow(
            question: TenantOperabilityQuestion::SelectorEligibility,
            lifecycle: $lifecycle,
            lane: $context->lane,
            discoverable: true,
            metadata: $this->metadata($context),
        );
    }

    private function rememberedContextOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome
    {
        if ($context->lane !== TenantInteractionLane::StandardActiveOperating) {
            return TenantOperabilityOutcome::deny(
                question: TenantOperabilityQuestion::RememberedContextValidity,
                lifecycle: $lifecycle,
                lane: $context->lane,
                reasonCode: TenantOperabilityReasonCode::RememberedContextStale,
                discoverable: false,
                metadata: $this->metadata($context),
            );
        }

        if (! $lifecycle->isSelectableInStandardLane()) {
            return TenantOperabilityOutcome::deny(
                question: TenantOperabilityQuestion::RememberedContextValidity,
                lifecycle: $lifecycle,
                lane: $context->lane,
                reasonCode: TenantOperabilityReasonCode::RememberedContextStale,
                discoverable: false,
                metadata: $this->metadata($context),
            );
        }

        return TenantOperabilityOutcome::allow(
            question: TenantOperabilityQuestion::RememberedContextValidity,
            lifecycle: $lifecycle,
            lane: $context->lane,
            discoverable: true,
            metadata: $this->metadata($context),
        );
    }

    private function tenantBoundViewabilityOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome
    {
        if ($context->lane !== TenantInteractionLane::AdministrativeManagement) {
            return $this->wrongLaneOutcome($context, TenantOperabilityQuestion::EnvironmentBoundViewability, $lifecycle);
        }

        return TenantOperabilityOutcome::allow(
            question: TenantOperabilityQuestion::EnvironmentBoundViewability,
            lifecycle: $lifecycle,
            lane: $context->lane,
            discoverable: true,
            metadata: $this->metadata($context),
        );
    }

    private function canonicalViewabilityOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome
    {
        if ($context->lane !== TenantInteractionLane::CanonicalWorkspaceRecord) {
            return $this->wrongLaneOutcome($context, TenantOperabilityQuestion::CanonicalLinkedRecordViewability, $lifecycle);
        }

        return TenantOperabilityOutcome::allow(
            question: TenantOperabilityQuestion::CanonicalLinkedRecordViewability,
            lifecycle: $lifecycle,
            lane: $context->lane,
            discoverable: true,
            metadata: $this->metadata($context),
        );
    }

    private function archiveEligibilityOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome
    {
        if ($context->lane !== TenantInteractionLane::AdministrativeManagement) {
            return $this->wrongLaneOutcome($context, TenantOperabilityQuestion::ArchiveEligibility, $lifecycle);
        }

        if (! $lifecycle->canArchive()) {
            return TenantOperabilityOutcome::deny(
                question: TenantOperabilityQuestion::ArchiveEligibility,
                lifecycle: $lifecycle,
                lane: $context->lane,
                reasonCode: $lifecycle === TenantLifecycle::Archived
                    ? TenantOperabilityReasonCode::TenantAlreadyArchived
                    : TenantOperabilityReasonCode::SelectorIneligibleLifecycle,
                discoverable: true,
                requiredCapability: $context->requiredCapability,
                metadata: $this->metadata($context),
            );
        }

        return TenantOperabilityOutcome::allow(
            question: TenantOperabilityQuestion::ArchiveEligibility,
            lifecycle: $lifecycle,
            lane: $context->lane,
            discoverable: true,
            requiredCapability: $context->requiredCapability,
            metadata: $this->metadata($context),
        );
    }

    private function restoreEligibilityOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome
    {
        if ($context->lane !== TenantInteractionLane::AdministrativeManagement) {
            return $this->wrongLaneOutcome($context, TenantOperabilityQuestion::RestoreEligibility, $lifecycle);
        }

        if (! $lifecycle->canRestore()) {
            return TenantOperabilityOutcome::deny(
                question: TenantOperabilityQuestion::RestoreEligibility,
                lifecycle: $lifecycle,
                lane: $context->lane,
                reasonCode: TenantOperabilityReasonCode::TenantNotArchived,
                discoverable: true,
                requiredCapability: $context->requiredCapability,
                metadata: $this->metadata($context),
            );
        }

        return TenantOperabilityOutcome::allow(
            question: TenantOperabilityQuestion::RestoreEligibility,
            lifecycle: $lifecycle,
            lane: $context->lane,
            discoverable: true,
            requiredCapability: $context->requiredCapability,
            metadata: $this->metadata($context),
        );
    }

    private function resumeOnboardingOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome
    {
        if (! in_array($context->lane, [TenantInteractionLane::AdministrativeManagement, TenantInteractionLane::OnboardingWorkflow], true)) {
            return $this->wrongLaneOutcome($context, TenantOperabilityQuestion::ResumeOnboardingEligibility, $lifecycle);
        }

        if (! $lifecycle->canResumeOnboarding()) {
            return TenantOperabilityOutcome::deny(
                question: TenantOperabilityQuestion::ResumeOnboardingEligibility,
                lifecycle: $lifecycle,
                lane: $context->lane,
                reasonCode: TenantOperabilityReasonCode::SelectorIneligibleLifecycle,
                discoverable: $context->lane === TenantInteractionLane::AdministrativeManagement,
                requiredCapability: $context->requiredCapability,
                metadata: $this->metadata($context),
            );
        }

        if ($context->onboardingDraft instanceof ManagedEnvironmentOnboardingSession && ! $context->onboardingDraft->isWorkflowResumable()) {
            return TenantOperabilityOutcome::deny(
                question: TenantOperabilityQuestion::ResumeOnboardingEligibility,
                lifecycle: $lifecycle,
                lane: $context->lane,
                reasonCode: TenantOperabilityReasonCode::OnboardingNotResumable,
                discoverable: $context->lane === TenantInteractionLane::AdministrativeManagement,
                requiredCapability: $context->requiredCapability,
                metadata: $this->metadata($context),
            );
        }

        return TenantOperabilityOutcome::allow(
            question: TenantOperabilityQuestion::ResumeOnboardingEligibility,
            lifecycle: $lifecycle,
            lane: $context->lane,
            discoverable: true,
            requiredCapability: $context->requiredCapability,
            metadata: $this->metadata($context),
        );
    }

    private function onboardingCompletionOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome
    {
        if ($context->lane !== TenantInteractionLane::OnboardingWorkflow) {
            return $this->wrongLaneOutcome($context, TenantOperabilityQuestion::OnboardingCompletionEligibility, $lifecycle);
        }

        if (! in_array($lifecycle, [TenantLifecycle::Draft, TenantLifecycle::Onboarding], true)) {
            return TenantOperabilityOutcome::deny(
                question: TenantOperabilityQuestion::OnboardingCompletionEligibility,
                lifecycle: $lifecycle,
                lane: $context->lane,
                reasonCode: TenantOperabilityReasonCode::SelectorIneligibleLifecycle,
                discoverable: false,
                requiredCapability: $context->requiredCapability,
                metadata: $this->metadata($context),
            );
        }

        return TenantOperabilityOutcome::allow(
            question: TenantOperabilityQuestion::OnboardingCompletionEligibility,
            lifecycle: $lifecycle,
            lane: $context->lane,
            discoverable: true,
            requiredCapability: $context->requiredCapability,
            metadata: $this->metadata($context),
        );
    }

    private function verificationReadinessOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome
    {
        if (! $lifecycle->supportsQuestion(TenantOperabilityQuestion::VerificationReadinessEligibility, $context->lane)) {
            return TenantOperabilityOutcome::deny(
                question: TenantOperabilityQuestion::VerificationReadinessEligibility,
                lifecycle: $lifecycle,
                lane: $context->lane,
                reasonCode: $context->lane === TenantInteractionLane::CanonicalWorkspaceRecord
                    ? TenantOperabilityReasonCode::CanonicalViewFollowupOnly
                    : TenantOperabilityReasonCode::WrongLane,
                discoverable: $context->lane === TenantInteractionLane::AdministrativeManagement,
                requiredCapability: $context->requiredCapability,
                metadata: $this->metadata($context),
            );
        }

        return TenantOperabilityOutcome::allow(
            question: TenantOperabilityQuestion::VerificationReadinessEligibility,
            lifecycle: $lifecycle,
            lane: $context->lane,
            discoverable: true,
            requiredCapability: $context->requiredCapability,
            metadata: $this->metadata($context),
        );
    }

    private function administrativeDiscoverabilityOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome
    {
        return TenantOperabilityOutcome::allow(
            question: TenantOperabilityQuestion::AdministrativeDiscoverability,
            lifecycle: $lifecycle,
            lane: $context->lane,
            discoverable: $lifecycle->isAdministrativelyDiscoverable(),
            metadata: $this->metadata($context),
        );
    }

    private function wrongLaneOutcome(
        TenantOperabilityContext $context,
        TenantOperabilityQuestion $question,
        TenantLifecycle $lifecycle,
    ): TenantOperabilityOutcome {
        return TenantOperabilityOutcome::deny(
            question: $question,
            lifecycle: $lifecycle,
            lane: $context->lane,
            reasonCode: TenantOperabilityReasonCode::WrongLane,
            discoverable: false,
            requiredCapability: $context->requiredCapability,
            metadata: $this->metadata($context),
        );
    }

    /**
     * @return array<string, mixed>
     */
    private function metadata(TenantOperabilityContext $context): array
    {
        return array_filter([
            'selected_tenant_id' => $context->selectedTenant?->getKey(),
            'linked_record_type' => $context->linkedRecordType,
            'linked_record_id' => $context->linkedRecordId,
            'page_category' => $context->pageCategory?->value,
            'onboarding_draft_id' => $context->onboardingDraft?->getKey(),
        ], static fn (mixed $value): bool => $value !== null && $value !== '');
    }
}
