<?php
namespace EasyChat\Library\Messenger;









class CronCadenceAnalyzer {

    private const MIN_INTERVAL_SECONDS = 40;
    private const MAX_INTERVAL_SECONDS = 100;
    private const STALE_AFTER_SECONDS = 180;
    private const RUN_TIMEOUT_SECONDS = 90;
    private const PROFILE_MIN_SECONDS = 10800;
    private const PROFILE_WINDOW_SECONDS = 86400;
    private const RECOVERY_WINDOW_SECONDS = 7200;

    public function analyze(array $history, ?int $now = null, array $profile = []): array {
        $now = $now !== null ? max(0, $now) : time();
        $records = $this->normalize($history);
        if (!$records) {
            return $this->result('missing', false, 0, 0, [], ['diagnosis' => 'not_observed']);
        }

        $last = $records[count($records) - 1];
        $last_started_at = (int)$last['started_at'];
        if ((string)$last['status'] === 'running' && $now - $last_started_at > self::RUN_TIMEOUT_SECONDS) {
            return $this->result('stalled', false, count($records), $last_started_at, [], ['diagnosis' => 'run_stalled']);
        }
        if ((string)$last['status'] === 'failed') {
            return $this->result('failed', false, count($records), $last_started_at, [], ['diagnosis' => 'last_run_failed']);
        }
        if ($now - $last_started_at > self::STALE_AFTER_SECONDS) {
            return $this->result('stale', false, count($records), $last_started_at, [], [
                'diagnosis' => 'no_recent_runs',
                'age_seconds' => max(0, $now - $last_started_at),
            ]);
        }

        $completed = array_values(array_filter($records, static function (array $record): bool {
            return in_array((string)$record['status'], ['success', 'failed'], true);
        }));
        if (!$completed) {
             
             
             
            return $this->result('running', false, 0, $last_started_at, [], ['diagnosis' => 'first_run_in_progress']);
        }
        if (count($completed) === 1) {
            return $this->result('observed_once', false, 1, $last_started_at, [], ['diagnosis' => 'collecting_baseline']);
        }
        if (count($completed) === 2) {
            $interval = (int)$completed[1]['started_at'] - (int)$completed[0]['started_at'];
            if ($interval < self::MIN_INTERVAL_SECONDS) {
                return $this->result('too_frequent', false, 2, $last_started_at, [$interval], [
                    'diagnosis' => 'too_frequent',
                    'observed_interval_seconds' => max(0, $interval),
                ]);
            }
            if ($interval > self::MAX_INTERVAL_SECONDS) {
                return $this->result('irregular', false, 2, $last_started_at, [$interval], [
                    'diagnosis' => $this->intervalDiagnosis($interval),
                    'observed_interval_seconds' => max(0, $interval),
                ]);
            }
            return $this->result('verifying', false, 2, $last_started_at, [$interval], ['diagnosis' => 'collecting_baseline']);
        }

         
         
         
        $recent = array_slice($completed, -3);
        $intervals = $this->intervals($recent);
        $stable_suffix_runs = $this->stableSuffixRuns($intervals);
        foreach (array_reverse($intervals) as $interval) {
            if ($interval < self::MIN_INTERVAL_SECONDS) {
                return $this->result('too_frequent', false, $stable_suffix_runs, $last_started_at, $intervals, [
                    'diagnosis' => 'too_frequent',
                    'observed_interval_seconds' => $this->median($intervals),
                ]);
            }
            if ($interval > self::MAX_INTERVAL_SECONDS) {
                $observed_interval = $this->median($intervals);
                return $this->result('irregular', false, $stable_suffix_runs, $last_started_at, $intervals, [
                    'diagnosis' => $this->intervalDiagnosis($observed_interval),
                    'observed_interval_seconds' => $observed_interval,
                ]);
            }
        }

        $profile_issue = $this->profileIssue($profile, $now);
        if ($profile_issue !== null) {
            return $this->result(
                (string)$profile_issue['state'],
                true,
                3,
                $last_started_at,
                $intervals,
                $profile_issue
            );
        }

        return $this->result('confirmed', true, 3, $last_started_at, $intervals, ['diagnosis' => 'healthy']);
    }

    private function normalize(array $history): array {
        $records = [];
        foreach ($history as $record) {
            if (!is_array($record)) {
                continue;
            }
            $started_at = max(0, (int)($record['started_at'] ?? 0));
            if ($started_at <= 0) {
                continue;
            }
            $status = (string)($record['status'] ?? 'running');
            if (!in_array($status, ['running', 'success', 'failed'], true)) {
                $status = 'failed';
            }
            $records[] = [
                'run_id' => substr((string)($record['run_id'] ?? ''), 0, 32),
                'started_at' => $started_at,
                'finished_at' => max(0, (int)($record['finished_at'] ?? 0)),
                'status' => $status,
                'exit_code' => isset($record['exit_code']) ? (int)$record['exit_code'] : null,
            ];
        }
        usort($records, static function (array $left, array $right): int {
            return $left['started_at'] <=> $right['started_at'];
        });
        return array_slice($records, -30);
    }

    private function intervals(array $records): array {
        $intervals = [];
        for ($index = 1, $count = count($records); $index < $count; $index++) {
            $interval = (int)$records[$index]['started_at'] - (int)$records[$index - 1]['started_at'];
            if ($interval > 0) {
                $intervals[] = $interval;
            }
        }
        return $intervals;
    }

    




