<?php

namespace Shm\ShmRPC\ShmRPCCodeGen;

use Shm\ShmRPC\ShmRPCCodeGen\CodeGenPhp\CodeGenPhp;
use Shm\ShmTypes\StructureType;
use Shm\ShmUtils\ShmInit;
use Shm\ShmUtils\ShmTwig;

class ShmRPCCodeGen
{

    public static array $tsTypes = [];


    public static function php(array $schema)
    {
        $phpCode = CodeGenPhp::php($schema);
        echo $phpCode;
    }



    public static function html(array $schema, $json = false)
    {




        $requestsData = [];

        $linkRequest = [];

        $keysGraph = [];
        foreach ($schema  as $key => $field) {


            if ($_GET['method'] ?? null) {
                if ($key !== $_GET['method']) {
                    continue;
                }
            }



            $ignore = $field['ignore'] ?? false;
            if ($ignore) {

                continue;
            }



            $requestsData[$key] = (new ShmRPCRequestCode($field['type'], $field['args'] ?? null, $key, $field['formData'] ?? null))->initialize();

            $linkRequest[$key] = "export const " . $key . " = rpc." . $key . ";";
        }


        ksort(TSType::$tsTypes);
        ksort($requestsData);



        $allTypesKeys = array_keys(TSType::$tsTypes);

        $types = implode("\n", array_values(TSType::$tsTypes));

        $requestsAll =   array_values($requestsData);
        $linkRequest = array_values($linkRequest);



        $requests = [
            "import type { RpcResponse, " . implode(',', $allTypesKeys) . " } from './types';",


            "import { debounce, map } from 'lodash';",


        ];

        if (!isset($_GET['nextjs'])) {
            $requests[] =  "import { observable, toJS } from 'mobx';";
        }

        if (isset($_GET['rn'])) {

            $requests[] = "import { fromByteArray, toByteArray } from 'react-native-quick-base64';";
            $requests[] = "import { Buffer } from 'buffer';";
            $requests[] = "import AsyncStorage from '@react-native-async-storage/async-storage';";
        }


        $requests = [
            ...$requests,

            '
            /**
 * Параметр `extensions` в RPC-запросе (`rpcClient.call`, тело: `method`, `params`, `extensions`, …)
 * — это массив имён **связанных коллекций**, которые серверу нужно **догрузить** к основному ответу
 * метода, не включая эти сущности в `result` целиком.
 *
 * **Зачем:** в схеме (см. `BaseType::extensionsStructure`) у полей-ссылок (`ID` / `IDs`) есть
 * соответствующий документ в MongoDB. Без `extensions` в `result` остаются только идентификаторы;
 * с `extensions` сервер (`ExtensionsResolver` в schema) по этим id выбирает документы из кэша/БД
 * и возвращает их **отдельно** в поле `extensions` ответа: объект «имя коллекции → массив документов».
 * Так клиент выбирает, какой граф данных подтягивать, и не платит за лишние join’ы.
 *
 * **Синтаксис строк:** `коллекция` — подгрузить на первом уровне от корневого результата;
 * `a->b` и `a->b->c` — те же коллекции, но для **вложенных** уровней: сначала грузятся `a`, затем
 * по id из полученных документов — `b`, затем `c` (см. `oneSubLevelExtensions` / `twoSubLevelExtensions`
 * в `ExtensionsResolver`). Разрешённые значения для каждого метода задаются кодогеном из схемы.
 *
 * Перед `undefined` у конкретного вызова используется `rpcClient.setDefaultExtensions` (если задано).
 * Пустой массив — дополнительные коллекции не запрашивать.
 * 
 * Далее в коде можно получить данные по примеру ExtensionStore.citiesByID(X) или ExtensionStore.citiesByIDs([X, Y, Z]) и т.д.
 */
            export const rpc = {',
            ...$requestsAll,
            '};',
            ...$linkRequest,
        ];

        // Загружаем код rpcClient из отдельного файла

        if (isset($_GET['rn'])) {
            $rpcClientPath = __DIR__ . '/rpcClient_rn.ts';
        } else {
            $rpcClientPath = __DIR__ . '/rpcClient.ts';
        }


        $rpcClientCode = file_get_contents($rpcClientPath);
        $requests[] = $rpcClientCode;



        $requests[] = ShmRPCCodeGenExtensionsStore::tsClass();



        $requests[] = 'export const enumHelper = ' . json_encode(TSType::$enumHelperCode, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);


        $enumHelperGet[] = 'export const enumHelperGet = {';

        foreach (TSType::$enumHelperCode as $collection => $values) {
            $enumHelperGet[] = '  "' . $collection . '": {';
            foreach ($values as $key => $value) {
                $enumHelperGet[] = '    "' . $key . '": (key:string|undefined|null, defaultValue?:any)=>{ return enumHelperValue(enumHelper.' . $collection . '["' . $key . '"], key, defaultValue);   },';
            }
            $enumHelperGet[] = '  },';
        }
        $enumHelperGet[] = '};';

        $requests[] = implode("\n", $enumHelperGet);


        $requests[] = "export const enumHelperValue = (
   values: { [key: string]: string },
   key: string | undefined | null,
   defaultValue: any
) => {
   if (!key) return defaultValue;
   return values[key!] ?? defaultValue;
}";


        $requests = implode("\n", $requests);

        $files = [];

        if (($_SERVER['SERVER_NAME'] ?? null) == "localhost") {
            /**
             * Если есть ShmInit::$rootDir . '/schema_history/' то удаляем эту директорию
             * это удаление старых лог файлов, которые не показали свою эффективность
             * Удаляем включая саму директорию schema_history
             */

            $dir =  ShmInit::$rootDir . '/schema_history';
            if (is_dir($dir)) {
                rmdir($dir);
            }




            /*
            $REQUEST_URI = $_SERVER['REQUEST_URI'];

            //Убарем GET параметры
            $REQUEST_URI = explode('?', $REQUEST_URI)[0];
            //Все / заменяем на _
            $REQUEST_URI = str_replace('/', '_', $REQUEST_URI);


            $dir =  ShmInit::$rootDir . '/schema_history/' . $REQUEST_URI;

*/
            /*

            if (!is_dir($dir)) {
                mkdir($dir, 0777, true);
            }

            $dirTypes = $dir . '/types';
            if (!is_dir($dirTypes)) {
                mkdir($dirTypes, 0777, true);
            }

            $dirRequests = $dir . '/requests';
            if (!is_dir($dirRequests)) {
                mkdir($dirRequests, 0777, true);
            }


            foreach (TSType::$tsTypes as $key => $type) {

                file_put_contents($dirTypes . '/' . $key . '.tmp', $type);
            }

            foreach ($requestsData as $key => $request) {
                file_put_contents($dirRequests . '/' . $key . '.tmp', $request);
            }
                */
        }

        $types = "export interface RpcError {
  type: 'UNAUTHORIZED' | 'VALIDATION_ERROR' | 'NOT_FOUND' | 'INTERNAL_ERROR' | 'FORBIDDEN' | 'RATE_LIMITED' | string;
  message: string;
  code?: number;
}

export interface RpcResponse<T = unknown, Extensions = unknown> {
  datetime: number;
  success: boolean;
  result: T | null;
  extensions: Extensions | null;
  error: RpcError | null;
}" . "\n\n" . $types;



