<?php

namespace App\Models;

use App\Support\Inventory\InventoryCoverage;
use App\Support\OperationCatalog;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationTypeResolution;
use App\Support\OperationRunType;
use App\Support\Operations\OperationLifecyclePolicy;
use App\Support\Operations\OperationRunFreshnessState;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Arr;

class OperationRun extends Model
{
    use HasFactory;

    public const string PROBLEM_CLASS_NONE = 'none';

    public const string PROBLEM_CLASS_ACTIVE_STALE_ATTENTION = 'active_stale_attention';

    public const string PROBLEM_CLASS_TERMINAL_FOLLOW_UP = 'terminal_follow_up';

    protected $guarded = [];

    protected $casts = [
        'summary_counts' => 'array',
        'failure_summary' => 'array',
        'context' => 'array',
        'started_at' => 'datetime',
        'completed_at' => 'datetime',
    ];

    protected static function booted(): void
    {
        static::creating(function (self $operationRun): void {
            if ($operationRun->workspace_id !== null) {
                return;
            }

            if ($operationRun->managed_environment_id === null) {
                return;
            }

            $tenant = ManagedEnvironment::query()->whereKey((int) $operationRun->managed_environment_id)->first();

            if (! $tenant instanceof ManagedEnvironment) {
                return;
            }

            if ($tenant->workspace_id === null) {
                return;
            }

            $operationRun->workspace_id = (int) $tenant->workspace_id;
        });
    }

    public function tenant(): BelongsTo
    {
        return $this->belongsTo(ManagedEnvironment::class, 'managed_environment_id')->withTrashed();
    }

