<?php
/**
 * WinRacing V51.8 — Backtest + Audit Release
 * PHP 7.4 compatible. Read-only DB integration: no CREATE/ALTER/DROP.
 *
 * V50 remains the immutable source. V51 adds a second-layer decision model:
 * calibration, cross-race normalization, re-ranking, risk control and exposure.
 */
class ChairmanSupremeDecisionEngineV51
{
    private $db;
    private $snapshotDir;

    public function __construct(mysqli $db, $snapshotDir = '')
    {
        $this->db = $db;
        $this->db->set_charset('utf8mb4');
        $this->snapshotDir = $snapshotDir !== ''
            ? rtrim($snapshotDir, '/\\')
            : dirname(__DIR__, 2) . '/runtime/cache/v51';
    }

    private function q($value) { return "'" . $this->db->real_escape_string((string)$value) . "'"; }
    private function num($value, $default = 0.0) { return is_numeric($value) ? (float)$value : (float)$default; }
    private function clamp($value, $min = 0.0, $max = 100.0) { return max($min, min($max, $value)); }

    private function rows($sql)
    {
        $rows = array();
        $result = $this->db->query($sql);
        if ($result instanceof mysqli_result) {
            while ($row = $result->fetch_assoc()) { $rows[] = $row; }
            $result->free();
        }
        return $rows;
    }

    private function tableExists($table)
    {
        $safe = $this->db->real_escape_string((string)$table);
        $result = $this->db->query("SHOW TABLES LIKE '" . $safe . "'");
        if (!($result instanceof mysqli_result)) { return false; }
        $exists = $result->num_rows > 0;
        $result->free();
        return $exists;
    }

    private function latestV50Run($date, $venue)
    {
        if (!$this->tableExists('wr_chairman_ultimate_run_log_v50')) { return ''; }
        $where = array('race_date=' . $this->q($date));
        if ($venue !== '') { $where[] = 'venue=' . $this->q($venue); }
        $rows = $this->rows(
            'SELECT run_key FROM wr_chairman_ultimate_run_log_v50 WHERE ' .
            implode(' AND ', $where) . ' ORDER BY created_at DESC,id DESC LIMIT 1'
        );
        return isset($rows[0]['run_key']) ? (string)$rows[0]['run_key'] : '';
    }


    private function tableColumns($table)
    {
        $columns = array();
        if (!$this->tableExists($table)) { return $columns; }
        $result = $this->db->query('SHOW COLUMNS FROM `' . str_replace('`', '', (string)$table) . '`');
        if ($result instanceof mysqli_result) {
            while ($row = $result->fetch_assoc()) { $columns[] = (string)$row['Field']; }
            $result->free();
        }
        return $columns;
    }

    private function resultRows($date, $venue)
    {
        if (!$this->tableExists('racing_results')) { return array(); }
        $cols = $this->tableColumns('racing_results');
        $raceCol = in_array('race_number', $cols, true) ? 'race_number' : (in_array('race_num', $cols, true) ? 'race_num' : '');
        $dateCol = in_array('race_date', $cols, true) ? 'race_date' : '';
        if ($raceCol === '' || $dateCol === '') { return array(); }
        $select = array($raceCol . ' AS race_num');
        foreach (array('win_horse','place_horse','third_horse','fourth_horse') as $c) {
            $select[] = in_array($c, $cols, true) ? ($c . ' AS ' . $c) : ("'' AS " . $c);
        }
        if (in_array('venue', $cols, true)) { $select[] = 'venue'; }
        $where = array($dateCol . '=' . $this->q($date));
        if ($venue !== '' && in_array('venue', $cols, true)) { $where[] = 'venue=' . $this->q($venue); }
        return $this->rows('SELECT ' . implode(',', $select) . ' FROM racing_results WHERE ' . implode(' AND ', $where) . ' ORDER BY ' . $raceCol);
    }

