<?php

declare(strict_types=1);

// Source-only reproducible grouping. Prints CSV to stdout; never connects to DB/HTTP.
$workspace = dirname(__DIR__, 5);
$matrixPath = dirname(__DIR__) . '/derived/interface-contract-matrix.csv';
$store = $workspace . '/zhctproject/store';
$front = $store . '/public/static/src';
$batch = [
    'P0_PAUSED_NEXT5' => [
        'staff/tree', 'staff/sport', 'dateMenu/getStaffByDepartment',
        'dateMenu/getStaffByUuid', 'dateMenu/getMenuList',
    ],
    'P1_READ_SELECTOR' => [
        'dateMenu/getButton', 'stageanalyze/eligibility_uuid',
        'stageanalyze/stage_eligibility_uuid', 'stageanalyze/team_list',
        'stageanalyze/eatSingleStatus', 'stageanalyze/eatMultiStatus',
        'ontoanalysis/getMealsStatus', 'weight/lists', 'weight/show', 'weight/chart',
        'rpe/info', 'user/lists', 'user/users', 'bill/isUpdate',
        'registerManagement/info', 'zhaikerHealth/baseOptions',
        'zhaikerHealth/groupAnalysis', 'ontoanalysis/MonitorList',
    ],
    'P2_CONDITIONAL_WRITE' => [
        'dateMenu/getMenuInfo', 'dateMenu/getWhere', 'dateMenu/getTarget',
        'dateMenu/getMenu', 'staff/info',
    ],
    'P3_TEAM_ANALYTICS_MIXED' => [
        'stageanalyze/get_day_teampower', 'stageanalyze/get_day_teamnutrient',
        'stageanalyze/get_day_team_microelement',
        'stageanalyze/get_day_team_microelement_popout',
        'stageanalyze/get_stage_teampower', 'stageanalyze/get_stage_team_nutrient',
        'stageanalyze/get_stage_team_nutrient_popout',
        'stageanalyze/get_stage_team_microelement',
        'stageanalyze/get_stage_team_microelement_popout',
        'stageanalyze/TeamDietaryPattern', 'stageanalyze/TeamProtein',
        'stageanalyze/TeamThreeMeals', 'stageanalyze/TeamDf',
        'stageanalyze/StageTeamThreeMeals', 'stageanalyze/StageTeamProtein',
        'stageanalyze/StageDietaryPattern', 'stageanalyze/StageIntakeFrequencies',
    ],
    'P4_PERSONAL_ANALYTICS_MIXED' => [
        'ontoanalysis/Oneweiliang', 'ontoanalysis/Moreweiliang',
        'ontoanalysis/Morenutrient', 'ontoanalysis/Morepower',
        'ontoanalysis/FoodRecord', 'ontoanalysis/DietaryPattern',
        'ontoanalysis/DietaryFiber', 'ontoanalysis/Dietary',
        'ontoanalysis/Protein', 'ontoanalysis/Onepower',
        'ontoanalysis/Onenutrient', 'ontoanalysis/Stage_protein',
        'ontoanalysis/Stage_meal', 'ontoanalysis/Stage_fiber',
        'ontoanalysis/Stage_dietary',
    ],
    'P5_EXPLICIT_WRITE' => [
        'rpe/addSingle', 'role/status', 'role/delete', 'staff/delete',
        'registerManagement/add', 'registerManagement/update', 'weight/add',
        'user/update', 'user/delete', 'food/delete',
        'physicalMature/add', 'physicalMature/update',
    ],
    'P6_PRINT_FILE' => [
        'prints/print_content', 'prints/print_list',
        'restaurant/foodNutritionExport', 'download/zip',
    ],
    'P7_UPLOAD_OSS' => ['cater/caterUpload'],
    'P8_AGENT_EXTERNAL' => ['agent/ask', 'agent/suggestionClick'],
    'P9_RESEARCH_EXPORT' => ['zhaikerHealth/groupAnalysisExport'],
];
$selected = [];
foreach ($batch as $name => $paths) {
    foreach ($paths as $path) {
        $key = strtolower('/p/' . $path);
        if (isset($selected[$key])) throw new RuntimeException('Duplicate selected path');
        $selected[$key] = $name;
    }
}
if (count($selected) !== 80) throw new RuntimeException('Selection is not 80 keys');
$matrix = fopen($matrixPath, 'rb');
if (!$matrix) throw new RuntimeException('Matrix missing');
$columns = array_flip(fgetcsv($matrix));
$matches = [];
while (($row = fgetcsv($matrix)) !== false) {
    if (($row[$columns['repo']] ?? '') !== 'store'
        || ($row[$columns['success_case']] ?? '') !== 'not-tested') continue;
    $key = strtolower((string)($row[$columns['path']] ?? ''));
    if (!isset($selected[$key])) continue;
    $method = strtoupper((string)$row[$columns['method']]);
    // This endpoint has both GET and POST matrix keys. The routed PC export
    // uses axios.get; selecting POST would silently classify the wrong key.
    if (in_array($key, ['/p/zhaikerhealth/baseoptions',
        '/p/zhaikerhealth/groupanalysisexport'], true) && $method !== 'GET') continue;
    if (isset($matches[$key]) && $matches[$key]['method'] !== $method) {
        throw new RuntimeException('Ambiguous HTTP verb for selected path: ' . $key);
    }
    $matches[$key] = ['method' => $method, 'path' => $row[$columns['path']],
        'gate' => $row[$columns['coverage_gate']]];
}
fclose($matrix);
if (count($matches) !== 80) {
    $missing = array_diff(array_keys($selected), array_keys($matches));
    throw new RuntimeException('Selected key missing/not untested: ' . implode(';', $missing));
}
$frontHits = [];
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(
    $front, FilesystemIterator::SKIP_DOTS));