    public function workspace(): BelongsTo
    {
        return $this->belongsTo(Workspace::class);
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function scopeActive(Builder $query): Builder
    {
        return $query->whereIn('status', [
            OperationRunStatus::Queued->value,
            OperationRunStatus::Running->value,
        ]);
    }

    public function scopeTerminalFailure(Builder $query): Builder
    {
        return $query
            ->where('status', OperationRunStatus::Completed->value)
            ->where('outcome', OperationRunOutcome::Failed->value);
    }

    public function scopeBaselineCompareForProfile(Builder $query, BaselineProfile|int $profile): Builder
    {
        $profileId = $profile instanceof BaselineProfile
            ? (int) $profile->getKey()
            : (int) $profile;

        return $query
            ->whereIn('type', OperationCatalog::rawValuesForCanonical(OperationRunType::BaselineCompare->value))
            ->where('context->baseline_profile_id', $profileId);
    }

    public function scopeLikelyStale(Builder $query, ?OperationLifecyclePolicy $policy = null): Builder
    {
        $policy ??= app(OperationLifecyclePolicy::class);

        return $query
            ->active()
            ->where(function (Builder $query) use ($policy): void {
                foreach ($policy->coveredTypeNames() as $type) {
                    $query->orWhere(function (Builder $typeQuery) use ($policy, $type): void {
                        $typeQuery
                            ->whereIn('type', OperationCatalog::rawValuesForCanonical($type))
                            ->where(function (Builder $stateQuery) use ($policy, $type): void {
                                $stateQuery
                                    ->where(function (Builder $queuedQuery) use ($policy, $type): void {
                                        $queuedQuery
                                            ->where('status', OperationRunStatus::Queued->value)
                                            ->whereNull('started_at')
                                            ->where('created_at', '<=', now()->subSeconds($policy->queuedStaleAfterSeconds($type)));
                                    })
                                    ->orWhere(function (Builder $runningQuery) use ($policy, $type): void {
                                        $runningQuery
                                            ->where('status', OperationRunStatus::Running->value)
                                            ->where(function (Builder $startedAtQuery) use ($policy, $type): void {
                                                $startedAtQuery
                                                    ->where('started_at', '<=', now()->subSeconds($policy->runningStaleAfterSeconds($type)))
                                                    ->orWhere(function (Builder $fallbackQuery) use ($policy, $type): void {
                                                        $fallbackQuery
                                                            ->whereNull('started_at')
                                                            ->where('created_at', '<=', now()->subSeconds($policy->runningStaleAfterSeconds($type)));
                                                    });
                                            });
                                    });
                            });
                    });
                }
            });
    }

    public function scopeHealthyActive(Builder $query, ?OperationLifecyclePolicy $policy = null): Builder
    {
        $policy ??= app(OperationLifecyclePolicy::class);
        $coveredTypes = $policy->coveredTypeNames();

        if ($coveredTypes === []) {
            return $query->active();
        }

        return $query
            ->active()
            ->where(function (Builder $query) use ($coveredTypes, $policy): void {
                $coveredRawTypes = collect($coveredTypes)
                    ->flatMap(static fn (string $type): array => OperationCatalog::rawValuesForCanonical($type))
                    ->unique()
                    ->values()
                    ->all();

                $query->whereNotIn('type', $coveredRawTypes);

                foreach ($coveredTypes as $type) {
                    $query->orWhere(function (Builder $typeQuery) use ($policy, $type): void {
                        $typeQuery
                            ->whereIn('type', OperationCatalog::rawValuesForCanonical($type))
                            ->where(function (Builder $stateQuery) use ($policy, $type): void {
                                $stateQuery
                                    ->where(function (Builder $queuedQuery) use ($policy, $type): void {
                                        $queuedQuery
                                            ->where('status', OperationRunStatus::Queued->value)
                                            ->where(function (Builder $freshQueuedQuery) use ($policy, $type): void {
                                                $freshQueuedQuery
                                                    ->whereNotNull('started_at')
                                                    ->orWhereNull('created_at')
                                                    ->orWhere('created_at', '>', now()->subSeconds($policy->queuedStaleAfterSeconds($type)));
                                            });
                                    })
                                    ->orWhere(function (Builder $runningQuery) use ($policy, $type): void {
                                        $runningQuery
                                            ->where('status', OperationRunStatus::Running->value)
                                            ->where(function (Builder $freshRunningQuery) use ($policy, $type): void {
                                                $freshRunningQuery
                                                    ->where('started_at', '>', now()->subSeconds($policy->runningStaleAfterSeconds($type)))
                                                    ->orWhere(function (Builder $fallbackQuery) use ($policy, $type): void {
                                                        $fallbackQuery
                                                            ->whereNull('started_at')
                                                            ->where(function (Builder $createdAtQuery) use ($policy, $type): void {
                                                                $createdAtQuery
                                                                    ->whereNull('created_at')
                                                                    ->orWhere('created_at', '>', now()->subSeconds($policy->runningStaleAfterSeconds($type)));
                                                            });
                                                    });
                                            });
                                    });
                            });
                    });
                }
            });
    }

    public function scopeDashboardNeedsFollowUp(Builder $query): Builder
    {
        return $query->where(function (Builder $query): void {
            $query
                ->where(function (Builder $terminalQuery): void {
                    $terminalQuery->terminalFollowUp();
                })
                ->orWhere(function (Builder $activeQuery): void {
                    $activeQuery->activeStaleAttention();
                });
        });
    }

    public function scopeActiveStaleAttention(Builder $query, ?OperationLifecyclePolicy $policy = null): Builder
    {
        return $query->likelyStale($policy);
    }

    public function scopeTerminalFollowUp(Builder $query): Builder
    {
        return $query
            ->where('status', OperationRunStatus::Completed->value)
            ->where(function (Builder $query): void {
                $query
                    ->whereIn('outcome', [
                        OperationRunOutcome::Blocked->value,
                        OperationRunOutcome::PartiallySucceeded->value,
                        OperationRunOutcome::Failed->value,
                    ])
                    ->orWhereNotNull('context->reconciliation->reconciled_at');
            });
    }

    public function getSelectionHashAttribute(): ?string
    {
        $context = is_array($this->context) ? $this->context : [];

        return isset($context['selection_hash']) && is_string($context['selection_hash'])
            ? $context['selection_hash']
            : null;
    }

    public function setSelectionHashAttribute(?string $value): void
    {
        $context = is_array($this->context) ? $this->context : [];
        $context['selection_hash'] = $value;

        $this->context = $context;
    }

    /**
     * @return array<string, mixed>
     */
    public function getSelectionPayloadAttribute(): array
    {
        $context = is_array($this->context) ? $this->context : [];

        return Arr::only($context, [
            'policy_types',
            'categories',
            'include_foundations',
            'include_dependencies',
        ]);
    }

    /**
     * @param  array<string, mixed>|null  $value
     */
    public function setSelectionPayloadAttribute(?array $value): void
    {
        $context = is_array($this->context) ? $this->context : [];

        if (is_array($value)) {
            $context = array_merge($context, Arr::only($value, [
                'policy_types',
                'categories',
                'include_foundations',
                'include_dependencies',
            ]));
        }

        $this->context = $context;
    }

    public function getFinishedAtAttribute(): mixed
    {
        return $this->completed_at;
    }

    public function setFinishedAtAttribute(mixed $value): void
    {
        $this->completed_at = $value;
    }

    public function inventoryCoverage(): ?InventoryCoverage
    {
        return InventoryCoverage::fromContext($this->context);
    }

    public function resolvedOperationType(): OperationTypeResolution
    {
        return OperationCatalog::resolve((string) $this->type);
    }

    public function canonicalOperationType(): string
    {
        return $this->resolvedOperationType()->canonical->canonicalCode;
    }

    public function isGovernanceArtifactOperation(): bool
    {
        return OperationCatalog::isGovernanceArtifactOperation((string) $this->type);
    }

    /**
     * @param  array<int, int>  $tenantIds
     * @return \Illuminate\Support\Collection<int, self>
     */
    public static function latestBaselineCompareRunsForProfile(
        BaselineProfile|int $profile,
        array $tenantIds,
        ?int $workspaceId = null,
        bool $completedOnly = false,
    ): \Illuminate\Support\Collection {
        if ($tenantIds === []) {
            return collect();
        }

        $runs = static::query()
            ->when($workspaceId !== null, fn (Builder $query): Builder => $query->where('workspace_id', $workspaceId))
            ->whereIn('managed_environment_id', $tenantIds)
            ->baselineCompareForProfile($profile)
            ->when($completedOnly, fn (Builder $query): Builder => $query->where('status', OperationRunStatus::Completed->value))
            ->orderByDesc('completed_at')
            ->orderByDesc('id')
            ->get();

        return $runs
            ->unique(static fn (self $run): int => (int) $run->managed_environment_id)
            ->values();
    }

    public static function latestCompletedCoverageBearingInventorySyncForTenant(int $tenantId): ?self
    {
        if ($tenantId <= 0) {
            return null;
        }

        return static::query()
            ->where('managed_environment_id', $tenantId)
            ->whereIn('type', OperationCatalog::rawValuesForCanonical(OperationRunType::InventorySync->value))
            ->where('status', OperationRunStatus::Completed->value)
            ->whereNotNull('completed_at')
            ->latest('completed_at')
            ->latest('id')
            ->cursor()
            ->first(static fn (self $run): bool => $run->inventoryCoverage() instanceof InventoryCoverage);
    }

    public function supportsOperatorExplanation(): bool
    {
        return OperationCatalog::supportsOperatorExplanation((string) $this->type);
    }

    public function governanceArtifactFamily(): ?string
    {
        return OperationCatalog::governanceArtifactFamily((string) $this->type);
    }

    /**
     * @return array<string, mixed>
     */
    public function artifactResultContext(): array
    {
        $context = is_array($this->context) ? $this->context : [];
        $result = is_array($context['result'] ?? null) ? $context['result'] : [];

        return array_merge($context, ['result' => $result]);
    }

    public function relatedArtifactId(): ?int
    {
        return match ($this->governanceArtifactFamily()) {
            'baseline_snapshot' => is_numeric(data_get($this->context, 'result.snapshot_id'))
                ? (int) data_get($this->context, 'result.snapshot_id')
                : null,
            default => null,
        };
    }

    /**
     * @return array<string, mixed>
     */
    public function reconciliation(): array
    {
        $context = is_array($this->context) ? $this->context : [];
        $reconciliation = $context['reconciliation'] ?? null;

        return is_array($reconciliation) ? $reconciliation : [];
    }

    public function isLifecycleReconciled(): bool
    {
        return $this->reconciliation() !== [];
    }

    public function lifecycleReconciliationReasonCode(): ?string
    {
        $reasonCode = $this->reconciliation()['reason_code'] ?? null;

        return is_string($reasonCode) && trim($reasonCode) !== '' ? trim($reasonCode) : null;
    }

    public function freshnessState(): OperationRunFreshnessState
    {
        return OperationRunFreshnessState::forRun($this);
    }

    /**
     * @return list<string>
     */
    public static function problemClasses(): array
    {
        return [
            self::PROBLEM_CLASS_NONE,
            self::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION,
            self::PROBLEM_CLASS_TERMINAL_FOLLOW_UP,
        ];
    }

    public function problemClass(): string
    {
        $freshnessState = $this->freshnessState();

        if ($freshnessState->isLikelyStale()) {
            return self::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION;
        }

        if ($freshnessState->isReconciledFailed()) {
            return self::PROBLEM_CLASS_TERMINAL_FOLLOW_UP;
        }

        if ((string) $this->status === OperationRunStatus::Completed->value) {
            return in_array((string) $this->outcome, [
                OperationRunOutcome::Blocked->value,
                OperationRunOutcome::PartiallySucceeded->value,
                OperationRunOutcome::Failed->value,
            ], true)
                ? self::PROBLEM_CLASS_TERMINAL_FOLLOW_UP
                : self::PROBLEM_CLASS_NONE;
        }

        return self::PROBLEM_CLASS_NONE;
    }

    public function hasStaleLineage(): bool
    {
        return $this->freshnessState()->isReconciledFailed();
    }

    public function isCurrentlyActive(): bool
    {
        return in_array((string) $this->status, [
            OperationRunStatus::Queued->value,
            OperationRunStatus::Running->value,
        ], true);
    }

    public function requiresOperatorReview(): bool
    {
        return $this->problemClass() !== self::PROBLEM_CLASS_NONE;
    }

    public function requiresDashboardFollowUp(): bool
    {
        return $this->requiresOperatorReview();
    }

    /**
     * @return array<string, mixed>
     */
    public function baselineGapEnvelope(): array
    {
        $context = is_array($this->context) ? $this->context : [];

        return match ($this->canonicalOperationType()) {
            'baseline.compare' => is_array(data_get($context, 'baseline_compare.evidence_gaps'))
                ? data_get($context, 'baseline_compare.evidence_gaps')
                : [],
            'baseline.capture' => is_array(data_get($context, 'baseline_capture.gaps'))
                ? data_get($context, 'baseline_capture.gaps')
                : [],
            default => [],
        };
    }

    public function hasStructuredBaselineGapPayload(): bool
    {
        $subjects = $this->baselineGapEnvelope()['subjects'] ?? null;

        if (! is_array($subjects) || ! array_is_list($subjects) || $subjects === []) {
            return false;
        }

        foreach ($subjects as $subject) {
            if (! is_array($subject)) {
                return false;
            }

            foreach ([
                'policy_type',
                'subject_key',
                'subject_class',
                'resolution_path',
                'resolution_outcome',
                'reason_code',
                'operator_action_category',
                'structural',
                'retryable',
            ] as $key) {
                if (! array_key_exists($key, $subject)) {
                    return false;
                }
            }
        }

        return true;
    }

    public function hasLegacyBaselineGapPayload(): bool
    {
        $envelope = $this->baselineGapEnvelope();
        $byReason = is_array($envelope['by_reason'] ?? null) ? $envelope['by_reason'] : [];

        if (array_key_exists('policy_not_found', $byReason)) {
            return true;
        }

        $subjects = $envelope['subjects'] ?? null;

        if (! is_array($subjects)) {
            return false;
        }

        if (! array_is_list($subjects)) {
            return $subjects !== [];
        }

        if ($subjects === []) {
            return false;
        }

        return ! $this->hasStructuredBaselineGapPayload();
    }
}
