<?php
/**
 * WinRacing V63.0 — Race-Day Shadow Run Validator
 * Executes a deterministic, read-only shadow validation over the certified V62 payload.
 * No DB writes, schema changes, score mutation or live-weight changes.
 */
final class ChairmanShadowRunValidatorV63
{
    public function validate(array $source, array $readiness, $raceDate = '', $venue = '')
    {
        $checks = array();
        $findings = array();
        $summary = isset($readiness['readiness_summary']) && is_array($readiness['readiness_summary'])
            ? $readiness['readiness_summary'] : array();

        $this->check($checks, $findings, 'V62_CERTIFIED',
            isset($summary['readiness_state']) && $summary['readiness_state'] === 'PASS',
            'CRITICAL', 'V62 production readiness is not PASS.');

        $this->check($checks, $findings, 'PRODUCTION_GATE_READY',
            isset($summary['production_gate']) && $summary['production_gate'] === 'READY',
            'CRITICAL', 'V62 production gate is not READY.');

        $ranking = isset($source['final_chairman_ranking']) && is_array($source['final_chairman_ranking'])
            ? $source['final_chairman_ranking'] : array();
        $dashboard = isset($source['ultimate_dashboard']) && is_array($source['ultimate_dashboard'])
            ? $source['ultimate_dashboard'] : array();

        $this->check($checks, $findings, 'DECISION_SET_AVAILABLE',
            count($ranking) > 0 || count($dashboard) > 0,
            'CRITICAL', 'No decision set is available for shadow validation.');

        $normalized = $this->normalizeDecisionSet($ranking, $dashboard);
        $this->check($checks, $findings, 'UNIQUE_RACE_KEYS',
            $this->hasUniqueKeys($normalized),
            'CRITICAL', 'Duplicate race/horse keys were detected.');

        $this->check($checks, $findings, 'SCORE_RANGE',
            $this->scoresWithinRange($normalized),
            'CRITICAL', 'A decision score is outside the accepted 0–100 range.');

        $fingerprintA = $this->fingerprint($source);
        $fingerprintB = $this->fingerprint($source);
        $this->check($checks, $findings, 'DETERMINISTIC_REPLAY',
            $fingerprintA !== '' && hash_equals($fingerprintA, $fingerprintB),
            'CRITICAL', 'Repeated shadow serialization is not deterministic.');

        $this->check($checks, $findings, 'RESULTS_STILL_ISOLATED',
            !$this->containsTrueFlag($source, array('results_used','result_available')),
            'CRITICAL', 'Result data became available inside the pre-race shadow payload.');

        $this->check($checks, $findings, 'READ_ONLY_CONTRACT',
            $this->immutable($source, $readiness),
            'CRITICAL', 'A prohibited mutation flag was detected.');

        $decisionCount = count($normalized);
        $this->check($checks, $findings, 'MINIMUM_DECISION_COVERAGE',
            $decisionCount >= 5,
            'WARNING', 'Shadow decision coverage is below five entries.');

        $critical = 0; $warning = 0;
        foreach ($findings as $finding) {
            if ($finding['severity'] === 'CRITICAL') { $critical++; }
            elseif ($finding['severity'] === 'WARNING') { $warning++; }
        }

        $score = max(0, 100 - ($critical * 35) - ($warning * 8));
        $state = $critical > 0 ? 'FAIL' : ($warning > 0 ? 'REVIEW' : 'PASS');
        $gate = $critical > 0 ? 'BLOCKED' : ($warning > 0 ? 'SHADOW_READY_WITH_REVIEW' : 'SHADOW_READY');

        return array(
            'status' => 'OK',
            'engine' => 'V63',
            'engine_version' => '63.0.0',
            'race_date' => (string)$raceDate,
            'venue' => (string)$venue,
            'mode' => 'READ_ONLY_SHADOW',
            'live_weights_changed' => false,
            'database_schema_changed' => false,
            'source_decisions_changed' => false,
            'shadow_summary' => array(
                'shadow_state' => $state,
                'shadow_gate' => $gate,
                'shadow_score' => round($score, 1),
                'check_count' => count($checks),
                'passed_count' => count($checks) - count($findings),
                'critical_count' => $critical,
                'warning_count' => $warning,
                'decision_count' => $decisionCount,
                'source_fingerprint' => $fingerprintA,
                'deterministic_replay' => $fingerprintA !== '' && hash_equals($fingerprintA, $fingerprintB),
                'v62_readiness_state' => isset($summary['readiness_state']) ? $summary['readiness_state'] : 'UNKNOWN',
                'v62_readiness_score' => isset($summary['readiness_score']) ? $summary['readiness_score'] : 0
            ),
            'checks' => $checks,
            'findings' => $findings,
            'shadow_contract' => array(
                'pre_race_only' => true,
                'v62_pass_required' => true,
                'deterministic_replay_required' => true,
                'db_write_permitted' => false,
                'schema_change_permitted' => false,
                'live_weight_change_permitted' => false,
                'production_decisions_changed' => false,
                'next_stage_permitted' => $critical === 0
            )
        );
    }