    private function buildBacktest(array $races, $date, $venue)
    {
        $resultRows = $this->resultRows($date, $venue);
        $byRace = array();
        foreach ($resultRows as $rr) { $byRace[(int)$rr['race_num']] = $rr; }
        $audited = array();
        $stats = array('evaluated'=>0,'banker_win'=>0,'banker_top2'=>0,'banker_top3'=>0,'banker_top4'=>0,'primary_count'=>0,'primary_top3'=>0,'secondary_count'=>0,'secondary_top3'=>0,'watch_count'=>0,'watch_top3'=>0,'skip_count'=>0,'skip_top3'=>0,'exposure_units'=>0.0,'exposure_top3_units'=>0.0);
        foreach ($races as $race) {
            $rn = (int)$race['race_num'];
            $banker = trim((string)$race['banker_horse']);
            $action = isset($race['supreme_action']) ? strtoupper((string)$race['supreme_action']) : 'SKIP';
            $row = isset($byRace[$rn]) ? $byRace[$rn] : null;
            $finish = array();
            if ($row) {
                foreach (array('win_horse','place_horse','third_horse','fourth_horse') as $c) {
                    $v = trim((string)$row[$c]); if ($v !== '') { $finish[] = $v; }
                }
            }
            $position = 0;
            foreach ($finish as $i=>$horse) { if ((string)$horse === (string)$banker) { $position=$i+1; break; } }
            $evaluated = !empty($finish);
            $hit3 = $position > 0 && $position <= 3;
            $units = isset($race['portfolio_exposure_units']) ? (float)$race['portfolio_exposure_units'] : 0.0;
            if ($evaluated) {
                $stats['evaluated']++;
                if ($position===1) $stats['banker_win']++;
                if ($position>0 && $position<=2) $stats['banker_top2']++;
                if ($hit3) $stats['banker_top3']++;
                if ($position>0 && $position<=4) $stats['banker_top4']++;
                $key=strtolower($action).'_count'; if(isset($stats[$key])) $stats[$key]++;
                $key2=strtolower($action).'_top3'; if($hit3 && isset($stats[$key2])) $stats[$key2]++;
                $stats['exposure_units'] += $units;
                if ($hit3) $stats['exposure_top3_units'] += $units;
            }
            $audited[] = array(
                'race_num'=>$rn,'banker_horse'=>$banker,'supreme_rank'=>(int)$race['supreme_rank'],
                'tier'=>(string)$race['supreme_tier'],'action'=>$action,'exposure_units'=>round($units,2),
                'result_available'=>$evaluated,'finish_order'=>$finish,'banker_finish_position'=>$position,
                'banker_win'=>$position===1,'banker_top2'=>$position>0&&$position<=2,
                'banker_top3'=>$hit3,'banker_top4'=>$position>0&&$position<=4,
                'audit_class'=>$evaluated?($hit3?'HIT':'MISS'):'PENDING'
            );
        }
        $n=max(1,(int)$stats['evaluated']);
        $summary=$stats;
        $summary['banker_win_rate']=round($stats['banker_win']/$n*100,2);
        $summary['banker_top2_rate']=round($stats['banker_top2']/$n*100,2);
        $summary['banker_top3_rate']=round($stats['banker_top3']/$n*100,2);
        $summary['banker_top4_rate']=round($stats['banker_top4']/$n*100,2);
        $summary['exposure_top3_capture_pct']=$stats['exposure_units']>0?round($stats['exposure_top3_units']/$stats['exposure_units']*100,2):0.0;
        $summary['results_available']=$stats['evaluated']>0;
        return array('summary'=>$summary,'races'=>$audited);
    }

    private function releaseAudit(array $payload)
    {
        $checks = array(
            'source_date_match' => isset($payload['source_race_date'],$payload['race_date']) && $payload['source_race_date']===$payload['race_date'],
            'source_run_present' => !empty($payload['source_v50_run']),
            'race_rows_present' => !empty($payload['races']),
            'engine_version_51_8' => isset($payload['engine_version']) && $payload['engine_version']==='51.8.0',
            'pre_race_decision_immutable' => isset($payload['results_used']) && $payload['results_used']===false,
            'db_schema_read_only' => true
        );
        $passed=0; foreach($checks as $v){ if($v) $passed++; }
        return array('checks'=>$checks,'passed'=>$passed,'total'=>count($checks),'ready'=>$passed===count($checks),'status'=>$passed===count($checks)?'READY':'REVIEW');
    }

    private function splitCsv($value)
    {
        $value = trim((string)$value);
        if ($value === '') { return array(); }
        $parts = preg_split('/\s*[,，|]\s*/u', $value, -1, PREG_SPLIT_NO_EMPTY);
        return is_array($parts) ? array_values(array_unique($parts)) : array();
    }

    private function tagBonus($tag)
    {
        $map = array('KING' => 8.0, 'MAIN_BANKER' => 5.0, 'BANKER' => 3.0, 'WATCH' => 0.0, 'SKIP' => -6.0);
        return isset($map[$tag]) ? $map[$tag] : 0.0;
    }

    private function stateAdjustment($state)
    {
        $map = array('PRIMARY' => 7.0, 'STRONG' => 4.0, 'COVER' => 1.0, 'WATCH' => -1.0, 'MIXED' => -7.0, 'SKIP' => -9.0);
        return isset($map[$state]) ? $map[$state] : -2.0;
    }

