<?php
declare(strict_types=1);
// This file can only be entered by a fixed wrapper; never a user-supplied include.
if (!defined('AEN_SCALAR_ROUTE')) { http_response_code(404); exit; }
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
header('X-Content-Type-Options: nosniff');
header('X-AEN-Demo: stateless-fixtures');
header('X-Request-Id: demo-'.bin2hex(random_bytes(6)));

function reply(int $status, array $payload): never {
    http_response_code($status);
    echo json_encode(['demo'=>true]+$payload, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_THROW_ON_ERROR);
    exit;
}
function fail(int $status, string $code, string $message, array $fields=[]): never {
    reply($status,['error'=>['code'=>$code,'message'=>$message,'fields'=>(object)$fields]]);
}
function query(string $key, string $default=''): string {
    $value=$_GET[$key]??$default;
    if (!is_string($value)||strlen($value)>120) fail(422,'INVALID_QUERY','Проверьте параметры запроса',[$key=>'Нужна короткая строка']);
    return $value;
}
function integerQuery(string $key, int $default, int $min, int $max): int {
    $value=query($key,(string)$default);
    if (!preg_match('/^(0|[1-9][0-9]*)$/D',$value)||(float)$value<$min||(float)$value>$max) fail(422,'INVALID_QUERY','Проверьте параметры запроса',[$key=>"Целое число от $min до $max"]);
    return (int)$value;
}
function rawBody(int $limit=12288): string {
    if ((int)($_SERVER['CONTENT_LENGTH']??0)>$limit) fail(413,'BODY_TOO_LARGE','Превышен лимит тела запроса');
    $raw=file_get_contents('php://input',false,null,0,$limit+1);
    if ($raw===false||strlen($raw)>$limit) fail(413,'BODY_TOO_LARGE','Превышен лимит тела запроса');
    return $raw;
}
function jsonBody(): array {
    if (strtolower(trim(explode(';',$_SERVER['CONTENT_TYPE']??'')[0]))!=='application/json') fail(415,'UNSUPPORTED_MEDIA','Нужен Content-Type: application/json');
    $raw=rawBody();
    try { $value=json_decode($raw,true,16,JSON_THROW_ON_ERROR); }
    catch (JsonException) { fail(400,'INVALID_JSON','Не удалось прочитать JSON'); }
    // Decode object shape separately: {} is a valid JSON object, [] is not.
    if (!is_array($value)||!str_starts_with(ltrim($raw),'{')) fail(400,'OBJECT_REQUIRED','Ожидается JSON-объект');
    return $value;
}
function tickets(): array {
    $kinds=['Монтаж освещения','Проверка щита','Сервис оборудования','Обновление сети','Диагностика линии','Подключение датчиков'];
    $states=['new','in_progress','done']; $out=[];
    for($i=1;$i<=18;$i++) $out[]=['id'=>$i,'title'=>$kinds[($i-1)%6].' · объект '.$i,'status'=>$states[($i-1)%3],'amountMinor'=>750000+$i*12500,'currency'=>'RUB','version'=>3];
    return $out;
}
$route=AEN_SCALAR_ROUTE;
$routes=['catalog'=>['GET',['status','page','limit']], 'detail'=>['GET',['id']], 'quote'=>['POST',[]], 'account'=>['GET',['action']], 'revision'=>['PATCH',[]], 'upload'=>['POST',[]], 'limited'=>['GET',[]], 'legacy'=>['GET',[]]];
if (!isset($routes[$route])) fail(404,'NOT_FOUND','Маршрут не найден');
[$allowed,$params]=$routes[$route];
if (($_SERVER['REQUEST_METHOD']??'')!==$allowed) { header('Allow: '.$allowed); fail(405,'METHOD_NOT_ALLOWED','Для этого маршрута нужен '.$allowed); }
foreach(array_keys($_GET) as $key) if(!in_array($key,$params,true)) fail(422,'UNKNOWN_QUERY','Неизвестный параметр',[(string)$key=>'Этот параметр не поддерживается']);
if (isset($_SERVER['HTTP_ORIGIN'])) {
    $origin=parse_url($_SERVER['HTTP_ORIGIN']);
    $originHost=strtolower(($origin['host']??'').(isset($origin['port'])?':'.$origin['port']:''));
    // Only server-owned scheme metadata is used; never trust forwarded headers from a client.
    $https=$_SERVER['HTTPS']??'';
    $scheme=(($https!==''&&strtolower((string)$https)!=='off')||($_SERVER['REQUEST_SCHEME']??'')==='https')?'https':'http';
    if (($origin['scheme']??'')!==$scheme||$originHost!==strtolower($_SERVER['HTTP_HOST']??'')) fail(403,'ORIGIN_DENIED','Для браузера разрешён только тот же origin');
}
switch($route) {
case 'catalog':
    $state=query('status','all');
    if (!in_array($state,['all','new','in_progress','done'],true)) fail(422,'INVALID_QUERY','Неизвестный статус',['status'=>'all, new, in_progress или done']);
    $page=integerQuery('page',1,1,100);$limit=integerQuery('limit',4,1,6);
    $rows=array_values(array_filter(tickets(),fn($x)=>$state==='all'||$x['status']===$state));
    reply(200,['data'=>array_slice($rows,($page-1)*$limit,$limit),'meta'=>['page'=>$page,'limit'=>$limit,'total'=>count($rows),'pages'=>(int)ceil(count($rows)/$limit)]]);
case 'detail':
    $id=integerQuery('id',1,1,999);
    foreach(tickets() as $ticket) if($ticket['id']===$id) reply(200,['data'=>$ticket]);
    fail(404,'TICKET_NOT_FOUND','Учебная заявка не найдена');
case 'quote':
    $v=jsonBody();$errors=[];
    foreach(array_keys($v) as $k) if(!in_array($k,['customer','items'],true)) $errors[$k]='Неизвестное поле';
    $c=$v['customer']??null;
    if(!is_array($c)||array_is_list($c)) $errors['customer']='Нужен объект клиента';
    else {
        foreach(array_keys($c) as $k) if(!in_array($k,['name','email'],true))$errors['customer.'.$k]='Неизвестное поле';
        if(!is_string($c['name']??null)||!preg_match('/^.{2,60}$/us',$c['name']))$errors['customer.name']='От 2 до 60 символов';
        if(!is_string($c['email']??null)||strlen($c['email'])>120||!filter_var($c['email'],FILTER_VALIDATE_EMAIL))$errors['customer.email']='Нужен корректный email';
    }
    $items=$v['items']??null;$total=0;
    if(!is_array($items)||!array_is_list($items)||count($items)<1||count($items)>5)$errors['items']='От 1 до 5 позиций';
    else foreach($items as $i=>$item){
        if(!is_array($item)||array_is_list($item)){$errors["items.$i"]='Нужен объект';continue;}
        foreach(array_keys($item) as $k)if(!in_array($k,['title','quantity','priceMinor'],true))$errors["items.$i.$k"]='Неизвестное поле';
        if(!is_string($item['title']??null)||!preg_match('/^.{1,70}$/us',$item['title']))$errors["items.$i.title"]='От 1 до 70 символов';
        if(!is_int($item['quantity']??null)||$item['quantity']<1||$item['quantity']>20)$errors["items.$i.quantity"]='Целое число от 1 до 20';
        if(!is_int($item['priceMinor']??null)||$item['priceMinor']<0||$item['priceMinor']>1000000)$errors["items.$i.priceMinor"]='От 0 до 1000000 копеек';
        if(is_int($item['quantity']??null)&&is_int($item['priceMinor']??null)&&$item['quantity']>=1&&$item['quantity']<=20&&$item['priceMinor']>=0&&$item['priceMinor']<=1000000)$total+=$item['quantity']*$item['priceMinor'];
    }
    if($errors)fail(422,'VALIDATION_FAILED','Исправьте отмеченные поля',$errors);
    reply(201,['data'=>['id'=>'DEMO-Q1001','totalMinor'=>$total,'currency'=>'RUB','items'=>count($items),'persisted'=>false],'message'=>'Предпросмотр расчёта готов. Заявка не сохранялась.']);
case 'account':
    $token=$_SERVER['HTTP_AUTHORIZATION']??$_SERVER['REDIRECT_HTTP_AUTHORIZATION']??'';
    if(!in_array($token,['Bearer demo-reader','Bearer demo-editor'],true)){header('WWW-Authenticate: Bearer realm="public-demo"');fail(401,'AUTH_REQUIRED','Используйте только публичный учебный токен');}
    $action=query('action','read');if(!in_array($action,['read','edit'],true))fail(422,'INVALID_QUERY','Действие должно быть read или edit');
    $role=$token==='Bearer demo-editor'?'editor':'reader';
    if($action==='edit'&&$role!=='editor')fail(403,'FORBIDDEN','Учебной роли reader недоступно редактирование');
    reply(200,['data'=>['name'=>'Учебный пользователь','role'=>$role,'action'=>$action],'message'=>'Публичный токен показывает контракт. Реальной защиты аккаунтов здесь нет.']);
case 'revision':
    $v=jsonBody();$errors=[];
    foreach(array_keys($v)as$k)if(!in_array($k,['expectedVersion','title'],true))$errors[$k]='Неизвестное поле';
    if(!is_int($v['expectedVersion']??null)||$v['expectedVersion']<1)$errors['expectedVersion']='Положительное целое число';
    if(!is_string($v['title']??null)||!preg_match('/^.{3,70}$/us',$v['title']))$errors['title']='От 3 до 70 символов';
    if($errors)fail(422,'VALIDATION_FAILED','Проверьте поля',$errors);
    if($v['expectedVersion']!==3)reply(409,['error'=>['code'=>'VERSION_CONFLICT','message'=>'Данные изменились: перечитайте карточку','fields'=>(object)[]],'currentVersion'=>3]);
    reply(200,['data'=>['id'=>1,'title'=>$v['title'],'version'=>4,'persisted'=>false],'message'=>'Показан результат проверки версии. Общая карточка не изменена.']);
case 'upload':
    if(strtolower(trim(explode(';',$_SERVER['CONTENT_TYPE']??'')[0]))!=='text/plain')fail(415,'UNSUPPORTED_MEDIA','Этот опыт принимает только text/plain');
    $raw=rawBody(4096);
    if($raw==='')fail(422,'EMPTY_FILE','Передайте непустой текст');
    if(!preg_match('//u',$raw))fail(422,'INVALID_ENCODING','Нужен UTF-8 текст');
    reply(200,['data'=>['bytes'=>strlen($raw),'lines'=>substr_count($raw,"\n")+(str_ends_with($raw,"\n")?0:1),'sha256'=>hash('sha256',$raw),'saved'=>false],'message'=>'Тело прочитано в памяти. Файл не сохранялся.']);
case 'limited':
    header('Retry-After: 3');fail(429,'DEMO_RATE_LIMIT','Учебный ответ 429. Это фиксированный сценарий, а не настоящий счётчик лимита.');
case 'legacy':
    reply(200,['data'=>['supported'=>true,'replacement'=>'catalog.php','deprecated'=>true],'message'=>'deprecated в контракте предупреждает о миграции, но сам по себе не выключает endpoint.']);
}
