<?php

declare(strict_types=1);

use App\Support\Baselines\BaselineReasonCodes;
use App\Support\Baselines\BaselineSnapshotLifecycleState;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunType;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        if (! Schema::hasTable('baseline_snapshots')) {
            return;
        }

        $needsLifecycleState = ! Schema::hasColumn('baseline_snapshots', 'lifecycle_state');
        $needsCompletedAt = ! Schema::hasColumn('baseline_snapshots', 'completed_at');
        $needsFailedAt = ! Schema::hasColumn('baseline_snapshots', 'failed_at');
        $needsCompletionMeta = ! Schema::hasColumn('baseline_snapshots', 'completion_meta_jsonb');

        if ($needsLifecycleState || $needsCompletedAt || $needsFailedAt || $needsCompletionMeta) {
            Schema::table('baseline_snapshots', function (Blueprint $table) use ($needsLifecycleState, $needsCompletedAt, $needsFailedAt, $needsCompletionMeta): void {
                if ($needsLifecycleState) {
                    $table->string('lifecycle_state')->nullable()->after('captured_at');
                }

                if ($needsCompletedAt) {
                    $table->timestampTz('completed_at')->nullable()->after('lifecycle_state');
                }

                if ($needsFailedAt) {
                    $table->timestampTz('failed_at')->nullable()->after('completed_at');
                }

                if ($needsCompletionMeta) {
                    $table->jsonb('completion_meta_jsonb')->nullable()->after('summary_jsonb');
                }
            });
        }

        DB::table('baseline_snapshots')
            ->orderBy('id')
            ->chunkById(200, function ($rows): void {
                foreach ($rows as $row) {
                    $summary = $this->decodeJson($row->summary_jsonb);
                    $persistedItems = (int) DB::table('baseline_snapshot_items')
                        ->where('baseline_snapshot_id', (int) $row->id)
                        ->count();

                    $classification = $this->classifyLegacySnapshot($row, $summary, $persistedItems);

                    DB::table('baseline_snapshots')
                        ->where('id', (int) $row->id)
                        ->update([
                            'lifecycle_state' => $classification['lifecycle_state'],
                            'completed_at' => $classification['completed_at'],
                            'failed_at' => $classification['failed_at'],
                            'completion_meta_jsonb' => json_encode($classification['completion_meta'], JSON_THROW_ON_ERROR),
                        ]);
                }
            }, 'id');

        DB::table('baseline_snapshots')
            ->whereNull('lifecycle_state')
            ->update([
                'lifecycle_state' => BaselineSnapshotLifecycleState::Incomplete->value,
            ]);

        if ($needsLifecycleState) {
            DB::statement(sprintf(
                "UPDATE baseline_snapshots SET lifecycle_state = '%s' WHERE lifecycle_state IS NULL",
                BaselineSnapshotLifecycleState::Incomplete->value,
            ));

            Schema::table('baseline_snapshots', function (Blueprint $table): void {
                $table->string('lifecycle_state')->default(BaselineSnapshotLifecycleState::Building->value)->nullable(false)->change();
            });
        }

        if (! $this->hasIndex('baseline_snapshots_lifecycle_state_index')) {
            Schema::table('baseline_snapshots', function (Blueprint $table): void {
                $table->index('lifecycle_state', 'baseline_snapshots_lifecycle_state_index');
            });
        }

        if (! $this->hasIndex('baseline_snapshots_profile_lifecycle_completed_idx')) {
            Schema::table('baseline_snapshots', function (Blueprint $table): void {
                $table->index(
                    ['workspace_id', 'baseline_profile_id', 'lifecycle_state', 'completed_at'],
                    'baseline_snapshots_profile_lifecycle_completed_idx',
                );
            });
        }
    }

    public function down(): void
    {
        if (! Schema::hasTable('baseline_snapshots')) {
            return;
        }

        if ($this->hasIndex('baseline_snapshots_profile_lifecycle_completed_idx')) {
            Schema::table('baseline_snapshots', function (Blueprint $table): void {
                $table->dropIndex('baseline_snapshots_profile_lifecycle_completed_idx');
            });
        }

        if ($this->hasIndex('baseline_snapshots_lifecycle_state_index')) {
            Schema::table('baseline_snapshots', function (Blueprint $table): void {
                $table->dropIndex('baseline_snapshots_lifecycle_state_index');
            });
        }

        Schema::table('baseline_snapshots', function (Blueprint $table): void {
            foreach (['completion_meta_jsonb', 'failed_at', 'completed_at', 'lifecycle_state'] as $column) {
                if (Schema::hasColumn('baseline_snapshots', $column)) {
                    $table->dropColumn($column);
                }
            }
        });
    }

    /**
     * @param  array<string, mixed>  $summary
     * @return array{
     *     lifecycle_state: string,
     *     completed_at: mixed,
     *     failed_at: mixed,
     *     completion_meta: array<string, mixed>
     * }
     */
    private function classifyLegacySnapshot(object $row, array $summary, int $persistedItems): array
    {
        $expectedItems = $this->normalizeInteger(data_get($summary, 'total_items'));
        $producerRun = $this->resolveProducerRunForSnapshot((int) $row->id, (int) $row->workspace_id, (int) $row->baseline_profile_id);
        $producerRunContext = $producerRun !== null ? $this->decodeJson($producerRun->context ?? null) : [];
        $producerExpectedItems = $this->normalizeInteger(data_get($producerRunContext, 'result.items_captured'))
            ?? $this->normalizeInteger(data_get($producerRunContext, 'baseline_capture.subjects_total'));
        $producerSubjectsTotal = $this->normalizeInteger(data_get($producerRunContext, 'baseline_capture.subjects_total'));
        $producerSucceeded = $producerRun !== null
            && in_array((string) ($producerRun->outcome ?? ''), [
                OperationRunOutcome::Succeeded->value,
                OperationRunOutcome::PartiallySucceeded->value,
            ], true);

        $completionMeta = [
            'expected_items' => $expectedItems ?? $producerExpectedItems,
            'persisted_items' => $persistedItems,
            'producer_run_id' => $producerRun?->id !== null ? (int) $producerRun->id : null,
            'was_empty_capture' => ($expectedItems ?? $producerExpectedItems ?? $producerSubjectsTotal) === 0 && $persistedItems === 0,
        ];

        if ($expectedItems !== null
            && $expectedItems === $persistedItems
            && ! ($expectedItems === 0 && $persistedItems === 0)) {
            return [
                'lifecycle_state' => BaselineSnapshotLifecycleState::Complete->value,
                'completed_at' => $producerRun->completed_at ?? $row->captured_at ?? $row->created_at ?? $row->updated_at,
                'failed_at' => null,
                'completion_meta' => $completionMeta + [
                    'finalization_reason_code' => 'baseline.snapshot.legacy_count_proof',
                ],
            ];
        }

        if ($producerSucceeded
            && $producerExpectedItems !== null
            && $producerExpectedItems === $persistedItems
            && ! ($producerExpectedItems === 0 && $persistedItems === 0)) {
            return [
                'lifecycle_state' => BaselineSnapshotLifecycleState::Complete->value,
                'completed_at' => $producerRun->completed_at ?? $row->captured_at ?? $row->created_at ?? $row->updated_at,
                'failed_at' => null,
                'completion_meta' => $completionMeta + [
                    'finalization_reason_code' => 'baseline.snapshot.legacy_producer_run_proof',
                ],
            ];
        }

        if ($producerSucceeded && $persistedItems === 0 && in_array(0, array_filter([
            $expectedItems,
            $producerExpectedItems,
            $producerSubjectsTotal,
        ], static fn (?int $value): bool => $value !== null), true)) {
            return [
                'lifecycle_state' => BaselineSnapshotLifecycleState::Incomplete->value,
                'completed_at' => null,
                'failed_at' => $producerRun->completed_at ?? $row->updated_at ?? $row->captured_at ?? $row->created_at,
                'completion_meta' => $completionMeta + [
                    'finalization_reason_code' => BaselineReasonCodes::CAPTURE_ZERO_SUBJECTS,
                ],
            ];
        }

        $reasonCode = $expectedItems !== null
            || $producerExpectedItems !== null
            || $producerRun !== null
            ? BaselineReasonCodes::SNAPSHOT_LEGACY_CONTRADICTORY
            : BaselineReasonCodes::SNAPSHOT_LEGACY_NO_PROOF;

        return [
            'lifecycle_state' => BaselineSnapshotLifecycleState::Incomplete->value,
            'completed_at' => null,
            'failed_at' => $row->updated_at ?? $row->captured_at ?? $row->created_at,
            'completion_meta' => $completionMeta + [
                'finalization_reason_code' => $reasonCode,
            ],
        ];
    }

    private function resolveProducerRunForSnapshot(int $snapshotId, int $workspaceId, int $profileId): ?object
    {
        $runs = DB::table('operation_runs')
            ->where('workspace_id', $workspaceId)
            ->where('type', OperationRunType::BaselineCapture->value)
            ->orderByDesc('completed_at')
            ->orderByDesc('id')
            ->limit(500)
            ->get(['id', 'outcome', 'completed_at', 'context']);

        foreach ($runs as $run) {
            $context = $this->decodeJson($run->context ?? null);
            $resultSnapshotId = $this->normalizeInteger(data_get($context, 'result.snapshot_id'));

            if ($resultSnapshotId === $snapshotId) {
                return $run;
            }
        }

        foreach ($runs as $run) {
            $context = $this->decodeJson($run->context ?? null);
            $runProfileId = $this->normalizeInteger(data_get($context, 'baseline_profile_id'));

            if ($runProfileId === $profileId) {
                return $run;
            }
        }

        return null;
    }

    /**
     * @return array<string, mixed>
     */
    private function decodeJson(mixed $value): array
    {
        if (is_array($value)) {
            return $value;
        }

        if (is_string($value) && trim($value) !== '') {
            $decoded = json_decode($value, true);

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

        return [];
    }

    private function normalizeInteger(mixed $value): ?int
    {
        if (is_int($value)) {
            return $value;
        }

        if (is_numeric($value)) {
            return (int) $value;
        }

        if (is_string($value) && ctype_digit(trim($value))) {
            return (int) trim($value);
        }

        return null;
    }

    private function hasIndex(string $indexName): bool
    {
        $driver = Schema::getConnection()->getDriverName();

        return match ($driver) {
            'pgsql' => DB::table('pg_indexes')
                ->where('schemaname', 'public')
                ->where('indexname', $indexName)
                ->exists(),
            'sqlite' => collect(DB::select("PRAGMA index_list('baseline_snapshots')"))
                ->contains(fn (object $index): bool => ($index->name ?? null) === $indexName),
            default => false,
        };
    }
};