foreach ($iterator as $file) {
    if (!preg_match('/\.(vue|js|ts)$/', $file->getFilename())) continue;
    $content = file_get_contents($file->getPathname());
    preg_match_all('~/?p/[A-Za-z0-9_/]+~i', $content, $found);
    $relative = substr($file->getPathname(), strlen($front) + 1);
    foreach (array_unique($found[0]) as $route) {
        $key = '/' . ltrim(strtolower($route), '/');
        if (isset($selected[$key])) $frontHits[$key][$relative] = true;
    }
}
$routerSources = '';
$routerDir = $front . '/router';
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(
    $routerDir, FilesystemIterator::SKIP_DOTS));
foreach ($iterator as $file) {
    if ($file->getExtension() === 'js') $routerSources .= file_get_contents($file->getPathname());
}
$routeText = file_get_contents($store . '/application/route.php');
$routeGroups = [];
preg_match_all("~Route::group\\(\\[\\s*['\"]name['\"]\\s*=>\\s*['\"]([^'\"]+)['\"]~",
    $routeText, $routeMatches, PREG_OFFSET_CAPTURE);
foreach ($routeMatches[0] as $index => $match) {
    $start = $match[1];
    $end = isset($routeMatches[0][$index + 1])
        ? $routeMatches[0][$index + 1][1] : strlen($routeText);
    $name = strtolower($routeMatches[1][$index][0]);
    $routeGroups[$name][] = ['text' => substr($routeText, $start, $end - $start),
        'offset' => $start];
}
$controllers = [];
foreach (glob($store . '/application/p/controller/*.php') as $file) {
    $controllers[strtolower(basename($file, '.php'))] = $file;
}
$logicFiles = [];
foreach (glob($store . '/application/p/logic/*.php') as $file) {
    $logicFiles[strtolower(basename($file, '.php'))] = $file;
}
$methodSource = static function (?string $file, string $method): array {
    if ($file === null || !is_file($file)) return ['', '', ''];
    $text = file_get_contents($file);
    if (!preg_match('/\bpublic\s+function\s+' . preg_quote($method, '/')
        . '\s*\(/i', $text, $hit, PREG_OFFSET_CAPTURE)) return ['', '', ''];
    $start = $hit[0][1];
    $line = substr_count(substr($text, 0, $start), "\n") + 1;
    $after = substr($text, $start + strlen($hit[0][0]));
    $next = null;
    if (preg_match('/\bpublic\s+function\s+[A-Za-z_][A-Za-z0-9_]*\s*\(/',
        $after, $m, PREG_OFFSET_CAPTURE)) $next = $m[0][1];
    $body = $next === null ? $after : substr($after, 0, $next);
    return [$body, basename($file), (string)$line];
};
$fixture = [
    'P0_PAUSED_NEXT5' => 'one_type0_actor;leaf_department_6to8_staff;menu_recommend_window',
    'P1_READ_SELECTOR' => 'one_type0_actor;bounded_natural_staff_department_date',
    'P2_CONDITIONAL_WRITE' => 'isolated_actor;dedicated_staff_menu_snapshot;rollback_log_or_qrcode',
    'P3_TEAM_ANALYTICS_MIXED' => 'one_type0_actor;bounded_team_date;initial_log_rollback_if_called',
    'P4_PERSONAL_ANALYTICS_MIXED' => 'one_type0_actor;one_staff_date;initial_log_rollback_if_called',
    'P5_EXPLICIT_WRITE' => 'isolated_owned_rows;transaction_and_exact_cleanup;approval_required',
    'P6_PRINT_FILE' => 'private_file_space;printer_stub;size_cap;exact_file_cleanup',
    'P7_UPLOAD_OSS' => 'private_image;isolated_OSS_stub;exact_file_cleanup',
    'P8_AGENT_EXTERNAL' => 'fake_LLM_and_tool_stubs;isolated_log_fixture',
    'P9_RESEARCH_EXPORT' => 'private_export;bounded_research_rows;queue_stubs',
];
$output = fopen('php://stdout', 'wb');
fputcsv($output, ['method', 'path', 'batch', 'matrix_gate',
    'frontend_ref_count', 'frontend_ref_examples', 'bundle_route_evidence',
    'route_ref', 'controller_ref', 'logic_ref', 'static_db_io', 'side_effect_gate',
    'delivery_class', 'shared_fixture', 'priority', 'review_note']);