    private function initialCalibration(array $race)
    {
        $ultimate = $this->num(isset($race['ultimate_confidence']) ? $race['ultimate_confidence'] : 0);
        $consensus = $this->num(isset($race['consensus_pct']) ? $race['consensus_pct'] : 0);
        $stability = $this->num(isset($race['stability_index']) ? $race['stability_index'] : 0);
        $stars = max(1, min(5, (int)(isset($race['star_rating']) ? $race['star_rating'] : 1)));
        $state = strtoupper((string)(isset($race['race_state']) ? $race['race_state'] : 'WATCH'));
        $tag = strtoupper((string)(isset($race['final_tag']) ? $race['final_tag'] : 'WATCH'));
        $avoidCount = count($this->splitCsv(isset($race['avoid_horses']) ? $race['avoid_horses'] : ''));
        $valueCount = count($this->splitCsv(isset($race['value_horses']) ? $race['value_horses'] : ''));
        $qCount = count($this->splitCsv(isset($race['q_top2']) ? $race['q_top2'] : ''));
        $top4Count = count($this->splitCsv(isset($race['trio_top4']) ? $race['trio_top4'] : ''));

        $riskPenalty = min(18.0, $avoidCount * 1.35);
        if ($stars <= 2) { $riskPenalty += 5.0; }
        if ($state === 'MIXED' || $state === 'SKIP') { $riskPenalty += 4.0; }
        if ($consensus < 35.0) { $riskPenalty += 4.0; }
        if ($stability < 35.0) { $riskPenalty += 3.0; }

        $structureBonus = min(6.0, ($valueCount * 0.8) + ($qCount >= 2 ? 1.5 : 0.0) + ($top4Count >= 4 ? 1.5 : 0.0));
        $starBonus = ($stars - 3) * 2.5;

        /* V51.2 calibration: disagreement is measured before any ranking. */
        $ultimateConsensusGap = abs($ultimate - $consensus);
        $ultimateStabilityGap = abs($ultimate - $stability);
        $consensusStabilityGap = abs($consensus - $stability);
        $disagreementIndex = $this->clamp(
            ($ultimateConsensusGap * 0.45) +
            ($ultimateStabilityGap * 0.35) +
            ($consensusStabilityGap * 0.20)
        );
        $disagreementPenalty = min(16.0, $disagreementIndex * 0.24);

        $rawSignal =
            ($ultimate * 0.42) +
            ($consensus * 0.25) +
            ($stability * 0.20) +
            (($stars / 5.0) * 100.0 * 0.13);

        $preCalibration = $this->clamp(
            $rawSignal + $this->tagBonus($tag) + $this->stateAdjustment($state) +
            $structureBonus + $starBonus - $riskPenalty - $disagreementPenalty
        );

        $reliability = $this->clamp(
            78.0 + ($stars * 3.0) + ($structureBonus * 1.5) -
            ($riskPenalty * 1.35) - ($disagreementIndex * 0.55),
            10.0,
            98.0
        );

        return array(
            'race_num' => (int)(isset($race['race_num']) ? $race['race_num'] : 0),
            'banker_horse' => (string)(isset($race['banker_horse']) ? $race['banker_horse'] : ''),
            'banker_name' => (string)(isset($race['banker_name']) ? $race['banker_name'] : ''),
            'v50_ultimate_confidence' => round($ultimate, 4),
            'consensus_pct' => round($consensus, 4),
            'stability_index' => round($stability, 4),
            'star_rating' => $stars,
            'race_state' => $state,
            'final_tag' => $tag,
            'q_top2' => (string)(isset($race['q_top2']) ? $race['q_top2'] : ''),
            'trio_top4' => (string)(isset($race['trio_top4']) ? $race['trio_top4'] : ''),
            'value_horses' => (string)(isset($race['value_horses']) ? $race['value_horses'] : ''),
            'avoid_horses' => (string)(isset($race['avoid_horses']) ? $race['avoid_horses'] : ''),
            'risk_penalty' => round($riskPenalty, 4),
            'structure_bonus' => round($structureBonus, 4),
            'disagreement_index' => round($disagreementIndex, 4),
            'disagreement_penalty' => round($disagreementPenalty, 4),
            'calibration_reliability' => round($reliability, 4),
            'raw_signal_score' => round($rawSignal, 4),
            'pre_calibration_score' => round($preCalibration, 4),
            'base_calibrated_score' => round($preCalibration, 4)
        );
    }

    private function mean(array $values)
    {
        return count($values) ? array_sum($values) / count($values) : 0.0;
    }

    private function stddev(array $values, $mean)
    {
        if (count($values) < 2) { return 0.0; }
        $sum = 0.0;
        foreach ($values as $value) { $sum += pow($value - $mean, 2); }
        return sqrt($sum / count($values));
    }

    private function supremeDecision(array $row, $rank)
    {
        $score = (float)$row['final_supreme_score'];
        $reliability = (float)$row['calibration_reliability'];
        $conflict = (float)$row['consensus_conflict_penalty'];
        $risk = (float)$row['risk_penalty'];
        $stars = (int)$row['star_rating'];
        $state = (string)$row['race_state'];

        $tier = 'D';
        if ($score >= 68.0 && $reliability >= 72.0 && $conflict <= 4.5 && $risk <= 8.0 && $stars >= 4 && $rank <= 2 && !in_array($state, array('MIXED','SKIP'), true)) {
            $tier = 'S';
        } elseif ($score >= 58.0 && $reliability >= 62.0 && $conflict <= 7.5 && $risk <= 12.0 && $stars >= 3 && $rank <= 4) {
            $tier = 'A';
        } elseif ($score >= 48.0 && $reliability >= 52.0 && $conflict <= 10.5 && $risk <= 15.0) {
            $tier = 'B';
        } elseif ($score >= 38.0 && $reliability >= 42.0 && $conflict <= 14.0) {
            $tier = 'C';
        }

        $actionMap = array('S'=>'PRIMARY','A'=>'SECONDARY','B'=>'WATCH','C'=>'WATCH','D'=>'SKIP');
        $legacyMap = array('S'=>'SUPREME_BANKER','A'=>'PRIME_BANKER','B'=>'COVER','C'=>'WATCH','D'=>'SKIP');
        $reasons = array();
        $reasons[] = 'Score ' . number_format($score, 1, '.', '');
        $reasons[] = 'Reliability ' . number_format($reliability, 1, '.', '');
        $reasons[] = 'Conflict ' . number_format($conflict, 1, '.', '');
        $reasons[] = 'Risk ' . number_format($risk, 1, '.', '');
        $reasons[] = 'Rank #' . (int)$rank;

        return array(
            'tier' => $tier,
            'action' => $actionMap[$tier],
            'legacy_tier' => $legacyMap[$tier],
            'reason' => implode(' | ', $reasons)
        );
    }

