<?php

declare(strict_types=1);

namespace App\Support\OpsUx;

use App\Models\OperationRun;
use App\Support\OperationRunStatus;

final class OperationRunProgressContract
{
    public const string NONE = 'none';

    public const string ACTIVITY = 'activity';

    public const string COUNTED = 'counted';

    public const string PHASED = 'phased';

    public const string COMPOSITE = 'composite';

    /**
     * @return array{
     *     capability: string,
     *     display: string,
     *     label: ?string,
     *     processed: ?int,
     *     total: ?int,
     *     percent: ?int
     * }
     */
    public static function forRun(OperationRun $run): array
    {
        $summaryCounts = SummaryCountsNormalizer::normalize(
            is_array($run->summary_counts) ? $run->summary_counts : [],
        );

        $context = is_array($run->context) ? $run->context : [];
        $capability = self::capabilityForRun($run, $summaryCounts, $context);

        return match ($capability) {
            self::COUNTED => self::countedModel($summaryCounts),
            self::PHASED => self::phasedModel($run, $context),
            self::COMPOSITE => self::compositeModel($run, $summaryCounts, $context),
            self::ACTIVITY => self::indeterminateModel(
                self::ACTIVITY,
                (string) $run->status === OperationRunStatus::Queued->value
                    ? 'Waiting for worker.'
                    : 'Progress details pending.',
            ),
            default => self::noneModel(),
        };
    }

    /**
     * @param  array<string, int>  $summaryCounts
     * @param  array<string, mixed>  $context
     */
    private static function capabilityForRun(OperationRun $run, array $summaryCounts, array $context): string
    {
        if (! $run->isCurrentlyActive()) {
            return self::NONE;
        }

        if ((string) $run->status === OperationRunStatus::Queued->value) {
            return self::ACTIVITY;
        }

        if (self::hasPhasedHint($context)) {
            return self::PHASED;
        }

        if (self::hasCompositeHint($summaryCounts, $context)) {
            return self::COMPOSITE;
        }

        if (self::hasCountedHint($summaryCounts)) {
            return self::COUNTED;
        }

        return self::ACTIVITY;
    }

    /**
     * @param  array<string, int>  $summaryCounts
     */
    private static function hasCountedHint(array $summaryCounts): bool
    {
        $total = $summaryCounts['total'] ?? null;
        $processed = $summaryCounts['processed'] ?? null;

        return is_int($total)
            && $total > 0
            && is_int($processed);
    }

    /**
     * @param  array<string, int>  $summaryCounts
     * @return array{
     *     capability: string,
     *     display: string,
     *     label: string,
     *     processed: int,
     *     total: int,
     *     percent: int
     * }
     */
    private static function countedModel(array $summaryCounts): array
    {
        $total = max(1, (int) ($summaryCounts['total'] ?? 0));
        $processed = min(max(0, (int) ($summaryCounts['processed'] ?? 0)), $total);
        $percent = max(0, min(100, (int) round(($processed / $total) * 100)));

        return [
            'capability' => self::COUNTED,
            'display' => self::COUNTED,
            'label' => sprintf('%d / %d processed (%d%%)', $processed, $total, $percent),
            'processed' => $processed,
            'total' => $total,
            'percent' => $percent,
        ];
    }

    /**
     * @param  array<string, mixed>  $context
     * @return array{
     *     capability: string,
     *     display: string,
     *     label: string,
     *     processed: null,
     *     total: null,
     *     percent: null
     * }
     */
    private static function phasedModel(OperationRun $run, array $context): array
    {
        $phase = self::phaseProgressMetadata($context);

        if ($phase !== null) {
            return self::indeterminateModel(self::PHASED, $phase['label']);
        }

        return self::indeterminateModel(
            self::PHASED,
            self::legacyPhasedLabel($run, $context) ?? 'Phase progress pending.',
        );
    }

    /**
     * @param  array<string, int>  $summaryCounts
     * @param  array<string, mixed>  $context
     * @return array{
     *     capability: string,
     *     display: string,
     *     label: string,
     *     processed: null,
     *     total: null,
     *     percent: null
     * }
     */
    private static function compositeModel(OperationRun $run, array $summaryCounts, array $context): array
    {
        $label = self::explicitCompositeLabel($context)
            ?? self::legacyCompositeLabel($run, $summaryCounts, $context)
            ?? 'Composite progress pending.';

        return self::indeterminateModel(self::COMPOSITE, $label);
    }

    /**
     * @return array{
     *     capability: string,
     *     display: string,
     *     label: string,
     *     processed: null,
     *     total: null,
     *     percent: null
     * }
     */
    private static function indeterminateModel(string $capability, string $label): array
    {
        return [
            'capability' => $capability,
            'display' => 'indeterminate',
            'label' => $label,
            'processed' => null,
            'total' => null,
            'percent' => null,
        ];
    }

    /**
     * @return array{
     *     capability: string,
     *     display: string,
     *     label: null,
     *     processed: null,
     *     total: null,
     *     percent: null
     * }
     */
    private static function noneModel(): array
    {
        return [
            'capability' => self::NONE,
            'display' => self::NONE,
            'label' => null,
            'processed' => null,
            'total' => null,
            'percent' => null,
        ];
    }