    private function normalizeDecisionSet(array $ranking, array $dashboard)
    {
        $source = count($ranking) > 0 ? $ranking : $dashboard;
        $out = array();
        foreach ($source as $index => $row) {
            if (!is_array($row)) { continue; }
            $race = isset($row['race_num']) ? $row['race_num'] : (isset($row['race_number']) ? $row['race_number'] : $index + 1);
            $horse = isset($row['horse_num']) ? $row['horse_num'] : (isset($row['horse_number']) ? $row['horse_number'] : (isset($row['banker_horse']) ? $row['banker_horse'] : ''));
            $score = $this->firstNumeric($row, array('final_score','chairman_score','score','confidence','confidence_pct','ranking_score'));
            $out[] = array('race'=>(string)$race,'horse'=>(string)$horse,'score'=>$score);
        }
        return $out;
    }

    private function firstNumeric(array $row, array $keys)
    {
        foreach ($keys as $key) {
            if (isset($row[$key]) && is_numeric($row[$key])) { return (float)$row[$key]; }
        }
        return null;
    }

    private function hasUniqueKeys(array $rows)
    {
        $seen = array();
        foreach ($rows as $row) {
            $key = $row['race'] . '|' . $row['horse'];
            if (isset($seen[$key])) { return false; }
            $seen[$key] = true;
        }
        return true;
    }

    private function scoresWithinRange(array $rows)
    {
        foreach ($rows as $row) {
            if ($row['score'] === null) { continue; }
            if ($row['score'] < 0 || $row['score'] > 100 || is_nan($row['score']) || is_infinite($row['score'])) { return false; }
        }
        return true;
    }

    private function fingerprint(array $payload)
    {
        $copy = $payload;
        $this->ksortRecursive($copy);
        $json = json_encode($copy, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        return is_string($json) ? hash('sha256', $json) : '';
    }

    private function ksortRecursive(&$value)
    {
        if (!is_array($value)) { return; }
        if ($this->isAssoc($value)) { ksort($value); }
        foreach ($value as &$child) { $this->ksortRecursive($child); }
        unset($child);
    }

    private function isAssoc(array $array)
    {
        $i = 0;
        foreach ($array as $key => $unused) {
            if ($key !== $i++) { return true; }
        }
        return false;
    }

    private function check(array &$checks, array &$findings, $name, $passed, $severity, $message)
    {
        $checks[] = array('check'=>$name,'status'=>$passed?'PASS':'FAIL','severity'=>$severity);
        if (!$passed) { $findings[] = array('type'=>$name,'severity'=>$severity,'message'=>$message); }
    }

    private function containsTrueFlag(array $data, array $keys)
    {
        foreach ($data as $key => $value) {
            $k = is_string($key) ? strtolower($key) : '';
            if (in_array($k, $keys, true) && $value === true) { return true; }
            if (is_array($value) && $this->containsTrueFlag($value, $keys)) { return true; }
        }
        return false;
    }

    private function immutable(array $source, array $readiness)
    {
        foreach (array($source, $readiness) as $payload) {
            if (isset($payload['live_weights_changed']) && $payload['live_weights_changed'] === true) { return false; }
            if (isset($payload['database_schema_changed']) && $payload['database_schema_changed'] === true) { return false; }
            if (isset($payload['source_decisions_changed']) && $payload['source_decisions_changed'] === true) { return false; }
        }
        return true;
    }
}