    private function desiredExposure(array $row, $portfolioState)
    {
        $tier = (string)$row['supreme_tier'];
        $action = (string)$row['supreme_action'];
        $score = (float)$row['final_supreme_score'];
        $reliability = (float)$row['calibration_reliability'];
        $conflict = (float)$row['consensus_conflict_penalty'];
        $risk = (float)$row['risk_penalty'];

        $baseMap = array('PRIMARY'=>2.40, 'SECONDARY'=>1.45, 'WATCH'=>0.45, 'SKIP'=>0.0);
        $base = isset($baseMap[$action]) ? $baseMap[$action] : 0.0;
        if ($tier === 'S') { $base += 0.35; }
        elseif ($tier === 'A') { $base += 0.15; }
        elseif ($tier === 'D') { $base = 0.0; }

        $qualityFactor = $this->clamp(($score * 0.55) + ($reliability * 0.45), 0.0, 100.0) / 100.0;
        $riskFactor = $this->clamp(1.0 - (($risk * 0.028) + ($conflict * 0.022)), 0.20, 1.0);
        $dayFactor = 0.70;
        if ($portfolioState === 'SELECTIVE_DAY') { $dayFactor = 0.85; }
        elseif ($portfolioState === 'STRONG_DAY') { $dayFactor = 1.00; }
        elseif ($portfolioState === 'SUPREME_DAY') { $dayFactor = 1.10; }

        $desired = $base * $qualityFactor * $riskFactor * $dayFactor;
        if ($action === 'WATCH') { $desired = min($desired, 0.60); }
        if ($action === 'SKIP') { $desired = 0.0; }
        return round($this->clamp($desired, 0.0, 2.75), 4);
    }

    private function portfolioCap($portfolioState)
    {
        $caps = array('WATCH_DAY'=>3.0, 'SELECTIVE_DAY'=>5.0, 'STRONG_DAY'=>7.0, 'SUPREME_DAY'=>8.5);
        return isset($caps[$portfolioState]) ? (float)$caps[$portfolioState] : 3.0;
    }

    private function allocatePortfolio(array $rows, $portfolioState)
    {
        $dailyCap = $this->portfolioCap($portfolioState);
        $desiredTotal = 0.0;
        foreach ($rows as $i => $row) {
            $desired = $this->desiredExposure($row, $portfolioState);
            $rows[$i]['portfolio_desired_units'] = $desired;
            $desiredTotal += $desired;
        }

        $scale = ($desiredTotal > $dailyCap && $desiredTotal > 0.0) ? ($dailyCap / $desiredTotal) : 1.0;
        $allocatedTotal = 0.0;
        foreach ($rows as $i => $row) {
            $allocated = round(min(2.75, $row['portfolio_desired_units'] * $scale), 2);
            if ((string)$row['supreme_action'] === 'SKIP') { $allocated = 0.0; }
            $rows[$i]['portfolio_exposure_units'] = $allocated;
            $rows[$i]['portfolio_scale_factor'] = round($scale, 4);
            $rows[$i]['portfolio_daily_cap_units'] = round($dailyCap, 2);
            $allocatedTotal += $allocated;
        }

        usort($rows, function ($a, $b) {
            if ($a['portfolio_exposure_units'] == $b['portfolio_exposure_units']) { return $a['supreme_rank'] <=> $b['supreme_rank']; }
            return ($a['portfolio_exposure_units'] < $b['portfolio_exposure_units']) ? 1 : -1;
        });
        $portfolioRank = 0;
        foreach ($rows as $i => $row) {
            if ((float)$row['portfolio_exposure_units'] > 0.0) { $portfolioRank++; $rows[$i]['portfolio_rank'] = $portfolioRank; }
            else { $rows[$i]['portfolio_rank'] = 0; }
            $pct = $allocatedTotal > 0.0 ? ((float)$row['portfolio_exposure_units'] / $allocatedTotal) * 100.0 : 0.0;
            $rows[$i]['portfolio_allocation_pct'] = round($pct, 2);
            $rows[$i]['portfolio_allocation_reason'] = 'Action ' . $row['supreme_action'] . ' | Tier ' . $row['supreme_tier'] . ' | Score ' . number_format((float)$row['final_supreme_score'], 1, '.', '') . ' | Reliability ' . number_format((float)$row['calibration_reliability'], 1, '.', '') . ' | Risk ' . number_format((float)$row['risk_penalty'], 1, '.', '');
        }

        usort($rows, function ($a, $b) { return $a['supreme_rank'] <=> $b['supreme_rank']; });
        return array($rows, round($desiredTotal, 2), round($allocatedTotal, 2), round($dailyCap, 2), round($scale, 4));
    }


    private function riskLevel($score)
    {
        $score = (float)$score;
        if ($score >= 75.0) { return 'CRITICAL'; }
        if ($score >= 50.0) { return 'HIGH'; }
        if ($score >= 25.0) { return 'MEDIUM'; }
        return 'LOW';
    }