        if ($json) {

            echo json_encode([
                'types' => $types,
                'requests' => $requests
            ]);
            exit;
        }


        header('Content-Type: text/html; charset=utf-8');

        // Escape backticks for JavaScript template literals
        $typesEscaped = str_replace('`', '\`', $types);
        $requestsEscaped = str_replace('`', '\`', $requests);

        echo ShmTwig::render('@shm/codegen', [
            'types' => $typesEscaped,
            'requests' => $requestsEscaped,
        ]);

        exit;
    }


    public static function openapi(array $schema): never
    {
        JsonSchemaBuilder::reset();

        JsonSchemaBuilder::addComponent('RpcError', [
            'type' => 'object',
            'required' => ['type', 'message'],
            'properties' => [
                'type' => ['type' => 'string'],
                'message' => ['type' => 'string'],
                'code' => ['type' => 'integer', 'nullable' => true],
            ],
        ]);

        $jsonRequestOneOf = [];
        $formDataRequestOneOf = [];
        $responseOneOf = [];
        $jsonDiscriminatorMapping = [];
        $formDataDiscriminatorMapping = [];

        foreach ($schema as $key => $field) {
            if (($_GET['method'] ?? null) && $key !== $_GET['method']) {
                continue;
            }

            if ($field['ignore'] ?? false) {
                continue;
            }

            $requestCode = new ShmRPCRequestCode(
                $field['type'],
                $field['args'] ?? null,
                $key,
                $field['formData'] ?? null
            );

            $requestSchemaName = 'RpcRequest_' . $key;
            $responseSchemaName = 'RpcResponse_' . $key;
            $isFormData = $field['formData'] ?? false;

            JsonSchemaBuilder::addComponent(
                $requestSchemaName,
                $isFormData
                    ? self::buildFormDataRequestSchema($key, $field)
                    : self::buildRpcRequestSchema($key, $field, $requestCode)
            );
            JsonSchemaBuilder::addComponent(
                $responseSchemaName,
                self::buildRpcResponseSchema($field, $requestCode)
            );

            $requestRef = JsonSchemaBuilder::ref($requestSchemaName);
            if ($isFormData) {
                $formDataRequestOneOf[] = $requestRef;
                $formDataDiscriminatorMapping[$key] = '#/components/schemas/' . $requestSchemaName;
            } else {
                $jsonRequestOneOf[] = $requestRef;
                $jsonDiscriminatorMapping[$key] = '#/components/schemas/' . $requestSchemaName;
            }

            $responseOneOf[] = JsonSchemaBuilder::ref($responseSchemaName);
        }

        $path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';

        $requestContent = [];
        if ($jsonRequestOneOf !== []) {
            $requestContent['application/json'] = [
                'schema' => [
                    'oneOf' => $jsonRequestOneOf,
                    'discriminator' => [
                        'propertyName' => 'method',
                        'mapping' => $jsonDiscriminatorMapping,
                    ],
                ],
            ];
        }
        if ($formDataRequestOneOf !== []) {
            $requestContent['multipart/form-data'] = [
                'schema' => [
                    'oneOf' => $formDataRequestOneOf,
                    'discriminator' => [
                        'propertyName' => 'method',
                        'mapping' => $formDataDiscriminatorMapping,
                    ],
                ],
            ];
        }

        $spec = [
            'openapi' => '3.0.3',
            'info' => [
                'title' => ($_SERVER['SERVER_NAME'] ?? null) ?: 'ShmRPC API',
                'version' => '1.0.0',
            ],
            'servers' => [
                ['url' => ''],
            ],
            'paths' => [
                $path => [
                    'post' => [
                        'summary' => 'ShmRPC',
                        'operationId' => 'rpc',
                        'requestBody' => [
                            'required' => true,
                            'content' => $requestContent,
                        ],
                        'responses' => [
                            '200' => [
                                'description' => 'RPC response',
                                'content' => [
                                    'application/json' => [
                                        'schema' => [
                                            'oneOf' => $responseOneOf,
                                        ],
                                    ],
                                ],
                            ],
                        ],
                    ],
                ],
            ],
            'components' => [
                'schemas' => JsonSchemaBuilder::getComponents(),
            ],
        ];

        header('Content-Type: application/json; charset=utf-8');
        echo json_encode($spec, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
        exit;
    }

    /**
     * @param array<string, mixed> $field
     * @return array<string, mixed>
     */
    private static function buildRpcRequestSchema(string $key, array $field, ShmRPCRequestCode $requestCode): array
    {
        $properties = [
            'method' => ['const' => $key],
            'token' => ['type' => 'string', 'nullable' => true],
            'context' => ['type' => 'string', 'nullable' => true],
        ];
        $required = ['method'];

        $args = $field['args'] ?? null;
        if ($args) {
            $properties['params'] = JsonSchemaBuilder::forType($args->keyIfNot($key), true);
        } else {
            $properties['params'] = ['type' => 'object', 'nullable' => true];
        }

        [$extensionsStructure, $extensionsCollection] = $requestCode->getExtensions();
        if (count($extensionsCollection) > 0) {
            $properties['extensions'] = [
                'type' => 'array',
                'items' => [
                    'type' => 'string',
                    'enum' => $extensionsCollection,
                ],
                'nullable' => true,
            ];
        }

        return [
            'type' => 'object',
            'required' => $required,
            'properties' => $properties,
        ];
    }

    /**
     * @param array<string, mixed> $field
     * @return array<string, mixed>
     */
    private static function buildFormDataRequestSchema(string $key, array $field): array
    {
        $properties = [
            'method' => ['type' => 'string', 'const' => $key],
        ];
        $required = ['method'];

        $args = $field['args'] ?? null;
        if ($args && isset($args->items)) {
            foreach ($args->items as $argKey => $argItem) {
                if ($argItem->hide || !$argItem->editable) {
                    continue;
                }
                $properties[$argKey] = ['type' => 'string', 'format' => 'binary'];
            }
        }

        return [
            'type' => 'object',
            'required' => $required,
            'properties' => $properties,
        ];
    }

    /**
     * @param array<string, mixed> $field
     * @return array<string, mixed>
     */
    private static function buildRpcResponseSchema(array $field, ShmRPCRequestCode $requestCode): array
    {
        $resultSchema = JsonSchemaBuilder::forType($field['type']);
        $resultSchema['nullable'] = true;

        $properties = [
            'success' => ['type' => 'boolean'],
            'result' => $resultSchema,
            'error' => [
                'nullable' => true,
                'allOf' => [JsonSchemaBuilder::ref('RpcError')],
            ],
            'datetime' => ['type' => 'integer'],
        ];

        [$extensionsStructure] = $requestCode->getExtensions();
        if (count($extensionsStructure) > 0) {
            $extensionProperties = [];
            foreach ($extensionsStructure as $extKey => $extType) {
                $itemSchema = JsonSchemaBuilder::forType($extType);
                $extensionProperties[$extKey] = [
                    'type' => 'array',
                    'items' => $itemSchema,
                    'nullable' => true,
                ];
            }
            $properties['extensions'] = [
                'type' => 'object',
                'properties' => $extensionProperties,
                'nullable' => true,
            ];
        } else {
            $properties['extensions'] = ['nullable' => true];
        }

        return [
            'type' => 'object',
            'required' => ['success', 'datetime'],
            'properties' => $properties,
        ];
    }


    public static function md(array $schema): never
    {
        MarkdownTypeRenderer::reset();

        $methodSections = [];
        $toc = [];

        foreach ($schema as $key => $field) {
            if (($_GET['method'] ?? null) && $key !== $_GET['method']) {
                continue;
            }

            if ($field['ignore'] ?? false) {
                continue;
            }

            $methodSections[] = self::buildMethodMarkdown($key, $field);
            $toc[] = '- [`' . $key . '`](#' . MarkdownTypeRenderer::anchor($key) . ')';
        }

        $path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';

        $md = self::buildProtocolSection($path, $toc);
        $md .= "## Методы\n\n" . implode("\n\n---\n\n", $methodSections);
        $md .= MarkdownTypeRenderer::getTypesSection();

        header('Content-Type: text/markdown; charset=utf-8');
        echo $md;
        exit;
    }

    /**
     * @param list<string> $toc
     */
    private static function buildProtocolSection(string $path, array $toc): string
    {
        $tocBlock = $toc !== [] ? implode("\n", $toc) : '_Нет методов_';

        return <<<MD
# Документация API

## Протокол

Все вызовы выполняются методом **POST** на эндпоинт:

```
POST {$path}
```

### Тело запроса (JSON)

| Поле | Тип | Обязательный | Описание |
|------|-----|--------------|----------|
| `method` | string | да | Имя RPC-метода |
| `params` | object | нет | Параметры метода (см. описание каждого метода) |
| `token` | string | нет | Токен авторизации |

### Тело ответа

| Поле | Тип | Описание |
|------|-----|----------|
| `success` | boolean | Успешность выполнения |
| `result` | mixed \\| null | Результат метода |
| `error` | [RpcError](#rpcerror) \\| null | Ошибка (если `success: false`) |
| `datetime` | integer | Unix timestamp ответа |

### Объект ошибки (`error`) {#rpcerror}

| Поле | Тип | Обязательный | Описание |
|------|-----|--------------|----------|
| `type` | string | да | Тип ошибки (см. таблицу ниже) |
| `message` | string | да | Текст ошибки |
| `code` | integer | нет | Числовой HTTP-код ошибки |

### Значения `error.type`

| type | HTTP | Описание |
|------|------|----------|
| `UNAUTHORIZED` | 401 | Не авторизован |
| `VALIDATION_ERROR` | 400 | Ошибка валидации |
| `NOT_FOUND` | 404 | Метод или ресурс не найден |
| `FORBIDDEN` | 403 | Доступ запрещён |
| `RATE_LIMITED` | 429 | Превышен лимит запросов |
| `INTERNAL_ERROR` | 500 | Внутренняя ошибка сервера |

## Оглавление

{$tocBlock}


MD;
    }

    /**
     * @param array<string, mixed> $field
     */
    private static function buildMethodMarkdown(string $key, array $field): string
    {
        $isFormData = $field['formData'] ?? false;
        $resultType = $field['type'];
        $args = $field['args'] ?? null;

        $title = $resultType->title ?? null;
        $titleBlock = $title ? "\n\n> " . $title : '';

        $contentType = $isFormData
            ? '`multipart/form-data`'
            : '`application/json`';

        $md = '### `' . $key . '` {#' . MarkdownTypeRenderer::anchor($key) . '}' . $titleBlock . "\n\n";
        $md .= '**Content-Type:** ' . $contentType . "\n\n";

        if ($isFormData) {
            $md .= "#### Параметры (form fields)\n\n";
            if ($args && isset($args->items)) {
                $rows = ["| Поле | Тип | Обязательный | Описание |", "|------|-----|--------------|----------|"];
                foreach ($args->items as $argKey => $argItem) {
                    if ($argItem->hide || !$argItem->editable) {
                        continue;
                    }
                    $required = $argItem->nullable ? 'нет' : 'да';
                    $desc = $argItem->title ?: ($argItem->description ?: '—');
                    $rows[] = '| `' . $argKey . '` | file (binary) | ' . $required . ' | ' . $desc . ' |';
                }
                $md .= implode("\n", $rows) . "\n\n";
            } else {
                $md .= "_Только поле `method`_\n\n";
            }
        } else {
            $md .= "#### Параметры (`params`)\n\n";
            if ($args) {
                $paramsType = $args->keyIfNot($key);
                $md .= 'Тип: `' . MarkdownTypeRenderer::typeNamePlain($paramsType, true) . "`\n\n";
                if ($paramsType instanceof StructureType) {
                    $md .= MarkdownTypeRenderer::structureTable($paramsType, true) . "\n\n";
                }
            } else {
                $md .= "_Параметры не требуются_\n\n";
            }
        }

        $resultLabel = MarkdownTypeRenderer::typeNamePlain($resultType);
        $md .= "#### Ответ (`result`)\n\n";
        $md .= 'Тип: `' . $resultLabel . ' | null`' . "\n\n";
        if ($resultType instanceof StructureType) {
            $md .= MarkdownTypeRenderer::structureTable($resultType, false) . "\n\n";
        }

        $requestExample = self::buildRequestExample($key, $field);
        $responseExample = self::buildResponseExample($field);

        $md .= "#### Пример запроса\n\n```json\n" . json_encode($requestExample, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n```\n\n";
        $md .= "#### Пример ответа\n\n```json\n" . json_encode($responseExample, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n```\n";

        return $md;
    }

    /**
     * @param array<string, mixed> $field
     * @return array<string, mixed>
     */
    private static function buildRequestExample(string $key, array $field): array
    {
        if ($field['formData'] ?? false) {
            return ['method' => $key, '...' => 'multipart form fields'];
        }

        $example = ['method' => $key];

        $args = $field['args'] ?? null;
        if ($args) {
            $example['params'] = MarkdownTypeRenderer::exampleValue($args->keyIfNot($key), true, true);
        }

        return $example;
    }

    /**
     * @param array<string, mixed> $field
     * @return array<string, mixed>
     */
    private static function buildResponseExample(array $field): array
    {
        $result = MarkdownTypeRenderer::exampleValue($field['type'], false);

        return [
            'success' => true,
            'result' => $result,
            'error' => null,
            'datetime' => 1710000000,
        ];
    }
}