<?php

declare(strict_types=1);

// A/B code-local development MySQL SELECT/metadata gate. No business HTTP/DML.
if ($argc!==2 || $argv[1]!=='--preflight-ab-bill4') {
    fwrite(STDERR,"Explicit A/B Bill4 read-only flag required\n");
    exit(2);
}
$phase='runtime-boundary';
try {
    $project='/workspace/zhct/zhctproject/store';
    $lanes=[
        'A'=>['root'=>'/tmp/zhct-http-store-a.l5omH8','pid'=>21023,'port'=>18097],
        'B'=>['root'=>'/tmp/zhct-http-store-mysql.dQTTBP','pid'=>10001,'port'=>18081],
    ];
    $expectedSources=[
        'model'=>'daa8f7181928d06b53e57504f229eaf04088e3ea7eaccfad55c07d091db03c18',
        'controller'=>'8d99ea508e165d694a156d43420cb98f3a809a0fb9a120716e4f9dd90a4b9cf2',
        'logic'=>'30850e03938d2860eb0230a5916860a5282c901c2ad0033455312337b2aad5ee',
        'recipe_batch'=>'0313e936e55b97d13e97a2437ba755409b4341cc670ac83bf9282cb5658f6db4',
    ];
    $sourcePaths=[
        'model'=>'application/model/Bill.php',
        'controller'=>'application/p/controller/Bill.php',
        'logic'=>'application/p/logic/Bill.php',
        'recipe_batch'=>'application/p/logic/RecipeBatch.php',
    ];
    $load=static function(string $path): array { return require $path; };
    $local=$load($project.'/application/config/local/database.php');
    $same=[];
    $boundary=[];
    foreach ($lanes as $lane=>$info) {
        $cmd=str_replace("\0",' ',(string)file_get_contents(
            '/proc/'.$info['pid'].'/cmdline'));
        if (strpos($cmd,' -S 127.0.0.1:'.$info['port'].' ')===false
            || strpos($cmd,$info['root'].'/public')===false
            || substr_count($cmd,' -S ')!==1) {
            throw new RuntimeException('Wrong A/B isolated Store process');
        }
        $workers='';
        foreach (explode("\0",(string)file_get_contents(
            '/proc/'.$info['pid'].'/environ')) as $entry) {
            $pair=explode('=',$entry,2);
            if (count($pair)===2 && $pair[0]==='PHP_CLI_SERVER_WORKERS') {
                $workers=$pair[1];
            }
        }
        if (!in_array($workers,['','1'],true)) {
            throw new RuntimeException('Multiple HTTP workers not approved');
        }
        $raw=@file_get_contents('http://127.0.0.1:'.$info['port']
            .'/__poc_boundary',false,stream_context_create([
                'http'=>['timeout'=>3,'ignore_errors'=>true],
            ]));
        $headers=$http_response_header??[];
        $status=preg_match('/^HTTP\/\S+\s+(\d+)/',
            (string)($headers[0]??''),$match)?(int)$match[1]:0;
        $json=json_decode((string)$raw,true);
        if ($status!==200 || !is_array($json)
            || empty($json['external_stubs'])
            || empty($json['redis_isolated'])
            || !empty($json['real_queue'])) {
            throw new RuntimeException('A/B HTTP isolation failed');
        }
        $boundary[$lane]=true;
        $config=$load($info['root'].'/application/database.php');
        $match=true;
        foreach (['hostname','database','username','password'] as $key) {
            $match=$match && isset($config[$key],$local[$key])
                && $config[$key]===$local[$key];
        }
        $match=$match && ($config['type']??null)==='mysql'
            && (int)($config['hostport']??3306)
                ===(int)($local['hostport']??3306);
        $same[$lane]=$match;
        if (!$match) throw new RuntimeException('Not code-local development MySQL');
        foreach ($sourcePaths as $key=>$file) {
            if ((string)hash_file('sha256',$info['root'].'/'.$file)
                !==$expectedSources[$key]) {
                throw new RuntimeException('Bill source SHA differs');
            }
        }
    }

    $phase='development-mysql';
    $db=new PDO('mysql:host='.$local['hostname'].';port='.
        (int)($local['hostport']??3306).';dbname='.$local['database']
        .';charset=utf8mb4',(string)$local['username'],
        (string)$local['password'],[
            PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_EMULATE_PREPARES=>false,
            PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC,
            PDO::ATTR_TIMEOUT=>8,
        ]);
    $version=(string)$db->query('SELECT VERSION()')->fetchColumn();
    if (strpos($version,'5.6.')===false) {
        throw new RuntimeException('Not code-local MySQL5.6');
    }
    $scalar=static function(string $sql,array $args=[]) use ($db): int {
        $stmt=$db->prepare($sql);
        $stmt->execute($args);
        return (int)$stmt->fetchColumn();
    };
    $schema=(string)$local['database'];
    $required=[
        'ydy_recipes'=>['id','uuid','dishes_uuid','menu_date',
            'restaurant_id','meal_times','limit_num'],
        'ydy_dishes'=>['uuid','name','category','file_path','price',
            'is_del','status','store_id'],
        'ydy_date_menu'=>['dishes_uuid','menu_date','restaurant_id',
            'meal_times','meal_type'],
        'ydy_restaurants'=>['id','uuid','status','is_delete'],
        'ydy_menu'=>['uuid','filter','link'],
        'ydy_user'=>['id','uuid','account','type','status'],
        'ydy_role'=>['id','uuid','name','status'],
        'ydy_user_role'=>['user_uuid','role_uuid'],
        'ydy_role_menu'=>['role_uuid','menu_uuid'],
        'ydy_user_data'=>['user_uuid','data_uuid','type'],
    ];
    $columnStmt=$db->prepare('SELECT COLUMN_NAME FROM information_schema.COLUMNS'
        .' WHERE TABLE_SCHEMA=? AND TABLE_NAME=?');
    $missing=[];
    foreach ($required as $table=>$columns) {
        $columnStmt->execute([$schema,$table]);
        $actual=array_map('strtolower',$columnStmt->fetchAll(PDO::FETCH_COLUMN));
        $delta=array_values(array_diff($columns,$actual));
        if ($delta!==[]) $missing[$table]=$delta;
    }
    if ($missing!==[]) {
        echo json_encode(['status'=>'blocked-schema','missing'=>$missing],
            JSON_UNESCAPED_SLASHES).PHP_EOL;
        exit(3);
    }
    $phase='permission-metadata';
    $meta=[];
    foreach (['ydy_user','ydy_role','ydy_user_role',
        'ydy_role_menu','ydy_user_data'] as $table) {
        $meta[$table]=[
            'triggers'=>$scalar('SELECT COUNT(*) FROM information_schema.TRIGGERS'
                .' WHERE TRIGGER_SCHEMA=? AND EVENT_OBJECT_TABLE=?',
                [$schema,$table]),
            'foreign_keys'=>$scalar('SELECT COUNT(*)'
                .' FROM information_schema.KEY_COLUMN_USAGE'
                .' WHERE TABLE_SCHEMA=? AND TABLE_NAME=?'
                .' AND REFERENCED_TABLE_NAME IS NOT NULL',
                [$schema,$table]),
        ];
    }
    $uniqueExpected=[
        'ydy_role'=>['name'],
        'ydy_user_role'=>['user_uuid','role_uuid'],
        'ydy_role_menu'=>['role_uuid','menu_uuid'],
        'ydy_user_data'=>['user_uuid','data_uuid','type'],
    ];
    $indexStmt=$db->prepare('SELECT INDEX_NAME,NON_UNIQUE,SEQ_IN_INDEX,'
        .' COLUMN_NAME FROM information_schema.STATISTICS'
        .' WHERE TABLE_SCHEMA=? AND TABLE_NAME=?'
        .' ORDER BY INDEX_NAME,SEQ_IN_INDEX');
    foreach ($uniqueExpected as $table=>$columns) {
        $indexStmt->execute([$schema,$table]);
        $indexes=[];
        foreach ($indexStmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $name=(string)$row['INDEX_NAME'];
            $indexes[$name]['unique']=(int)$row['NON_UNIQUE']===0;
            $indexes[$name]['columns'][]=strtolower((string)$row['COLUMN_NAME']);
        }
        $found=false;
        foreach ($indexes as $index) {
            if ($index['unique'] && $index['columns']===$columns) $found=true;
        }
        $meta[$table]['required_unique_present']=$found;
    }
    $markerCounts=[];
    foreach (['ydy_user'=>'account','ydy_role'=>'name',
        'ydy_user_role'=>'create_by','ydy_role_menu'=>'create_by'] as $table=>$field) {
        $markerCounts[$table]=$scalar('SELECT COUNT(*) FROM '.$table
            .' WHERE '.$field.' LIKE ?', ['CDXPARBILL%']);
    }

    $phase='natural-recipe';
    $candidateSql='SELECT r.menu_date menu_date,'
        .' r.restaurant_id restaurant_id,r.meal_times meal_times,'
        .' r.dishes_uuid dishes_uuid,st.uuid restaurant_uuid'
        .' FROM ydy_recipes r JOIN ydy_dishes d ON d.uuid=r.dishes_uuid'
        .' JOIN ydy_restaurants st ON st.id=r.restaurant_id'
        .' WHERE d.is_del=1 AND d.status=1 AND d.store_id=10001'
        .' AND st.status=1 AND st.is_delete=2'
        .' AND r.meal_times BETWEEN 1 AND 3 AND EXISTS('
        .' SELECT 1 FROM ydy_date_menu dm'
        .' WHERE dm.dishes_uuid=r.dishes_uuid'
        .' AND dm.menu_date=r.menu_date'
        .' AND dm.restaurant_id=r.restaurant_id'
        .' AND dm.meal_times=r.meal_times AND dm.meal_type=3)'
        .' ORDER BY r.menu_date DESC LIMIT 1';
    $candidate=$db->query($candidateSql)->fetch(PDO::FETCH_ASSOC);
    if (!$candidate) throw new RuntimeException('No natural Bill recipe/menu');
    $date=substr((string)$candidate['menu_date'],0,10);
    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date)) {
        throw new RuntimeException('Invalid natural date');
    }
    $restaurant=(int)$candidate['restaurant_id'];
    $meal=(int)$candidate['meal_times'];
    $counts=[
        'recipe_day_restaurant_meal'=>$scalar('SELECT COUNT(*) FROM ydy_recipes'
            .' WHERE menu_date=? AND restaurant_id=? AND meal_times=?',
            [$date,$restaurant,$meal]),
        'visible_bill_day'=>$scalar('SELECT COUNT(*) FROM ydy_recipes r'
            .' JOIN ydy_dishes d ON d.uuid=r.dishes_uuid'
            .' WHERE r.menu_date=? AND r.restaurant_id=?'
            .' AND r.meal_times=? AND d.is_del=1',
            [$date,$restaurant,$meal]),
        'copyable_day'=>$scalar('SELECT COUNT(*) FROM ydy_recipes r'
            .' JOIN ydy_dishes d ON d.uuid=r.dishes_uuid'
            .' WHERE r.menu_date=? AND r.restaurant_id=?'
            .' AND r.meal_times=? AND d.store_id=10001'
            .' AND d.is_del=1 AND d.status=1',
            [$date,$restaurant,$meal]),
        'distinct_dates_restaurant'=>$scalar('SELECT COUNT(DISTINCT menu_date)'
            .' FROM ydy_recipes WHERE restaurant_id=?',[$restaurant]),
    ];
    $menuStmt=$db->prepare('SELECT uuid,filter FROM ydy_menu'
        .' WHERE LOWER(filter) LIKE ?');
    $menuStmt->execute(['%p/bill/look_bill_list%']);
    $menuMatches=[];
    foreach ($menuStmt->fetchAll(PDO::FETCH_ASSOC) as $menu) {
        $filter='/'.trim(strtolower((string)$menu['filter']),' /');
        if ($filter==='/p/bill/look_bill_list') {
            $menuMatches[]=substr(hash('sha256',(string)$menu['uuid']),0,12);
        }
    }
    $priceStmt=$db->prepare('SELECT DATA_TYPE,NUMERIC_PRECISION,NUMERIC_SCALE'
        .' FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=?'
        .' AND TABLE_NAME=? AND COLUMN_NAME=?');
    $priceStmt->execute([$schema,'ydy_dishes','price']);
    $priceMeta=$priceStmt->fetch(PDO::FETCH_ASSOC);

    $phase='empty-native-sql';
    $emptySql='SELECT `w`.`name`,`a`.`dishes_uuid`,`w`.`category` as type,'
        .'`a`.`meal_times`,`w`.`file_path`,`w`.`price`,`a`.`limit_num`'
        .' FROM `ydy_recipes` `a` LEFT JOIN `ydy_dishes` `w`'
        .' ON `a`.`dishes_uuid`=`w`.`uuid`'
        .' WHERE `a`.`menu_date` = \'2001-01-02\''
        .' AND `w`.`is_del` = 1 AND `a`.`restaurant_id` = \'0\''
        .' AND 1=0';
    $emptyRows=$db->query($emptySql)->fetchAll(PDO::FETCH_ASSOC);
    echo json_encode([
        'status'=>'read-only-preflight','mysql_version'=>$version,
        'runtime_boundary'=>$boundary,'same_code_local'=>$same,
        'bill_four_source_sha256_fixed'=>true,
        'required_tables'=>count($required),'missing_columns'=>0,
        'permission_metadata'=>$meta,'marker_counts'=>$markerCounts,
        'candidate'=>[
            'relation_sha12'=>substr(hash('sha256',
                (string)$candidate['dishes_uuid'].'|'.$date.'|'
                .$restaurant.'|'.$meal),0,12),
            'date_sha12'=>substr(hash('sha256',$date),0,12),
            'meal_times'=>$meal,
            'restaurant_sha12'=>substr(hash('sha256',
                (string)$candidate['restaurant_uuid']),0,12),
        ],
        'counts'=>$counts,
        'bill_menu_exact_count'=>count($menuMatches),
        'bill_menu_sha12'=>$menuMatches,
        'price_decl'=>[
            'type'=>$priceMeta?strtolower((string)$priceMeta['DATA_TYPE']):null,
            'precision'=>$priceMeta?(int)$priceMeta['NUMERIC_PRECISION']:null,
            'scale'=>$priceMeta?(int)$priceMeta['NUMERIC_SCALE']:null,
        ],
        'native_backtick_forced_empty_rows'=>count($emptyRows),
    ],JSON_UNESCAPED_SLASHES).PHP_EOL;
    if (array_sum($markerCounts)!==0 || count($menuMatches)!==1
        || min($counts)<1 || count($emptyRows)!==0) exit(1);
} catch (Throwable $error) {
    echo json_encode(['status'=>'blocked','phase'=>$phase,
        'class'=>get_class($error),
        'code'=>(string)$error->getCode()],JSON_UNESCAPED_SLASHES).PHP_EOL;
    exit(1);
}