    private function applyDynamicRiskControl(array $rows, $dailyCap, $portfolioState)
    {
        $beforeTotal = 0.0;
        $actionCounts = array('PRIMARY'=>0, 'SECONDARY'=>0, 'WATCH'=>0, 'SKIP'=>0);
        foreach ($rows as $row) {
            $beforeTotal += (float)$row['portfolio_exposure_units'];
            $action = isset($row['supreme_action']) ? (string)$row['supreme_action'] : 'SKIP';
            if (!isset($actionCounts[$action])) { $actionCounts[$action] = 0; }
            $actionCounts[$action]++;
        }

        $riskAdjustedTotal = 0.0;
        foreach ($rows as $i => $row) {
            $units = (float)$row['portfolio_exposure_units'];
            $original = $units;
            $reliability = (float)$row['calibration_reliability'];
            $conflict = (float)$row['consensus_conflict_penalty'];
            $risk = (float)$row['risk_penalty'];
            $action = (string)$row['supreme_action'];
            $reasons = array();
            $factor = 1.0;

            if ($reliability < 50.0) { $factor *= 0.55; $reasons[] = 'low reliability'; }
            elseif ($reliability < 60.0) { $factor *= 0.75; $reasons[] = 'soft reliability'; }
            if ($conflict > 12.0) { $factor *= 0.55; $reasons[] = 'high conflict'; }
            elseif ($conflict > 8.0) { $factor *= 0.78; $reasons[] = 'conflict caution'; }
            if ($risk > 15.0) { $factor *= 0.60; $reasons[] = 'high race risk'; }
            elseif ($risk > 11.0) { $factor *= 0.82; $reasons[] = 'race risk caution'; }
            if ($action === 'WATCH' && isset($actionCounts['WATCH']) && $actionCounts['WATCH'] >= 5) {
                $factor *= 0.85; $reasons[] = 'watch overload';
            }
            if ($action === 'SKIP') { $factor = 0.0; $reasons[] = 'skip action'; }

            $units = round(max(0.0, min(2.75, $units * $factor)), 2);
            $rows[$i]['risk_original_exposure_units'] = round($original, 2);
            $rows[$i]['risk_adjustment_factor'] = round($factor, 4);
            $rows[$i]['risk_adjusted_exposure_units'] = $units;
            $rows[$i]['portfolio_exposure_units'] = $units;
            $rows[$i]['risk_control_reason'] = $reasons ? implode(', ', $reasons) : 'no reduction';
            $riskAdjustedTotal += $units;
        }

        /* Concentration guard: no single race above 45% of active exposure. */
        if ($riskAdjustedTotal > 0.0) {
            $singleLimit = max(0.35, min(2.75, $riskAdjustedTotal * 0.45));
            foreach ($rows as $i => $row) {
                $units = (float)$row['portfolio_exposure_units'];
                if ($units > $singleLimit) {
                    $rows[$i]['portfolio_exposure_units'] = round($singleLimit, 2);
                    $rows[$i]['risk_adjusted_exposure_units'] = round($singleLimit, 2);
                    $rows[$i]['risk_control_reason'] .= ', top-1 concentration guard';
                }
            }
        }

        $afterTotal = 0.0;
        foreach ($rows as $row) { $afterTotal += (float)$row['portfolio_exposure_units']; }

        /* Final daily cap guard after all adjustments. */
        $finalScale = ($afterTotal > $dailyCap && $afterTotal > 0.0) ? ($dailyCap / $afterTotal) : 1.0;
        if ($finalScale < 1.0) {
            foreach ($rows as $i => $row) {
                $rows[$i]['portfolio_exposure_units'] = round((float)$row['portfolio_exposure_units'] * $finalScale, 2);
                $rows[$i]['risk_adjusted_exposure_units'] = $rows[$i]['portfolio_exposure_units'];
                $rows[$i]['risk_control_reason'] .= ', daily cap rebalance';
            }
        }

        $finalTotal = 0.0;
        foreach ($rows as $row) { $finalTotal += (float)$row['portfolio_exposure_units']; }
        usort($rows, function ($a, $b) {
            if ($a['portfolio_exposure_units'] == $b['portfolio_exposure_units']) { return $a['supreme_rank'] <=> $b['supreme_rank']; }
            return ($a['portfolio_exposure_units'] < $b['portfolio_exposure_units']) ? 1 : -1;
        });

        $top1 = isset($rows[0]) ? (float)$rows[0]['portfolio_exposure_units'] : 0.0;
        $top3 = 0.0;
        for ($i=0; $i<min(3, count($rows)); $i++) { $top3 += (float)$rows[$i]['portfolio_exposure_units']; }
        $top1Pct = $finalTotal > 0.0 ? ($top1 / $finalTotal) * 100.0 : 0.0;
        $top3Pct = $finalTotal > 0.0 ? ($top3 / $finalTotal) * 100.0 : 0.0;

        $avgConflict = $this->mean(array_column($rows, 'consensus_conflict_penalty'));
        $avgReliability = $this->mean(array_column($rows, 'calibration_reliability'));
        $riskScore = $this->clamp(
            ($top1Pct > 45.0 ? ($top1Pct - 45.0) * 1.4 : 0.0) +
            ($top3Pct > 85.0 ? ($top3Pct - 85.0) * 1.0 : 0.0) +
            max(0.0, 60.0 - $avgReliability) * 0.8 +
            $avgConflict * 1.4 +
            ((isset($actionCounts['WATCH']) && $actionCounts['WATCH'] >= 6) ? 8.0 : 0.0)
        );
        $level = $this->riskLevel($riskScore);

        $portfolioRank = 0;
        foreach ($rows as $i => $row) {
            if ((float)$row['portfolio_exposure_units'] > 0.0) { $portfolioRank++; $rows[$i]['portfolio_rank'] = $portfolioRank; }
            else { $rows[$i]['portfolio_rank'] = 0; }
            $rows[$i]['portfolio_allocation_pct'] = $finalTotal > 0.0
                ? round(((float)$row['portfolio_exposure_units'] / $finalTotal) * 100.0, 2)
                : 0.0;
            $rows[$i]['dynamic_risk_level'] = $level;
            $rows[$i]['dynamic_risk_score'] = round($riskScore, 2);
        }
        usort($rows, function ($a, $b) { return $a['supreme_rank'] <=> $b['supreme_rank']; });

        return array($rows, array(
            'risk_level' => $level,
            'risk_score' => round($riskScore, 2),
            'exposure_before_risk_units' => round($beforeTotal, 2),
            'exposure_after_risk_units' => round($finalTotal, 2),
            'risk_reduction_units' => round(max(0.0, $beforeTotal - $finalTotal), 2),
            'top1_exposure_pct' => round($top1Pct, 2),
            'top3_exposure_pct' => round($top3Pct, 2),
            'average_conflict' => round($avgConflict, 2),
            'average_reliability' => round($avgReliability, 2),
            'primary_count' => isset($actionCounts['PRIMARY']) ? $actionCounts['PRIMARY'] : 0,
            'secondary_count' => isset($actionCounts['SECONDARY']) ? $actionCounts['SECONDARY'] : 0,
            'watch_count' => isset($actionCounts['WATCH']) ? $actionCounts['WATCH'] : 0,
            'skip_count' => isset($actionCounts['SKIP']) ? $actionCounts['SKIP'] : 0,
            'final_scale_factor' => round($finalScale, 4),
            'portfolio_state' => $portfolioState
        ));
    }

