<?php

declare(strict_types=1);

namespace App\Services\SupportCenter;

use App\Enums\SupportCenter\HostSystemUpdateExecutionStep;
use App\Exceptions\SupportCenter\HostSystemUpdatePostApplyException;
use App\Models\HostSystemUpdateExecution;
use Illuminate\Database\Migrations\Migrator;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Throwable;

/**
 * 系统更新：按小批次执行 pending 迁移（每步一次 HTTP 请求，参考 WebInstallMigrationRunner）。
 */
final class HostSystemUpdateMigrationBatchRunner
{
    public function __construct(
        private readonly Migrator $migrator,
        private readonly HostSystemUpdateExecutionContextResolver $contextResolver,
        private readonly HostSystemUpdateArtisanCommandRunner $commandRunner,
        private readonly HostSystemUpdateExecutionLogger $executionLogger,
        private readonly SupportCenterAuditLogger $auditLogger,
    ) {}

    public function runNextBatch(HostSystemUpdateExecution $execution): HostSystemUpdateMigrationBatchResult
    {
        $startedAt = now()->utc();
        $batchSize = $this->batchSize();
        $package = $this->contextResolver->resolveReadyPackage($execution);
        $manifest = $this->contextResolver->manifest($execution);
        $declaredPaths = $this->declaredMigrationPaths($manifest);

        $repository = $this->migrator->getRepository();

        if (! $repository->repositoryExists()) {
            $repository->createRepository();
        }

        $paths = $this->resolveMigrationPaths($declaredPaths);
        /** @var array<string, string> $files */
        $files = $this->migrator->getMigrationFiles($paths);
        $ran = $repository->getRan();

        /** @var list<string> $pendingFiles */
        $pendingFiles = (new Collection($files))
            ->reject(fn (string $file, string $name): bool => in_array($name, $ran, true))
            ->values()
            ->all();

        $total = count($files);
        $completed = count(array_intersect(array_keys($files), $ran));
        $pending = count($pendingFiles);

        if ($pending === 0) {
            $progress = new HostSystemUpdateMigrationProgress(
                total: $total,
                completed: $completed,
                pending: 0,
                current: null,
                batchSize: $batchSize,
            );

            return new HostSystemUpdateMigrationBatchResult(
                stageCompleted: true,
                progress: $progress,
            );
        }

        if (! config('dberp.system_update.use_migrator_batches', true)) {
            return $this->runSingleArtisanMigrate($execution, $total, $completed, $pending, $batchSize, $startedAt);
        }

        /** @var list<string> $batchFiles */
        $batchFiles = array_slice($pendingFiles, 0, $batchSize);
        $batchNames = array_map(
            fn (string $file): string => $this->migrator->getMigrationName($file),
            $batchFiles,
        );

        try {
            $this->migrator->requireFiles($batchFiles);
            $this->migrator->runPending($batchFiles, ['pretend' => false]);
        } catch (Throwable $exception) {
            $this->executionLogger->logStepFailed(
                $execution,
                HostSystemUpdateExecutionStep::RunMigrations->value,
                (string) __('dberp.system_update.post_apply.log.migration_failed'),
                'host_system_update_migration_failed',
                HostSystemUpdatePostApplyException::migrationFailed()->userFacingMessage(),
                ['attempted' => $batchNames],
                $startedAt,
            );
            $this->auditLogger->logSystemUpdateMigrationFailed(
                $execution,
                $package,
                HostSystemUpdatePostApplyException::migrationFailed(),
            );

            throw HostSystemUpdatePostApplyException::migrationFailed();
        }

        $ranAfter = $repository->getRan();
        $completedAfter = count(array_intersect(array_keys($files), $ranAfter));
        $pendingAfter = max(0, $total - $completedAfter);

        $progress = new HostSystemUpdateMigrationProgress(
            total: $total,
            completed: $completedAfter,
            pending: $pendingAfter,
            current: $batchNames !== [] ? $batchNames[array_key_last($batchNames)] : null,
            batchSize: $batchSize,
        );

        $this->executionLogger->logTransition(
            $execution,
            HostSystemUpdateExecutionStep::RunMigrations->value,
            (string) __('dberp.system_update.post_apply.log.migration_batch_completed', [
                'count' => count($batchNames),
                'pending' => $pendingAfter,
            ]),
            [
                'executed' => $batchNames,
                'pending' => $pendingAfter,
            ],
        );

        return new HostSystemUpdateMigrationBatchResult(
            stageCompleted: $pendingAfter === 0,
            progress: $progress,
            executedMigrationNames: $batchNames,
        );
    }