    /**
     * @param  array<string, mixed>  $context
     */
    private static function hasPhasedHint(array $context): bool
    {
        if (self::phaseProgressMetadata($context) !== null) {
            return true;
        }

        foreach (['baseline_capture.evidence_capture', 'baseline_compare.evidence_capture'] as $path) {
            $phaseStats = data_get($context, $path);

            if (is_array($phaseStats) && self::looksLikePhaseStats($phaseStats)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param  array<string, mixed>  $phaseStats
     */
    private static function looksLikePhaseStats(array $phaseStats): bool
    {
        return count(array_intersect(
            array_keys($phaseStats),
            ['requested', 'succeeded', 'skipped', 'failed', 'throttled'],
        )) >= 2;
    }

    /**
     * @param  array<string, int>  $summaryCounts
     * @param  array<string, mixed>  $context
     */
    private static function hasCompositeHint(array $summaryCounts, array $context): bool
    {
        if (self::explicitCompositeLabel($context) !== null) {
            return true;
        }

        $operationCount = $summaryCounts['operation_count'] ?? null;

        if (is_int($operationCount) && $operationCount > 1) {
            return true;
        }

        foreach (['child_run_ids', 'operation_run_ids'] as $path) {
            $runIds = data_get($context, $path);

            if (! is_array($runIds)) {
                continue;
            }

            $numericIds = array_filter($runIds, static fn (mixed $runId): bool => is_numeric($runId));

            if (count($numericIds) > 1) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param  array<string, mixed>  $context
     * @return array{key: string, label: string}|null
     */
    private static function phaseProgressMetadata(array $context): ?array
    {
        $phase = data_get($context, 'progress.phase');

        if (! is_array($phase)) {
            return null;
        }

        $key = self::cleanString($phase['key'] ?? null);

        if ($key === null || ! in_array($key, ['preparing', 'fetching', 'processing', 'persisting', 'finalizing'], true)) {
            return null;
        }

        $label = self::cleanString($phase['label'] ?? null) ?? self::defaultPhaseLabel($key);

        return [
            'key' => $key,
            'label' => $label,
        ];
    }

    private static function defaultPhaseLabel(string $key): string
    {
        return match ($key) {
            'preparing' => 'Preparing work.',
            'fetching' => 'Collecting required evidence.',
            'processing' => 'Processing current work.',
            'persisting' => 'Saving results.',
            'finalizing' => 'Finalizing operation.',
            default => 'Phase progress pending.',
        };
    }

    /**
     * @param  array<string, mixed>  $context
     */
    private static function explicitCompositeLabel(array $context): ?string
    {
        return self::cleanString(data_get($context, 'progress.composite.label'));
    }

    /**
     * @param  array<string, mixed>  $context
     */
    private static function legacyPhasedLabel(OperationRun $run, array $context): ?string
    {
        return match ((string) $run->type) {
            'baseline_capture' => is_array(data_get($context, 'baseline_capture.evidence_capture'))
                ? 'Capturing evidence.'
                : null,
            'baseline_compare' => is_array(data_get($context, 'baseline_compare.evidence_capture'))
                ? 'Refreshing comparison evidence.'
                : null,
            default => null,
        };
    }

    /**
     * @param  array<string, int>  $summaryCounts
     * @param  array<string, mixed>  $context
     */
    private static function legacyCompositeLabel(OperationRun $run, array $summaryCounts, array $context): ?string
    {
        if ((string) $run->type !== 'environment.review.compose') {
            return null;
        }

        $operationCount = self::intOrNull($summaryCounts['operation_count'] ?? data_get($context, 'progress.composite.operation_count'));
        $failedCount = self::intOrNull(data_get($context, 'progress.composite.failed_count'));
        $partialCount = self::intOrNull(data_get($context, 'progress.composite.partial_count'));

        $baseLabel = $operationCount !== null && $operationCount > 0
            ? sprintf('Review composition is aggregating %d %s.', $operationCount, $operationCount === 1 ? 'operation' : 'operations')
            : 'Review composition is aggregating related operations.';

        $attentionLabel = self::compositeAttentionLabel($failedCount, $partialCount);

        return $attentionLabel === null
            ? $baseLabel
            : sprintf('%s %s', $baseLabel, $attentionLabel);
    }

    private static function compositeAttentionLabel(?int $failedCount, ?int $partialCount): ?string
    {
        $failedCount = $failedCount !== null && $failedCount > 0 ? $failedCount : null;
        $partialCount = $partialCount !== null && $partialCount > 0 ? $partialCount : null;

        if ($failedCount === null && $partialCount === null) {
            return null;
        }

        if ($failedCount !== null && $partialCount !== null) {
            return sprintf(
                '%d %s and %d %s currently need review.',
                $failedCount,
                $failedCount === 1 ? 'failed operation' : 'failed operations',
                $partialCount,
                $partialCount === 1 ? 'partial operation' : 'partial operations',
            );
        }

        if ($failedCount !== null) {
            return sprintf(
                '%d %s currently need review.',
                $failedCount,
                $failedCount === 1 ? 'failed operation' : 'failed operations',
            );
        }

        return sprintf(
            '%d %s currently need review.',
            $partialCount,
            $partialCount === 1 ? 'partial operation' : 'partial operations',
        );
    }

    private static function cleanString(mixed $value): ?string
    {
        if (! is_string($value)) {
            return null;
        }

        $value = trim($value);

        return $value === '' ? null : $value;
    }

    private static function intOrNull(mixed $value): ?int
    {
        return is_int($value) ? $value : null;
    }
}