<?php

declare(strict_types=1);

$dialect = getenv('POC_DIALECT');
$baseUrl = rtrim((string)getenv('POC_BASE_URL'), '/');
if (!$baseUrl || !in_array($dialect, ['mysql', 'dm'], true)) exit(2);
set_exception_handler(static function (Throwable $error): void {
    echo json_encode(['status' => 'failed', 'failure_class' => get_class($error),
        'failure_line' => $error->getLine(), 'failure_file' => basename($error->getFile()),
        'message_sha256' => hash('sha256', $error->getMessage())]), "\n";
    exit(2);
});
$host = getenv('POC_HOST');
$port = getenv('POC_PORT');
$schema = getenv('POC_SCHEMA');
$dsn = $dialect === 'dm'
    ? "dm:host=$host;port=$port;schema=$schema"
    : "mysql:host=$host;port=$port;dbname=$schema;charset=utf8mb4";
$pdo = new PDO($dsn, getenv('POC_USER'), getenv('POC_PASSWORD'),
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$s = static function (string $name) use ($dialect): string {
    return $dialect === 'dm' ? 'YDY_' . strtoupper($name) : 'ydy_' . strtolower($name);
};
$candidate = $pdo->query('SELECT E.ID,E.CODE,E.RESTAURANT_ID FROM ' . $s('equipment') . ' E'
    . ' JOIN ' . $s('restaurants') . ' R ON E.RESTAURANT_ID=R.ID'
    . " WHERE E.DEL_FLAG=0 AND E.TYPE IN (2,3,4,10) AND E.CODE<>'' AND R.IS_DELETE=2"
    . ' ORDER BY E.ID ' . ($dialect === 'dm' ? 'FETCH FIRST 1 ROWS ONLY' : 'LIMIT 1'));
$equipment = array_change_key_case($candidate->fetch(PDO::FETCH_ASSOC) ?: [], CASE_LOWER);
$dishQuery = $pdo->query('SELECT UUID,MENU_QUANTITY,ATTRIBUTE FROM ' . $s('dishes')
    . ' WHERE IS_DEL=1 AND STATUS=1 AND ATTRIBUTE=1 ORDER BY ID '
    . ($dialect === 'dm' ? 'FETCH FIRST 1 ROWS ONLY' : 'LIMIT 1'));
$dish = array_change_key_case($dishQuery->fetch(PDO::FETCH_ASSOC) ?: [], CASE_LOWER);
if (!$equipment || !$dish || empty($dish['uuid'])) {
    throw new RuntimeException('Device recipe equipment/dish fixture missing');
}
$restaurantId = (int)$equipment['restaurant_id'];
$dishUuid = (string)$dish['uuid'];
$originalQuantity = (int)$dish['menu_quantity'];
$originalAttribute = (int)$dish['attribute'];
if (($argv[1] ?? '') === '--audit') {
    $audit = new PDO($dsn, getenv('POC_USER'), getenv('POC_PASSWORD'),
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
    $check = $audit->prepare('SELECT COUNT(*) FROM ' . $s('recipes')
        . ' WHERE RESTAURANT_ID=? AND DISHES_UUID=? AND MENU_DATE>=? AND MENU_DATE<? AND CREATE_BY=?');
    $check->execute([$restaurantId, $dishUuid, date('Y-m-d', strtotime('+3650 day')),
        date('Y-m-d', strtotime('+3670 day')), 'DeviceApi']);
    $remaining = (int)$check->fetchColumn();
    $invalidCheck = $audit->prepare('SELECT COUNT(*) FROM ' . $s('recipes')
        . ' WHERE RESTAURANT_ID=? AND CREATE_BY LIKE ?');
    $invalidCheck->execute([$restaurantId, 'CDXDBCP10%']);
    $remaining += (int)$invalidCheck->fetchColumn();
    echo json_encode(['status' => $remaining === 0 ? 'independent_zero_pass' : 'audit_failed',
        'dialect' => $dialect, 'mode' => '--audit', 'remaining_test_window_device_recipes' => $remaining]), "\n";
    exit($remaining === 0 ? 0 : 2);
}
$sourceDate = '';
$targetDate = '';
$slotCount = $pdo->prepare('SELECT COUNT(*) FROM ' . $s('recipes')
    . ' WHERE RESTAURANT_ID=? AND MENU_DATE=? AND MEAL_TIMES=1');
for ($offset = 3650; $offset < 3670; $offset += 2) {
    $candidateSource = date('Y-m-d', strtotime('+' . $offset . ' day'));
    $candidateTarget = date('Y-m-d', strtotime('+' . ($offset + 1) . ' day'));
    $slotCount->execute([$restaurantId, $candidateSource]);
    $sourceCount = (int)$slotCount->fetchColumn();
    $slotCount->execute([$restaurantId, $candidateTarget]);
    if ($sourceCount === 0 && (int)$slotCount->fetchColumn() === 0) {
        $sourceDate = $candidateSource;
        $targetDate = $candidateTarget;
        break;
    }
}
if ($sourceDate === '') throw new RuntimeException('No empty future recipe slots available');
$post = static function (string $route, array $payload, string $case = '') use ($baseUrl): array {
    $context = stream_context_create(['http' => [
        'method' => 'POST',
        'header' => "Content-Type: application/x-www-form-urlencoded\r\n"
            . ($case === 'ROLLBACK_SECOND_SLOT' ? "X-DB-Pilot-Case: ROLLBACK_SECOND_SLOT\r\n" : ''),
        'content' => http_build_query($payload, '', '&', PHP_QUERY_RFC3986),
        'timeout' => 20, 'ignore_errors' => true,
    ]]);
    $body = file_get_contents($baseUrl . $route, false, $context);
    $response = is_string($body) ? json_decode($body, true) : null;
    if (!is_array($response)) throw new RuntimeException('Device recipe HTTP response not JSON');
    return $response;
};
$base = [
    'equipment_code' => (string)$equipment['code'],
    'restaurant_id' => $restaurantId,
    'dishes_uuid' => $dishUuid,
    'meal_times' => 1,
];
$recipeRows = $pdo->prepare('SELECT UUID FROM ' . $s('recipes')
    . ' WHERE RESTAURANT_ID=? AND MENU_DATE=? AND MEAL_TIMES=1 AND DISHES_UUID=?');
$dishCurrent = $pdo->prepare('SELECT MENU_QUANTITY,ATTRIBUTE FROM ' . $s('dishes') . ' WHERE UUID=?');
$invalidUuid = 'CDXDBCP10' . strtoupper(bin2hex(random_bytes(8)));
try {
    $add = $post('/api/device/recipe/addDishes', $base + ['menu_date' => $sourceDate]);
    $recipeRows->execute([$restaurantId, $sourceDate, $dishUuid]);
    $sourceRows = $recipeRows->fetchAll(PDO::FETCH_ASSOC);
    if ((int)($add['code'] ?? -1) !== 0
        || (int)($add['data']['added_count'] ?? 0) !== 1
        || count($sourceRows) !== 1) {
        throw new RuntimeException('Device recipe add/database readback mismatch');
    }
    $repeatAdd = $post('/api/device/recipe/addDishes', $base + ['menu_date' => $sourceDate]);
    $recipeRows->execute([$restaurantId, $sourceDate, $dishUuid]);
    if ((int)($repeatAdd['code'] ?? -1) !== 0
        || (int)($repeatAdd['data']['added_count'] ?? -1) !== 0
        || count($recipeRows->fetchAll(PDO::FETCH_ASSOC)) !== 1) {
        throw new RuntimeException('Device recipe duplicate add was not idempotent');
    }
    $copy = $post('/api/device/recipe/copy', [
        'equipment_code' => (string)$equipment['code'],
        'restaurant_id' => $restaurantId,
        'source_type' => 'day', 'source_date' => $sourceDate,
        'target_date' => $targetDate, 'meal_times' => 1,
        'overwrite' => 1, 'strict' => 1,
    ]);
    $recipeRows->execute([$restaurantId, $targetDate, $dishUuid]);
    if ((int)($copy['code'] ?? -1) !== 0
        || (int)($copy['data']['copied_count'] ?? 0) !== 1
        || count($recipeRows->fetchAll(PDO::FETCH_ASSOC)) !== 1) {
        throw new RuntimeException('Device recipe copy/database readback mismatch');
    }
    // The prior fetchAll consumed the cursor; read the owned target again for idempotency.
    $recipeRows->execute([$restaurantId, $targetDate, $dishUuid]);
    $initialTarget = array_change_key_case($recipeRows->fetch(PDO::FETCH_ASSOC) ?: [], CASE_LOWER);
    $copyRequest = ['equipment_code' => (string)$equipment['code'], 'restaurant_id' => $restaurantId,
        'source_type' => 'day', 'source_date' => $sourceDate, 'target_date' => $targetDate,
        'meal_times' => 1, 'strict' => 1];
    $copySkip = $post('/api/device/recipe/copy', $copyRequest + ['overwrite' => 0]);
    $recipeRows->execute([$restaurantId, $targetDate, $dishUuid]);
    $targetAfterSkip = $recipeRows->fetchAll(PDO::FETCH_ASSOC);
    if (($copySkip['code'] ?? null) !== 0 || ($copySkip['data']['copied_count'] ?? null) !== 0
        || ($copySkip['data']['skipped_count'] ?? null) !== 0 || count($targetAfterSkip) !== 1
        || (array_change_key_case($targetAfterSkip[0], CASE_LOWER)['uuid'] ?? null) !== ($initialTarget['uuid'] ?? null)) {
        throw new RuntimeException('Non-overwriting copy changed the existing owned target');
    }
    $copyOverwrite = $post('/api/device/recipe/copy', $copyRequest + ['overwrite' => 1]);
    $recipeRows->execute([$restaurantId, $targetDate, $dishUuid]);
    if (($copyOverwrite['code'] ?? null) !== 0 || ($copyOverwrite['data']['copied_count'] ?? null) !== 1
        || ($copyOverwrite['data']['deleted_count'] ?? null) !== 1
        || count($recipeRows->fetchAll(PDO::FETCH_ASSOC)) !== 1) {
        throw new RuntimeException('Overwriting copy did not replace exactly one owned row');
    }
    // Own invalid second source slot. First slot performs real DELETE/INSERT before this fails.
    $emptySecond = $pdo->prepare('SELECT COUNT(*) FROM ' . $s('recipes')
        . ' WHERE RESTAURANT_ID=? AND MENU_DATE IN (?,?) AND MEAL_TIMES=2');
    $emptySecond->execute([$restaurantId, $sourceDate, $targetDate]);
    if ((int)$emptySecond->fetchColumn() !== 0) throw new RuntimeException('Second test slot not empty');
    $missingDish = $pdo->prepare('SELECT COUNT(*) FROM ' . $s('dishes') . ' WHERE UUID=?');
    $missingDish->execute([$invalidUuid]);
    if ((int)$missingDish->fetchColumn() !== 0) throw new RuntimeException('Invalid fixture UUID collision');
    $invalidInsert = $pdo->prepare('INSERT INTO ' . $s('recipes')
        . ' (UUID,DISHES_UUID,RESTAURANT_ID,MENU_DATE,MEAL_TIMES,TYPE,CREATE_TIME,CREATE_BY,LIMIT_NUM)'
        . ' VALUES (?,?,?,?,2,1,?,?,0)');
    $invalidInsert->execute([$invalidUuid, $invalidUuid, $restaurantId, $sourceDate,
        date('Y-m-d H:i:s'), $invalidUuid]);
    $transactionSnapshot = static function (PDO $db) use ($s, $restaurantId, $sourceDate, $targetDate, $dishUuid): string {
        $recipes = $db->prepare('SELECT * FROM ' . $s('recipes')
            . ' WHERE RESTAURANT_ID=? AND MENU_DATE IN (?,?) AND MEAL_TIMES IN (1,2) ORDER BY UUID');
        $recipes->execute([$restaurantId, $sourceDate, $targetDate]);
        $dish = $db->prepare('SELECT MENU_QUANTITY,ATTRIBUTE FROM ' . $s('dishes') . ' WHERE UUID=?');
        $dish->execute([$dishUuid]);
        return hash('sha256', serialize([$recipes->fetchAll(PDO::FETCH_ASSOC), $dish->fetchAll(PDO::FETCH_ASSOC)]));
    };
    $beforeRollback = $transactionSnapshot($pdo);
    $rollbackRequest = $copyRequest;
    $rollbackRequest['meal_times'] = '1,2';
    $rollbackRequest['overwrite'] = 1;
    $rollback = $post('/api/device/recipe/copy', $rollbackRequest, 'ROLLBACK_SECOND_SLOT');
    $freshRollback = new PDO($dsn, getenv('POC_USER'), getenv('POC_PASSWORD'),
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
    $rollbackExact = $beforeRollback === $transactionSnapshot($freshRollback);
    if (($rollback['code'] ?? null) !== 1 || ($rollback['message'] ?? '') !== '源菜谱存在停用或删除菜品'
        || !$rollbackExact) throw new RuntimeException('Second-slot failure did not fully restore original rows/counters');
    $removeInvalid = $pdo->prepare('DELETE FROM ' . $s('recipes') . ' WHERE UUID=? AND CREATE_BY=? AND DISHES_UUID=?');
    $removeInvalid->execute([$invalidUuid, $invalidUuid, $invalidUuid]);
    if ($removeInvalid->rowCount() !== 1) throw new RuntimeException('Invalid fixture cleanup mismatch');
    $dishCurrent->execute([$dishUuid]);
    $afterCopy = array_change_key_case($dishCurrent->fetch(PDO::FETCH_ASSOC) ?: [], CASE_LOWER);
    if ((int)($afterCopy['menu_quantity'] ?? -1) !== $originalQuantity + 2) {
        throw new RuntimeException('Dish menu quantity did not reflect add and copy');
    }
    foreach ([$targetDate, $sourceDate] as $date) {
        $deleted = $post('/api/device/recipe/deleteDishes', $base + ['menu_date' => $date]);
        $deletes[$date === $sourceDate ? 'delete_source' : 'delete_target'] = $deleted;
        $recipeRows->execute([$restaurantId, $date, $dishUuid]);
        if ((int)($deleted['code'] ?? -1) !== 0
            || (int)($deleted['data']['deleted_count'] ?? 0) !== 1
            || $recipeRows->fetch(PDO::FETCH_ASSOC)) {
            throw new RuntimeException('Device recipe delete/database readback mismatch');
        }
    }
    $deleteEmpty = $post('/api/device/recipe/deleteDishes', $base + ['menu_date' => $targetDate]);
    $recipeRows->execute([$restaurantId, $targetDate, $dishUuid]);
    if (($deleteEmpty['code'] ?? null) !== 0 || ($deleteEmpty['data']['deleted_count'] ?? null) !== 0
        || $recipeRows->fetch(PDO::FETCH_ASSOC)) {
        throw new RuntimeException('Repeated delete is not an empty idempotent result');
    }
    $dishCurrent->execute([$dishUuid]);
    $afterDelete = array_change_key_case($dishCurrent->fetch(PDO::FETCH_ASSOC) ?: [], CASE_LOWER);
    if ((int)($afterDelete['menu_quantity'] ?? -1) !== $originalQuantity
        || (int)($afterDelete['attribute'] ?? -1) !== $originalAttribute) {
        throw new RuntimeException('Dish counters or attribute were not restored');
    }
    // Map only fixture-owned identities after independent database/request validation.
    // Do not mask arbitrary IDs, dates, missing fields or JSON type differences.
    $sourceRecipe = array_change_key_case($sourceRows[0], CASE_LOWER);
    if (($add['data']['recipe_uuid'] ?? null) !== ($sourceRecipe['uuid'] ?? null)) {
        throw new RuntimeException('Returned recipe UUID is not the independently read fixture');
    }
    $contracts = ['add' => $add, 'repeat' => $repeatAdd, 'copy' => $copy,
        'copy_skip_existing' => $copySkip, 'copy_overwrite_existing' => $copyOverwrite,
        'delete_empty_target' => $deleteEmpty] + $deletes;
    foreach ($contracts as $case => &$response) {
        $data =& $response['data'];
        if (($data['equipment_code'] ?? null) !== $base['equipment_code']
            || ($data['restaurant_id'] ?? null) !== $restaurantId) {
            throw new RuntimeException('Returned device/restaurant identity or type mismatch');
        }
        $data['equipment_code'] = 'FIXTURE_DEVICE';
        $data['restaurant_id'] = 1; // Preserve the required JSON integer contract.
        if (strpos($case, 'copy') === 0) {
            if (($data['date_pairs'] ?? null) !== [['source_date' => $sourceDate, 'target_date' => $targetDate]]) {
                throw new RuntimeException('Returned date pairs mismatch');
            }
            $data['date_pairs'] = [['source_date' => 'FIXTURE_SOURCE_DATE', 'target_date' => 'FIXTURE_TARGET_DATE']];
        } else {
            if (($data['dishes_uuid'] ?? null) !== $dishUuid
                || ($data['menu_date'] ?? null) !== (in_array($case, ['delete_target', 'delete_empty_target'], true) ? $targetDate : $sourceDate)) {
                throw new RuntimeException('Returned dish/date identity or type mismatch');
            }
            $data['dishes_uuid'] = 'FIXTURE_DISH';
            $data['menu_date'] = in_array($case, ['delete_target', 'delete_empty_target'], true) ? 'FIXTURE_TARGET_DATE' : 'FIXTURE_SOURCE_DATE';
            if ($case === 'add') $data['recipe_uuid'] = 'FIXTURE_RECIPE';
        }
        unset($data);
    }
    unset($response);
    $contracts['copy_rollback_second_slot'] = $rollback;
    echo json_encode(['dialect' => $dialect, 'status' => 'passed',
        'recipe_add_duplicate_copy_delete' => true,
        'response_contracts' => $contracts,
        'second_slot_failure_full_state_rollback' => $rollbackExact,
        'source_and_target_rows_after_cleanup' => 0,
        'dish_quantity_restored' => true]) . PHP_EOL;
} finally {
    $pdo->prepare('DELETE FROM ' . $s('recipes') . ' WHERE UUID=? AND CREATE_BY=? AND DISHES_UUID=?')
        ->execute([$invalidUuid, $invalidUuid, $invalidUuid]);
    $remaining = 0;
    foreach ([$sourceDate, $targetDate] as $date) {
        $recipeRows->execute([$restaurantId, $date, $dishUuid]);
        $rows = $recipeRows->fetchAll(PDO::FETCH_ASSOC);
        if (count($rows) > 1) throw new RuntimeException('Unexpected recipes in synthetic future slot');
        if ($rows) {
            $pdo->prepare('DELETE FROM ' . $s('recipes')
                . ' WHERE RESTAURANT_ID=? AND MENU_DATE=? AND MEAL_TIMES=1 AND DISHES_UUID=?')
                ->execute([$restaurantId, $date, $dishUuid]);
            $remaining += count($rows);
        }
    }
    $dishCurrent->execute([$dishUuid]);
    $observed = array_change_key_case($dishCurrent->fetch(PDO::FETCH_ASSOC) ?: [], CASE_LOWER);
    $currentQuantity = (int)($observed['menu_quantity'] ?? -1);
    if ($currentQuantity !== $originalQuantity || (int)($observed['attribute'] ?? -1) !== $originalAttribute) {
        if ($currentQuantity < $originalQuantity - 2 || $currentQuantity > $originalQuantity + 2) {
            throw new RuntimeException('Dish counter changed beyond synthetic recipe scope');
        }
        $restore = $pdo->prepare('UPDATE ' . $s('dishes')
            . ' SET MENU_QUANTITY=?,ATTRIBUTE=? WHERE UUID=? AND MENU_QUANTITY=?');
        $restore->execute([$originalQuantity, $originalAttribute, $dishUuid, $currentQuantity]);
        if ($restore->rowCount() !== 1) throw new RuntimeException('Dish state restore compare-and-set failed');
    }
    foreach ([$sourceDate, $targetDate] as $date) {
        $slotCount->execute([$restaurantId, $date]);
        if ((int)$slotCount->fetchColumn() !== 0) {
            throw new RuntimeException('Synthetic device recipe slot cleanup left rows');
        }
    }
}