$priorityPaths = [
    '/p/datemenu/getbutton' => 1,
    '/p/ontoanalysis/getmealsstatus' => 2,
    '/p/stageanalyze/eligibility_uuid' => 3,
    '/p/stageanalyze/stage_eligibility_uuid' => 4,
    '/p/stageanalyze/team_list' => 5,
    '/p/stageanalyze/eatsinglestatus' => 6,
    '/p/stageanalyze/eatmultistatus' => 7,
    '/p/weight/lists' => 8,
    '/p/weight/show' => 9,
    '/p/weight/chart' => 10,
];
foreach ($selected as $key => $batchName) {
    $record = $matches[$key];
    $parts = explode('/', trim($record['path'], '/'));
    $controllerName = strtolower((string)($parts[1] ?? ''));
    $action = (string)($parts[2] ?? '');
    $routeRef = 'matrix_runtime_route_only';
    foreach ($routeGroups[$controllerName] ?? [] as $group) {
        $pattern = "~Route::" . strtolower($record['method']) . "\\(['\"]/"
            . preg_quote($action, '~') . "['\"]~i";
        if (preg_match($pattern, $group['text'], $routeHit, PREG_OFFSET_CAPTURE)) {
            $line = substr_count(substr($routeText, 0,
                $group['offset'] + $routeHit[0][1]), "\n") + 1;
            $routeRef = 'application/route.php:' . $line;
            break;
        }
    }
    $controllerFile = $controllers[$controllerName] ?? null;
    $logicFile = $logicFiles[$controllerName] ?? null;
    $logicMethod = $action;
    if ($key === '/p/datemenu/getmenuinfo') $logicMethod = 'menuInfo';
    if ($key === '/p/ontoanalysis/moreweiliang') $logicMethod = 'Moreweiliangs';
    if ($key === '/p/ontoanalysis/morepower') $logicMethod = 'Morepowers';
    if ($key === '/p/food/delete') $logicMethod = 'del';
    if ($key === '/p/restaurant/foodnutritionexport') {
        $logicFile = $logicFiles['dishes'] ?? null;
    }
    if ($key === '/p/agent/ask') $logicFile = $logicFiles['agentassistant'] ?? null;
    [$body, $controllerBase, $controllerLine] = $methodSource($controllerFile, $action);
    [$logicBody, $logicBase, $logicLine] = $methodSource($logicFile, $logicMethod);
    $controllerRef = $controllerBase === '' ? 'unresolved'
        : 'application/p/controller/' . $controllerBase . ':' . $controllerLine;
    $logicRef = $logicBase === '' ? 'transitive_unresolved'
        : 'application/p/logic/' . $logicBase . ':' . $logicLine;
    if ($key === '/p/cater/caterupload') $logicRef = 'application/model/File.php:75';
    if ($key === '/p/agent/suggestionclick') {
        $logicRef = 'application/model/AgentSuggestionClickLog.php:8;BaseModel.add';
    }
    if ($key === '/p/download/zip') $logicRef = 'controller_direct_filesystem';
    $sideEffect = 'none_seen_directly';
    $dbIo = 'read_likely_transitive_review';
    $note = 'static_only_not_HTTP_or_DB_accepted';
    if (strpos($body, 'add_initial_log') !== false) {
        $sideEffect = 'db_write_initial_log';
        $dbIo = 'read_plus_log_write';
    } elseif ($key === '/p/staff/info') {
        $sideEffect = 'db_write_user_code_plus_qrcode_file_if_missing';
        $dbIo = 'read_plus_conditional_write';
    } elseif ($key === '/p/datemenu/getwhere') {
        $sideEffect = 'getPower_if_missing_and_make_flag';
        $dbIo = 'read_plus_conditional_write';
    } elseif ($key === '/p/datemenu/gettarget') {
        $sideEffect = 'getPower_unconditional';
        $dbIo = 'read_plus_possible_write';
    } elseif ($key === '/p/datemenu/getmenu') {
        $sideEffect = 'change_flag_may_write_menu';
        $dbIo = 'read_or_write_branch';
    } elseif ($batchName === 'P5_EXPLICIT_WRITE') {
        $sideEffect = 'business_DML';
        $dbIo = 'write';
    } elseif (in_array($batchName,
        ['P6_PRINT_FILE', 'P7_UPLOAD_OSS', 'P8_AGENT_EXTERNAL', 'P9_RESEARCH_EXPORT'], true)) {
        $sideEffect = 'file_or_external_or_queue_risk';
        $dbIo = 'mixed_needs_isolated_boundary';
    } elseif (strpos($logicBody, '->save(') !== false
        || strpos($logicBody, '->update(') !== false
        || strpos($logicBody, '->delete(') !== false) {
        $sideEffect = 'logic_mutation_signal_review_before_HTTP';
        $dbIo = 'mixed_possible_write';
    } elseif ($batchName === 'P3_TEAM_ANALYTICS_MIXED'
        || $batchName === 'P4_PERSONAL_ANALYTICS_MIXED') {
        $sideEffect = 'analysis_chain_may_log_or_heavy_query';
        $dbIo = 'read_candidate_transitive';
    }
    if ($key === '/p/datemenu/getbutton') {
        $dbIo = 'read_date_menu_recipes';
        $sideEffect = 'time_sensitive_response_no_DML_seen';
    }
    if ($key === '/p/weight/lists' || $key === '/p/weight/show'
        || $key === '/p/weight/chart') $dbIo = 'read_member_weight_staff_status';
    if ($key === '/p/ontoanalysis/getmealsstatus') $dbIo = 'read_staff_date_menu_status';
    if ($key === '/p/zhaikerhealth/baseoptions'
        || $key === '/p/zhaikerhealth/groupanalysis') {
        $dbIo = 'read_store_plus_ai_customer_db';
        $sideEffect = 'cross_db_source_and_result_type_gate';
    }
    if ($key === '/p/agent/suggestionclick') {
        $sideEffect = 'db_write_click_log';
        $dbIo = 'write';
    }
    if (count($frontHits[$key] ?? []) < 1) {
        throw new RuntimeException('Selected path has no current frontend literal: ' . $key);
    }
    $refs = array_keys($frontHits[$key]);
    sort($refs, SORT_STRING);
    $routed = false;
    foreach ($refs as $ref) {
        if ($ref === 'main.js') { $routed = true; break; }
        $view = preg_replace('/\.vue$/', '', $ref);
        if (strpos($routerSources, '@/' . $view) !== false) { $routed = true; break; }
        if (strpos($ref, 'api/') === 0) {
            $apiImport = '@/' . preg_replace('/\.js$/', '', $ref);
            foreach (glob($front . '/view/researchManagement/*.vue') as $consumer) {
                $consumerRef = substr($consumer, strlen($front) + 1);
                $consumerRef = preg_replace('/\.vue$/', '', $consumerRef);
                if (strpos(file_get_contents($consumer), $apiImport) !== false
                    && strpos($routerSources, '@/' . $consumerRef) !== false) {
                    $routed = true;
                    break 2;
                }
            }
        }
    }
    $deliveryClass = $routed ? 'routed_current_source_candidate'
        : 'frontend_literal_component_or_history_pending';
    if ($routed && in_array($batchName,
        ['P3_TEAM_ANALYTICS_MIXED', 'P4_PERSONAL_ANALYTICS_MIXED'], true)) {
        $deliveryClass = 'routed_legacy_monitor_business_usage_pending';
    }
    if ($routed && (strpos($key, '/p/physicalmature/') === 0
        || strpos($key, '/p/registermanagement/') === 0)) {
        $deliveryClass = 'routed_erecord_natural_source_pending';
    }
    if ($routed && in_array($batchName,
        ['P6_PRINT_FILE', 'P7_UPLOAD_OSS', 'P8_AGENT_EXTERNAL', 'P9_RESEARCH_EXPORT'], true)) {
        $deliveryClass = 'routed_side_effect_isolation_pending';
    }
    $rowFixture = $fixture[$batchName];
    if (preg_match('~^/p/(role|user)/~', $key) && $batchName === 'P5_EXPLICIT_WRITE') {
        $rowFixture = 'isolated_role_user_scope;transaction_and_exact_cleanup';
    } elseif (preg_match('~^/p/(staff|registermanagement|physicalmature)/~', $key)
        && $batchName === 'P5_EXPLICIT_WRITE') {
        $rowFixture = 'isolated_staff_erecord_rows;transaction_and_exact_cleanup';
    } elseif (preg_match('~^/p/(rpe|weight)/~', $key)
        && $batchName === 'P5_EXPLICIT_WRITE') {
        $rowFixture = 'isolated_staff_training_weight_rows;external_dispatch_stub';
    } elseif ($key === '/p/food/delete') {
        $rowFixture = 'isolated_food_row;soft_delete_and_exact_cleanup';
    } elseif ($key === '/p/zhaikerhealth/baseoptions'
        || $key === '/p/zhaikerhealth/groupanalysis') {
        $rowFixture = 'one_type0_actor;bounded_ai_health_sessions;both_customer_db_identities';
    }
    if (strpos($key, '/p/professionevaluation/') !== false
        || strpos($key, '/p/physicalmature/') !== false) {
        $note .= ';erecord_natural_source_not_checked_in_this_batch_do_not_mark_NA';
    }
    if ($batchName === 'P0_PAUSED_NEXT5') $note .= ';A_first_HTTP_red_then_dev_MySQL_2006';
    fputcsv($output, [$record['method'], $record['path'], $batchName,
        $record['gate'], count($refs), implode('|', array_slice($refs, 0, 3)),
        $routed ? 'router_or_main_present' : 'literal_only_runtime_unproven',
        $routeRef, $controllerRef, $logicRef, $dbIo, $sideEffect,
        $deliveryClass, $rowFixture,
        $priorityPaths[$key] ?? '', $note]);
}
fclose($output);