    private function finalize(array $calibrated)
    {
        if (!$calibrated) { return array(array(), array()); }

        $preScores = array();
        foreach ($calibrated as $row) { $preScores[] = (float)$row['pre_calibration_score']; }
        $dayMean = $this->mean($preScores);
        $daySd = $this->stddev($preScores, $dayMean);

        /* Reliability-weighted shrinkage prevents weak/contradictory signals from overconfidence. */
        foreach ($calibrated as $i => $row) {
            $reliabilityWeight = $this->clamp((float)$row['calibration_reliability'], 10.0, 98.0) / 100.0;
            $shrunk = ($row['pre_calibration_score'] * $reliabilityWeight) + ($dayMean * (1.0 - $reliabilityWeight));
            $z = $daySd > 0.0001 ? (($shrunk - $dayMean) / $daySd) : 0.0;
            $relative = $this->clamp(50.0 + ($z * 12.0));
            $score = $this->clamp(($shrunk * 0.82) + ($relative * 0.18));
            $uncertainty = $this->clamp(((100.0 - $row['calibration_reliability']) * 0.12) + ($row['disagreement_index'] * 0.08), 1.5, 18.0);

            $calibrated[$i]['daily_calibration_mean'] = round($dayMean, 4);
            $calibrated[$i]['daily_calibration_sd'] = round($daySd, 4);
            $calibrated[$i]['calibrated_score'] = round($shrunk, 4);
            $calibrated[$i]['relative_strength'] = round($relative, 4);
            $calibrated[$i]['supreme_confidence'] = round($score, 4);
            $calibrated[$i]['confidence_gap'] = round($score - $row['v50_ultimate_confidence'], 4);
            $calibrated[$i]['confidence_uncertainty'] = round($uncertainty, 4);
            $calibrated[$i]['confidence_low'] = round($this->clamp($score - $uncertainty), 4);
            $calibrated[$i]['confidence_high'] = round($this->clamp($score + $uncertainty), 4);
            $calibrated[$i]['calibration_status'] = $row['calibration_reliability'] >= 70.0
                ? 'RELIABLE'
                : ($row['calibration_reliability'] >= 50.0 ? 'CAUTION' : 'UNSTABLE');
        }

        /* First establish the V51.2 calibration-only order. */
        usort($calibrated, function ($a, $b) {
            if ($a['supreme_confidence'] == $b['supreme_confidence']) { return $a['race_num'] <=> $b['race_num']; }
            return ($a['supreme_confidence'] < $b['supreme_confidence']) ? 1 : -1;
        });
        foreach ($calibrated as $i => $row) {
            $calibrated[$i]['calibration_rank'] = $i + 1;
        }

        /* V51.3 consensus governance: agreement rewards aligned evidence; conflicts reduce rank. */
        foreach ($calibrated as $i => $row) {
            $ultimate = (float)$row['v50_ultimate_confidence'];
            $consensus = (float)$row['consensus_pct'];
            $stability = (float)$row['stability_index'];
            $stars = (int)$row['star_rating'];
            $state = (string)$row['race_state'];
            $tag = (string)$row['final_tag'];

            $pairGapMean = (abs($ultimate - $consensus) + abs($ultimate - $stability) + abs($consensus - $stability)) / 3.0;
            $agreement = $this->clamp(100.0 - ($pairGapMean * 1.55));
            $signalStrength = $this->clamp(($consensus * 0.48) + ($stability * 0.37) + (($stars / 5.0) * 100.0 * 0.15));

            $stateConflict = in_array($state, array('MIXED','SKIP'), true) ? 5.0 : 0.0;
            $tagConflict = in_array($tag, array('WATCH','SKIP'), true) ? 3.0 : 0.0;
            $lowConsensusPenalty = $consensus < 35.0 ? (35.0 - $consensus) * 0.18 : 0.0;
            $conflictPenalty = min(18.0, ((100.0 - $agreement) * 0.10) + $stateConflict + $tagConflict + $lowConsensusPenalty);

            $supportBonus = 0.0;
            if ($agreement >= 78.0) { $supportBonus += 3.0; }
            if ($consensus >= 55.0 && $stability >= 50.0) { $supportBonus += 2.5; }
            if ($stars >= 4 && !in_array($state, array('MIXED','SKIP'), true)) { $supportBonus += 1.5; }

            $reRankScore = $this->clamp(
                ((float)$row['supreme_confidence'] * 0.70) +
                ($agreement * 0.18) +
                ($signalStrength * 0.12) +
                $supportBonus - $conflictPenalty
            );

            $calibrated[$i]['consensus_agreement_score'] = round($agreement, 4);
            $calibrated[$i]['consensus_signal_strength'] = round($signalStrength, 4);
            $calibrated[$i]['consensus_support_bonus'] = round($supportBonus, 4);
            $calibrated[$i]['consensus_conflict_penalty'] = round($conflictPenalty, 4);
            $calibrated[$i]['consensus_re_rank_score'] = round($reRankScore, 4);
            $calibrated[$i]['final_supreme_score'] = round($reRankScore, 4);
        }

        usort($calibrated, function ($a, $b) {
            if ($a['final_supreme_score'] == $b['final_supreme_score']) {
                if ($a['supreme_confidence'] == $b['supreme_confidence']) { return $a['race_num'] <=> $b['race_num']; }
                return ($a['supreme_confidence'] < $b['supreme_confidence']) ? 1 : -1;
            }
            return ($a['final_supreme_score'] < $b['final_supreme_score']) ? 1 : -1;
        });

        $topScores = array_slice(array_column($calibrated, 'final_supreme_score'), 0, min(5, count($calibrated)));
        $portfolioConfidence = $this->mean($topScores);
        $portfolioState = 'WATCH_DAY';
        if ($portfolioConfidence >= 72.0) { $portfolioState = 'SUPREME_DAY'; }
        elseif ($portfolioConfidence >= 63.0) { $portfolioState = 'STRONG_DAY'; }
        elseif ($portfolioConfidence >= 54.0) { $portfolioState = 'SELECTIVE_DAY'; }

        $count = count($calibrated);
        foreach ($calibrated as $i => $row) {
            $rank = $i + 1;
            $percentile = $count > 1 ? (1.0 - ($i / ($count - 1))) * 100.0 : 100.0;
            $rankDelta = (int)$row['calibration_rank'] - $rank;
            $rankDirection = $rankDelta > 0 ? 'UP' : ($rankDelta < 0 ? 'DOWN' : 'SAME');
            $decision = $this->supremeDecision($row, $rank);
            $tier = $decision['tier'];
            $calibrated[$i]['supreme_rank'] = $rank;
            $calibrated[$i]['supreme_percentile'] = round($percentile, 2);
            $calibrated[$i]['consensus_rank_delta'] = $rankDelta;
            $calibrated[$i]['consensus_rank_direction'] = $rankDirection;
            $calibrated[$i]['supreme_tier'] = $tier;
            $calibrated[$i]['supreme_legacy_tier'] = $decision['legacy_tier'];
            $calibrated[$i]['supreme_action'] = $decision['action'];
            $calibrated[$i]['decision_reason'] = $decision['reason'];
            $calibrated[$i]['reason'] = 'V51.8 backtest and audit release over V51.7 dashboard; immutable V50 source unchanged.';
        }

        list($calibrated, $desiredExposure, $allocatedExposure, $dailyCap, $portfolioScale) = $this->allocatePortfolio($calibrated, $portfolioState);
        list($calibrated, $riskSummary) = $this->applyDynamicRiskControl($calibrated, $dailyCap, $portfolioState);
        $finalExposure = isset($riskSummary['exposure_after_risk_units']) ? (float)$riskSummary['exposure_after_risk_units'] : $allocatedExposure;
        $day = $this->buildSummary($calibrated, $portfolioConfidence, $portfolioState);
        $day['portfolio_desired_exposure_units'] = $desiredExposure;
        $day['portfolio_pre_risk_exposure_units'] = $allocatedExposure;
        $day['portfolio_total_exposure_units'] = $finalExposure;
        $day['dynamic_risk'] = $riskSummary;
        $day['dynamic_risk_level'] = isset($riskSummary['risk_level']) ? $riskSummary['risk_level'] : 'LOW';
        $day['dynamic_risk_score'] = isset($riskSummary['risk_score']) ? $riskSummary['risk_score'] : 0.0;
        $day['risk_reduction_units'] = isset($riskSummary['risk_reduction_units']) ? $riskSummary['risk_reduction_units'] : 0.0;
        $day['top1_exposure_pct'] = isset($riskSummary['top1_exposure_pct']) ? $riskSummary['top1_exposure_pct'] : 0.0;
        $day['top3_exposure_pct'] = isset($riskSummary['top3_exposure_pct']) ? $riskSummary['top3_exposure_pct'] : 0.0;
        $day['portfolio_daily_cap_units'] = $dailyCap;
        $day['portfolio_scale_factor'] = $portfolioScale;
        $day['portfolio_cap_used_pct'] = $dailyCap > 0.0 ? round(($finalExposure / $dailyCap) * 100.0, 2) : 0.0;
        $day['calibration_mean'] = round($dayMean, 4);
        $day['calibration_sd'] = round($daySd, 4);
        $day['average_reliability'] = round($this->mean(array_column($calibrated, 'calibration_reliability')), 4);
        $day['average_disagreement'] = round($this->mean(array_column($calibrated, 'disagreement_index')), 4);
        $day['average_consensus_agreement'] = round($this->mean(array_column($calibrated, 'consensus_agreement_score')), 4);
        $day['average_consensus_conflict_penalty'] = round($this->mean(array_column($calibrated, 'consensus_conflict_penalty')), 4);
        return array($calibrated, $day);
    }