    public function snapshotProgress(HostSystemUpdateExecution $execution): HostSystemUpdateMigrationProgress
    {
        $batchSize = $this->batchSize();
        $manifest = $this->contextResolver->manifest($execution);
        $paths = $this->resolveMigrationPaths($this->declaredMigrationPaths($manifest));
        $files = $this->migrator->getMigrationFiles($paths);
        $repository = $this->migrator->getRepository();

        if (! $repository->repositoryExists()) {
            $total = count($files);

            return new HostSystemUpdateMigrationProgress(
                total: $total,
                completed: 0,
                pending: $total,
                current: null,
                batchSize: $batchSize,
            );
        }

        $ran = $repository->getRan();
        $total = count($files);
        $completed = count(array_intersect(array_keys($files), $ran));

        return new HostSystemUpdateMigrationProgress(
            total: $total,
            completed: $completed,
            pending: max(0, $total - $completed),
            current: null,
            batchSize: $batchSize,
        );
    }

    public function logMigrationStageCompleted(
        HostSystemUpdateExecution $execution,
        HostSystemUpdateMigrationProgress $progress,
    ): void {
        $package = $this->contextResolver->resolveReadyPackage($execution);
        $startedAt = now()->utc();

        $this->executionLogger->logStepCompleted(
            $execution,
            HostSystemUpdateExecutionStep::RunMigrations->value,
            (string) __('dberp.system_update.post_apply.log.migration_completed'),
            $progress->toArray(),
            $startedAt,
        );

        $this->auditLogger->logSystemUpdateMigrationCompleted(
            $execution,
            $package,
            new HostSystemUpdateArtisanCommandResult(
                command: 'migrate',
                arguments: ['--force', '--no-interaction'],
                exitCode: 0,
                stdout: '',
                stderr: '',
                startedAt: $startedAt,
                finishedAt: now()->utc(),
                durationMs: 0,
            ),
        );
    }

    public function batchSize(): int
    {
        return max(1, (int) config('dberp.system_update.migration_batch_size', 10));
    }

    private function runSingleArtisanMigrate(
        HostSystemUpdateExecution $execution,
        int $total,
        int $completed,
        int $pending,
        int $batchSize,
        Carbon $startedAt,
    ): HostSystemUpdateMigrationBatchResult {
        try {
            $result = $this->commandRunner->runMigrate();

            if (! $result->succeeded()) {
                throw HostSystemUpdatePostApplyException::migrationFailed();
            }
        } catch (Throwable $exception) {
            if (! $exception instanceof HostSystemUpdatePostApplyException) {
                throw HostSystemUpdatePostApplyException::migrationFailed();
            }

            throw $exception;
        }

        $progress = new HostSystemUpdateMigrationProgress(
            total: $total,
            completed: $total,
            pending: 0,
            current: null,
            batchSize: $batchSize,
        );

        $this->executionLogger->logTransition(
            $execution,
            HostSystemUpdateExecutionStep::RunMigrations->value,
            (string) __('dberp.system_update.post_apply.log.migration_completed'),
            $progress->toArray(),
        );

        return new HostSystemUpdateMigrationBatchResult(
            stageCompleted: true,
            progress: $progress,
        );
    }

    /**
     * @param  list<string>  $declaredPaths
     * @return list<string>
     */
    private function resolveMigrationPaths(array $declaredPaths): array
    {
        if ($declaredPaths === []) {
            return array_values(array_unique(array_merge(
                $this->migrator->paths(),
                [database_path('migrations')],
            )));
        }

        $absolute = [];

        foreach ($declaredPaths as $path) {
            $normalized = str_replace('\\', '/', trim($path));
            $absolute[] = str_starts_with($normalized, '/')
                || preg_match('/^[A-Za-z]:\\//', $normalized) === 1
                ? $normalized
                : base_path($normalized);
        }

        return array_values(array_unique($absolute));
    }

    /**
     * @param  array<string, mixed>  $manifest
     * @return list<string>
     */
    private function declaredMigrationPaths(array $manifest): array
    {
        $migrations = $manifest[HostSystemUpdateManifestContract::MIGRATIONS] ?? [];

        if (! is_array($migrations)) {
            return [];
        }

        return array_values(array_filter($migrations, static fn (mixed $path): bool => is_string($path) && $path !== ''));
    }
}
