<?php

declare(strict_types=1);

namespace App\Services\Operations;

use App\Jobs\Concerns\BridgesFailedOperationRun;
use App\Support\Operations\OperationLifecyclePolicy;
use RuntimeException;

final class OperationLifecyclePolicyValidator
{
    public function __construct(
        private readonly OperationLifecyclePolicy $policy,
    ) {}

    /**
     * @return array{
     *     valid:bool,
     *     errors:array<int, string>,
     *     definitions:array<string, array<string, mixed>>
     * }
     */
    public function validate(): array
    {
        $errors = [];
        $definitions = [];

        foreach ($this->policy->coveredTypeNames() as $operationType) {
            $definition = $this->policy->definition($operationType);

            if ($definition === null) {
                $errors[] = sprintf('Missing lifecycle policy definition for [%s].', $operationType);

                continue;
            }

            $definitions[$operationType] = $definition;
            $jobClass = $this->policy->jobClass($operationType);

            if ($jobClass === null || ! class_exists($jobClass)) {
                $errors[] = sprintf('Lifecycle policy [%s] points to a missing job class.', $operationType);

                continue;
            }

            $timeout = $this->jobTimeoutSeconds($operationType);

            if (! is_int($timeout) || $timeout <= 0) {
                $errors[] = sprintf('Lifecycle policy [%s] requires an explicit positive job timeout.', $operationType);
            }

            if (! $this->jobFailsOnTimeout($operationType)) {
                $errors[] = sprintf('Lifecycle policy [%s] requires failOnTimeout=true.', $operationType);
            }

            if ($this->policy->requiresDirectFailedBridge($operationType) && ! $this->jobUsesDirectFailedBridge($operationType)) {
                $errors[] = sprintf('Lifecycle policy [%s] requires a direct failed-job bridge.', $operationType);
            }

            $retryAfter = $this->policy->queueRetryAfterSeconds($this->policy->queueConnection($operationType));
            $safetyMargin = $this->policy->retryAfterSafetyMarginSeconds();

            if (is_int($timeout) && is_int($retryAfter) && $timeout >= ($retryAfter - $safetyMargin)) {
                $errors[] = sprintf(
                    'Lifecycle policy [%s] has timeout %d which is not safely below retry_after %d (margin %d).',
                    $operationType,
                    $timeout,
                    $retryAfter,
                    $safetyMargin,
                );
            }

            $expectedMaxRuntime = $this->policy->expectedMaxRuntimeSeconds($operationType);

            if (is_int($expectedMaxRuntime) && is_int($retryAfter) && $expectedMaxRuntime >= ($retryAfter - $safetyMargin)) {
                $errors[] = sprintf(
                    'Lifecycle policy [%s] expected runtime %d is not safely below retry_after %d (margin %d).',
                    $operationType,
                    $expectedMaxRuntime,
                    $retryAfter,
                    $safetyMargin,
                );
            }
        }

        return [
            'valid' => $errors === [],
            'errors' => $errors,
            'definitions' => $definitions,
        ];
    }

    public function assertValid(): void
    {
        $result = $this->validate();

        if (($result['valid'] ?? false) === true) {
            return;
        }

        throw new RuntimeException(implode(' ', $result['errors'] ?? ['Lifecycle policy validation failed.']));
    }

    public function jobTimeoutSeconds(string $operationType): ?int
    {
        $jobClass = $this->policy->jobClass($operationType);

        if ($jobClass === null || ! class_exists($jobClass)) {
            return null;
        }

        $timeout = get_class_vars($jobClass)['timeout'] ?? null;

        return is_numeric($timeout) ? (int) $timeout : null;
    }

    public function jobFailsOnTimeout(string $operationType): bool
    {
        $jobClass = $this->policy->jobClass($operationType);

        if ($jobClass === null || ! class_exists($jobClass)) {
            return false;
        }

        return (bool) (get_class_vars($jobClass)['failOnTimeout'] ?? false);
    }

    public function jobUsesDirectFailedBridge(string $operationType): bool
    {
        $jobClass = $this->policy->jobClass($operationType);

        if ($jobClass === null || ! class_exists($jobClass)) {
            return false;
        }

        return in_array(BridgesFailedOperationRun::class, class_uses_recursive($jobClass), true);
    }
}