    private function stableSuffixRuns(array $intervals): int {
        $runs = 1;
        foreach (array_reverse($intervals) as $interval) {
            if ($interval < self::MIN_INTERVAL_SECONDS || $interval > self::MAX_INTERVAL_SECONDS) {
                break;
            }
            $runs++;
        }
        return min(3, $runs);
    }

    private function median(array $values): int {
        $values = array_values(array_filter(array_map('intval', $values), static function (int $value): bool {
            return $value > 0;
        }));
        if (!$values) {
            return 0;
        }
        sort($values, SORT_NUMERIC);
        $middle = intdiv(count($values), 2);
        if (count($values) % 2 === 1) {
            return (int)$values[$middle];
        }
        return (int)round(((int)$values[$middle - 1] + (int)$values[$middle]) / 2);
    }

    private function intervalDiagnosis(int $seconds): string {
        if ($seconds > self::MAX_INTERVAL_SECONDS && $seconds <= 3900) {
            return 'fixed_interval';
        }
        if ($seconds > 3900) {
            return 'sparse_schedule';
        }
        return 'irregular_intervals';
    }

    private function profileIssue(array $profile, int $now): ?array {
        $first_seen_at = max(0, (int)($profile['first_seen_at'] ?? 0));
        if ($first_seen_at <= 0 || $now - $first_seen_at < self::PROFILE_MIN_SECONDS) {
            return null;
        }

        $quarters = $this->normalizeBuckets($profile['quarters'] ?? []);
        if ($this->recentCoverageRecovered($quarters, $now)) {
            return null;
        }

        $window_start = max($first_seen_at, $now - self::PROFILE_WINDOW_SECONDS);
        $window_seconds = max(60, $now - $window_start);
        $expected_runs = max(1, (int)floor($window_seconds / 60));
        $hours = $this->normalizeBuckets($profile['hours'] ?? []);
        $started_runs = 0;
        $active_hours = 0;
        foreach ($hours as $bucket) {
            $bucket_start = (int)$bucket[0];
            if ($bucket_start + 3600 <= $window_start || $bucket_start > $now) {
                continue;
            }
            $started = max(0, (int)$bucket[1]);
            $started_runs += $started;
            if ($started >= 20) {
                $active_hours++;
            }
        }
        $coverage = min(1, $started_runs / $expected_runs);
        $observed_hours = max(1, (int)ceil($window_seconds / 3600));

        if ($coverage < 0.55 && $active_hours <= max(1, (int)floor($observed_hours * 0.4))) {
            return [
                'state' => 'limited_hours',
                'diagnosis' => 'limited_hours',
                'coverage_percent' => (int)round($coverage * 100),
                'expected_runs' => $expected_runs,
                'profile_runs' => $started_runs,
                'observed_hours' => $observed_hours,
            ];
        }
        if ($window_seconds >= 21600 && $coverage < 0.8) {
            return [
                'state' => 'low_coverage',
                'diagnosis' => 'low_coverage',
                'coverage_percent' => (int)round($coverage * 100),
                'expected_runs' => $expected_runs,
                'profile_runs' => $started_runs,
                'observed_hours' => $observed_hours,
            ];
        }
        return null;
    }

    private function recentCoverageRecovered(array $quarters, int $now): bool {
        $window_start = $now - self::RECOVERY_WINDOW_SECONDS;
        $active_buckets = 0;
        $started_runs = 0;
        $failed_runs = 0;
        foreach ($quarters as $bucket) {
            $bucket_start = (int)$bucket[0];
             
             
             
             
            if ($bucket_start + 900 <= $window_start || $bucket_start > $now) {
                continue;
            }
            $started = max(0, (int)$bucket[1]);
            $started_runs += $started;
            $failed_runs += max(0, (int)$bucket[3]);
            if ($started > 0) {
                $active_buckets++;
            }
        }
        return $active_buckets >= 8 && $started_runs >= 100 && $failed_runs <= 2;
    }

    private function normalizeBuckets($buckets): array {
        if (!is_array($buckets)) {
            return [];
        }
        $normalized = [];
        foreach ($buckets as $bucket) {
            if (!is_array($bucket) || max(0, (int)($bucket[0] ?? 0)) <= 0) {
                continue;
            }
            $normalized[] = [
                max(0, (int)$bucket[0]),
                max(0, (int)($bucket[1] ?? 0)),
                max(0, (int)($bucket[2] ?? 0)),
                max(0, (int)($bucket[3] ?? 0)),
            ];
        }
        usort($normalized, static function (array $left, array $right): int {
            return (int)$left[0] <=> (int)$right[0];
        });
        return $normalized;
    }

    private function result(string $state, bool $confirmed, int $observed, int $last_started_at, array $intervals, array $extra = []): array {
        $attention_states = ['stale', 'failed', 'stalled', 'too_frequent', 'irregular', 'limited_hours', 'low_coverage'];
        return array_merge([
            'state' => $state,
            'confirmed' => $confirmed,
            'observed_runs' => max(0, $observed),
            'required_runs' => 3,
            'last_started_at' => max(0, $last_started_at),
            'intervals' => array_values(array_map('intval', $intervals)),
            'requires_attention' => in_array($state, $attention_states, true),
            'severity' => in_array($state, ['failed', 'stalled'], true) ? 'error' : 'warning',
        ], $extra);
    }
}