    private function buildSummary(array $races, $portfolioConfidence, $portfolioState)
    {
        if (!$races) { return array(); }
        $top = $races[0];
        $top3 = array(); $strong = array(); $skip = array(); $exposure = 0.0;
        foreach ($races as $race) {
            if (count($top3) < 3) { $top3[] = 'R' . $race['race_num'] . '#' . $race['banker_horse']; }
            if (in_array($race['supreme_tier'], array('S','A'), true)) { $strong[] = 'R' . $race['race_num'] . '#' . $race['banker_horse']; }
            if ($race['supreme_tier'] === 'D') { $skip[] = 'R' . $race['race_num']; }
            $exposure += (float)$race['portfolio_exposure_units'];
        }
        return array(
            'best_banker_race' => $top['race_num'],
            'best_banker_horse' => $top['banker_horse'],
            'best_banker_name' => $top['banker_name'],
            'best_banker_score' => isset($top['final_supreme_score']) ? $top['final_supreme_score'] : $top['supreme_confidence'],
            'best_banker_tier' => $top['supreme_tier'],
            'top3_bankers' => implode(',', $top3),
            'strong_bankers' => implode(',', $strong),
            'skip_races' => implode(',', $skip),
            'portfolio_confidence' => round($portfolioConfidence, 4),
            'portfolio_state' => $portfolioState,
            'portfolio_total_exposure_units' => round($exposure, 2),
            'race_count' => count($races)
        );
    }

    private function snapshotPath($date, $venue)
    {
        $safeVenue = preg_replace('/[^A-Za-z0-9_-]+/', '_', (string)$venue);
        if ($safeVenue === '') { $safeVenue = 'ALL'; }
        return $this->snapshotDir . '/' . preg_replace('/[^0-9-]/', '', (string)$date) . '_' . $safeVenue . '.json';
    }

    private function writeSnapshot(array $payload, $date, $venue)
    {
        if (!is_dir($this->snapshotDir) && !@mkdir($this->snapshotDir, 0775, true) && !is_dir($this->snapshotDir)) { return false; }
        $json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
        if ($json === false) { return false; }
        return @file_put_contents($this->snapshotPath($date, $venue), $json, LOCK_EX) !== false;
    }

    public function latest($date, $venue = '')
    {
        if (!$this->tableExists('wr_chairman_ultimate_race_v50')) {
            return array('status'=>'SOURCE_MISSING','message'=>'V50 race table not found. Build/install V50 first.','pre_race_only'=>true,'results_used'=>false,'source_v50_run'=>'','source_race_date'=>$date,'source_venue'=>$venue,'day'=>array(),'races'=>array());
        }
        $runKey = $this->latestV50Run($date, $venue);
        if ($runKey === '') {
            return array('status'=>'EMPTY','message'=>'No V50 run found for selected date/venue.','pre_race_only'=>true,'results_used'=>false,'source_v50_run'=>'','source_race_date'=>$date,'source_venue'=>$venue,'day'=>array(),'races'=>array());
        }
        $where = array('race_date=' . $this->q($date), 'run_key=' . $this->q($runKey));
        if ($venue !== '') { $where[] = 'venue=' . $this->q($venue); }
        $sourceRows = $this->rows('SELECT * FROM wr_chairman_ultimate_race_v50 WHERE ' . implode(' AND ', $where) . ' ORDER BY race_num');
        $initial = array();
        foreach ($sourceRows as $sourceRow) { $initial[] = $this->initialCalibration($sourceRow); }
        list($races, $day) = $this->finalize($initial);
        $payload = array(
            'status' => $races ? 'OK' : 'EMPTY',
            'engine' => 'V51',
            'engine_version' => '51.8.0',
            'pre_race_only' => true,
            'results_used' => false,
            'source_v50_run' => $runKey,
            'source_race_date' => $date,
            'source_venue' => $venue,
            'race_date' => $date,
            'venue' => $venue,
            'day' => $day,
            'races' => $races
        );
        $payload['backtest'] = $this->buildBacktest($races, $date, $venue);
        $payload['audit_results_used'] = !empty($payload['backtest']['summary']['results_available']);
        $payload['release_audit'] = $this->releaseAudit($payload);
        return $payload;
    }

    public function build($date, $venue = '')
    {
        $payload = $this->latest($date, $venue);
        $payload['snapshot_written'] = false;
        if ($payload['status'] === 'OK') { $payload['snapshot_written'] = $this->writeSnapshot($payload, $date, $venue); }
        return $payload;
    }
}
