<?php

/**
 * Enforce GET-only access for grid/data endpoints with a JSON 405 response.
 */
function gridRequireGetRequest(): void
{
    if (requestInput()->isMethod('GET')) {
        return;
    }

    http_response_code(405);
    \MintyPHP\Router::json(['error' => 'method_not_allowed']);
}

/**
 * Load a local filter schema file and parse request query values.
 *
 * @return array<string,mixed>
 */
function gridParseFiltersFromSchemaFile(string $schemaFile, ?array $query = null): array
{
    $schemaFile = trim($schemaFile);
    if ($schemaFile === '' || !is_file($schemaFile)) {
        return [];
    }

    $schema = require $schemaFile;
    if (!is_array($schema)) {
        return [];
    }

    return gridParseFilters($query ?? requestInput()->queryAll(), gridSchemaQuery($schema));
}

/**
 * Emit a standardized grid JSON payload.
 */
function gridJsonDataResult(array $rows, int $total): void
{
    \MintyPHP\Router::json([
        'data' => array_values($rows),
        'total' => max(0, $total),
    ]);
}

/**
 * Resolve a badge variant from a status/outcome map.
 *
 * @param array<string,string> $variantMap
 */
function gridResolveBadgeVariant(
    string $value,
    array $variantMap,
    string $default = 'neutral',
    bool $normalizeLowercase = true
): string {
    $key = trim($value);
    if ($normalizeLowercase) {
        $key = strtolower($key);
    }

    $variant = (string) ($variantMap[$key] ?? $default);
    return trim($variant) !== '' ? $variant : $default;
}

/**
 * Build a csv_strings sanitizer for string-backed enums.
 *
 * @param class-string<\BackedEnum> $enumClass
 */
function gridEnumSanitizer(string $enumClass, bool $lowercase = true): callable
{
    return static function (string $value) use ($enumClass, $lowercase): string {
        if (!enum_exists($enumClass) || !is_subclass_of($enumClass, \BackedEnum::class)) {
            return '';
        }

        $value = trim($value);
        if ($value === '') {
            return '';
        }
        if ($lowercase) {
            $value = strtolower($value);
        }

        foreach ($enumClass::cases() as $case) {
            $candidate = (string) $case->value;
            $normalizedCandidate = $lowercase ? strtolower($candidate) : $candidate;
            if ($normalizedCandidate === $value) {
                return $candidate;
            }
        }

        return '';
    };
}

/**
 * Identity helper for filter schema declarations.
 *
 * @param array<string,mixed>|array<int,mixed> $schema
 * @return array<string,mixed>|array<int,mixed>
 */
function gridFilterSchema(array $schema = []): array
{
    return $schema;
}

/**
 * Render a value as safe inline JavaScript JSON.
 */
function gridJsonForJs(mixed $value): void
{
    $json = json_encode(
        $value,
        JSON_UNESCAPED_UNICODE
        | JSON_UNESCAPED_SLASHES
        | JSON_HEX_TAG
        | JSON_HEX_AMP
        | JSON_HEX_APOS
        | JSON_HEX_QUOT
    );

    if (!is_string($json)) {
        $json = 'null';
    }

    echo $json;
}

/**
 * Extract query parsing rules from a list filter schema.
 *
 * Supports two shapes:
 * - ['query' => [...], 'toolbar' => [...]]
 * - [['key' => '...', 'query' => [...]], ...]
 *
 * @return array<string,mixed>
 */
function gridSchemaQuery(array $schema): array
{
    if (isset($schema['query']) && is_array($schema['query'])) {
        return $schema['query'];
    }

    $rules = [];
    foreach ($schema as $entry) {
        if (!is_array($entry)) {
            continue;
        }
        $key = trim((string) ($entry['key'] ?? ''));
        if ($key === '') {
            continue;
        }
        if (is_array($entry['query'] ?? null)) {
            $rules[$key] = $entry['query'];
        }
    }

    return $rules;
}

/**
 * Extract toolbar field schema from a list filter schema.
 *
 * @return array<int,array<string,mixed>>
 */
function gridSchemaToolbar(array $schema): array
{
    if (isset($schema['toolbar']) && is_array($schema['toolbar'])) {
        return array_values(array_filter($schema['toolbar'], 'is_array'));
    }

    $toolbar = [];
    foreach ($schema as $entry) {
        if (!is_array($entry) || (($entry['render'] ?? true) === false)) {
            continue;
        }
        if (!isset($entry['type']) || !isset($entry['key'])) {
            continue;
        }
        $toolbar[] = $entry;
    }

    return $toolbar;
}

/**
 * Split toolbar schema into search and drawer sections.
 *
 * @param array<int,array<string,mixed>> $toolbarSchema
 * @param string[] $searchKeys
 * @return array{search: array<int,array<string,mixed>>, drawer: array<int,array<string,mixed>>}
 */
function gridSplitToolbarSchema(array $toolbarSchema, array $searchKeys = []): array
{
    $searchKeyMap = [];
    foreach ($searchKeys as $searchKey) {
        $key = trim((string) $searchKey);
        if ($key !== '') {
            $searchKeyMap[$key] = true;
        }
    }

    $search = [];
    $drawer = [];
    foreach ($toolbarSchema as $field) {
        $key = trim((string) ($field['key'] ?? ''));
        $isSearch = (bool) ($field['search'] ?? false) || ($key !== '' && isset($searchKeyMap[$key]));
        if ($isSearch) {
            $search[] = $field;
            continue;
        }
        $drawer[] = $field;
    }

    return ['search' => $search, 'drawer' => $drawer];
}

/**
 * Index toolbar schema entries by filter key.
 *
 * @param array<int,array<string,mixed>> $toolbarSchema
 * @return array<string,array<string,mixed>>
 */
function gridSchemaByKey(array $toolbarSchema): array
{
    $schemaByKey = [];
    foreach ($toolbarSchema as $field) {
        $key = trim((string) ($field['key'] ?? ''));
        if ($key === '') {
            continue;
        }
        $schemaByKey[$key] = $field;
    }

    return $schemaByKey;
}

/**
 * Build toolbar state values from parsed filter state and toolbar schema defaults.
 *
 * @param array<int,array<string,mixed>> $toolbarSchema
 * @param array<string,mixed> $filterState
 * @return array<string,mixed>
 */
function gridBuildToolbarFilterState(array $toolbarSchema, array $filterState): array
{
    $state = [];
    foreach ($toolbarSchema as $field) {

        $key = trim((string) ($field['key'] ?? ''));
        if ($key === '') {
            continue;
        }

        $type = strtolower(trim((string) ($field['type'] ?? '')));
        $default = array_key_exists('default', $field) ? $field['default'] : '';
        $value = array_key_exists($key, $filterState) ? $filterState[$key] : $default;

        if ($type === 'multi_csv') {
            $state[$key] = array_values(array_unique(gridNormalizeLabelList($value, ',')));
            continue;
        }

        if (is_array($value)) {
            $value = (string) (reset($value) ?: '');
        }

        $state[$key] = trim((string) $value);
    }

    return $state;
}

/**
 * Build normalized list-filter context used by index actions/templates.
 *
 * @param array<string,mixed> $filterSchema
 * @param array<string,mixed> $options
 * @return array{
 *   filterState: array<string,mixed>,
 *   toolbarFilterSchema: array<int,array<string,mixed>>,
 *   toolbarFilterState: array<string,mixed>,
 *   searchToolbarFilterSchema: array<int,array<string,mixed>>,
 *   drawerToolbarFilterSchema: array<int,array<string,mixed>>,
 *   schemaByKey: array<string,array<string,mixed>>,
 *   toolbarOptionSets: array<string,mixed>,
 *   clientFilterSchema: array<int,array<string,mixed>>,
 *   searchConfig: ?array<string,mixed>
 * }
 */
function gridBuildListFilterContext(array $filterSchema, array $options = []): array
{
    $query = is_array($options['query'] ?? null) ? $options['query'] : requestInput()->queryAll();
    $querySchema = is_array($options['query_schema'] ?? null)
        ? $options['query_schema']
        : gridSchemaQuery($filterSchema);

    $filterState = is_array($options['filter_state'] ?? null)
        ? $options['filter_state']
        : gridParseFilters($query, $querySchema);

    $toolbarFilterSchema = gridSchemaToolbar($filterSchema);
    $toolbarFilterState = gridBuildToolbarFilterState($toolbarFilterSchema, $filterState);
    $toolbarStateOverrides = is_array($options['toolbar_state_overrides'] ?? null)
        ? $options['toolbar_state_overrides']
        : [];
    if ($toolbarStateOverrides) {
        $toolbarFilterState = array_replace($toolbarFilterState, $toolbarStateOverrides);
    }

    $searchKeys = is_array($options['search_keys'] ?? null) ? $options['search_keys'] : ['search'];
    $toolbarSchemaSections = gridSplitToolbarSchema($toolbarFilterSchema, $searchKeys);
    $toolbarOptionSets = is_array($options['toolbar_option_sets'] ?? null) ? $options['toolbar_option_sets'] : [];

    return [
        'filterState' => $filterState,
        'toolbarFilterSchema' => $toolbarFilterSchema,
        'toolbarFilterState' => $toolbarFilterState,
        'searchToolbarFilterSchema' => $toolbarSchemaSections['search'],
        'drawerToolbarFilterSchema' => $toolbarSchemaSections['drawer'],
        'schemaByKey' => gridSchemaByKey($toolbarFilterSchema),
        'toolbarOptionSets' => $toolbarOptionSets,
        'clientFilterSchema' => gridSchemaClientFilters($filterSchema),
        'searchConfig' => gridSchemaClientSearch($filterSchema),
    ];
}

/**
 * Build id => label maps from option set items.
 *
 * @param array<int,array<string,mixed>> $items
 * @return array<string,string>
 */
function gridOptionMapFromItems(array $items): array
{
    $map = [];
    foreach ($items as $item) {
        $id = trim((string) ($item['id'] ?? ''));
        if ($id === '') {
            continue;
        }
        $map[$id] = (string) ($item['description'] ?? $id);
    }

    return $map;
}

/**
 * Build id => label maps from schema allowed declarations.
 *
 * @param array<string,mixed> $field
 * @return array<string,string>
 */
function gridOptionMapFromAllowed(array $field): array
{
    $map = [];
    foreach ((array) ($field['allowed'] ?? []) as $item) {
        $id = trim((string) ($item['id'] ?? ''));
        if ($id === '') {
            continue;
        }
        $label = (string) ($item['description'] ?? $id);
        $translate = !array_key_exists('translate', $item) || (bool) $item['translate'];
        $map[$id] = $translate ? t($label) : $label;
    }

    return $map;
}

/**
 * Build client filter schema consumed by JS helper gridFiltersFromSchema().
 *
 * @return array<int,array<string,mixed>>
 */
function gridSchemaClientFilters(array $schema): array
{
    if (isset($schema['client_filters']) && is_array($schema['client_filters'])) {
        return array_values(array_filter($schema['client_filters'], 'is_array'));
    }

    $filters = [];
    foreach (gridSchemaToolbar($schema) as $field) {
        $type = strtolower(trim((string) ($field['type'] ?? '')));
        if (!in_array($type, ['text', 'date', 'number', 'select', 'multi_csv', 'hidden'], true)) {
            continue;
        }
        if ((bool) ($field['search'] ?? false)) {
            continue;
        }
        if (($field['bind'] ?? true) === false) {
            continue;
        }

        $key = trim((string) ($field['key'] ?? ''));
        if ($key === '') {
            continue;
        }
        $inputId = trim((string) ($field['input_id'] ?? ''));
        if ($inputId === '') {
            $inputId = $key . '-filter';
        }

        $filter = [
            'type' => $type,
            'input' => '#' . $inputId,
            'param' => (string) ($field['param'] ?? $key),
        ];

        if (array_key_exists('default', $field)) {
            $filter['default'] = $field['default'];
        }
        if (isset($field['event'])) {
            $filter['event'] = (string) $field['event'];
        }
        if (isset($field['normalize'])) {
            $filter['normalize'] = (string) $field['normalize'];
        }

        $filters[] = $filter;
    }

    return $filters;
}

/**
 * Build client search config consumed by createServerGrid().
 *
 * @return array<string,mixed>|null
 */
function gridSchemaClientSearch(array $schema): ?array
{
    foreach (gridSchemaToolbar($schema) as $field) {
        if (!(bool) ($field['search'] ?? false)) {
            continue;
        }
        $key = trim((string) ($field['key'] ?? ''));
        if ($key === '') {
            continue;
        }
        $inputId = trim((string) ($field['input_id'] ?? ''));
        if ($inputId === '') {
            $inputId = $key . '-filter';
        }

        return [
            'input' => '#' . $inputId,
            'param' => (string) ($field['param'] ?? $key),
            'debounce' => (int) ($field['debounce'] ?? 250),
        ];
    }

    return null;
}

/**
 * Parse filter/query values by a compact schema.
 *
 * Supported types:
 * - int
 * - number
 * - bool
 * - string
 * - enum
 * - uuid
 * - date (Y-m-d)
 * - csv_ids
 * - csv_uuid
 * - csv_strings
 * - order
 * - dir
 *
 * @return array<string, mixed>
 */
function gridParseFilters(array $query, array $schema): array
{
    $parsed = [];

    foreach ($schema as $key => $rule) {
        if (!is_array($rule)) {
            $rule = ['type' => (string) $rule];
        }

        $type = strtolower(trim((string) ($rule['type'] ?? 'string')));
        $source = (string) ($rule['source'] ?? $key);

        if ($source === '') {
            $source = (string) $key;
        }

        if ($type === 'int') {
            $default = (int) ($rule['default'] ?? 0);
            $min = isset($rule['min']) ? (int) $rule['min'] : null;
            $max = isset($rule['max']) ? (int) $rule['max'] : null;
            $value = (int) ($query[$source] ?? $default);
            if ($min !== null && $value < $min) {
                $value = $default;
            }
            if ($max !== null && $value > $max) {
                $value = $max;
            }
            $parsed[$key] = $value;
            continue;
        }

        if ($type === 'number') {
            $default = (float) ($rule['default'] ?? 0);
            $min = isset($rule['min']) ? (float) $rule['min'] : null;
            $max = isset($rule['max']) ? (float) $rule['max'] : null;
            $parsed[$key] = gridQueryNumber($query, $source, $default, $min, $max);
            continue;
        }

        if ($type === 'bool') {
            $default = (bool) ($rule['default'] ?? false);
            $parsed[$key] = gridQueryBool($query, $source, $default);
            continue;
        }

        if ($type === 'string') {
            $parsed[$key] = gridQueryString($query, $source, (string) ($rule['default'] ?? ''));
            continue;
        }

        if ($type === 'enum') {
            $parsed[$key] = gridQueryEnum(
                $query,
                $source,
                (array) ($rule['allowed'] ?? []),
                (string) ($rule['default'] ?? ''),
                (bool) ($rule['lowercase'] ?? false)
            );
            continue;
        }

        if ($type === 'date') {
            $parsed[$key] = gridQueryDate($query, $source, (string) ($rule['default'] ?? ''));
            continue;
        }

        if ($type === 'uuid') {
            $parsed[$key] = gridQueryUuid($query, $source, (string) ($rule['default'] ?? ''));
            continue;
        }

        if ($type === 'csv_ids') {
            $ids = gridQueryCsvIds($query, $source, (int) ($rule['max'] ?? 200));
            $parsed[$key] = (($rule['return'] ?? 'csv') === 'array')
                ? $ids
                : implode(',', array_map('strval', $ids));
            continue;
        }

        if ($type === 'csv_uuid') {
            $items = gridQueryCsvUuids($query, $source, (int) ($rule['max'] ?? 200));
            $parsed[$key] = (($rule['return'] ?? 'csv') === 'array')
                ? $items
                : implode(',', $items);
            continue;
        }

        if ($type === 'csv_strings') {
            $items = gridQueryCsvStrings(
                $query,
                $source,
                (int) ($rule['max'] ?? 200),
                $rule['sanitizer'] ?? null
            );
            $parsed[$key] = (($rule['return'] ?? 'csv') === 'array')
                ? $items
                : implode(',', $items);
            continue;
        }

        if ($type === 'order') {
            $allowed = array_values(array_filter(array_map(
                static fn (mixed $item): string => trim((string) $item),
                (array) ($rule['allowed'] ?? [])
            ), static fn (string $item): bool => $item !== ''));
            $default = (string) ($rule['default'] ?? ($allowed[0] ?? ''));
            $parsed[$key] = gridQueryEnum($query, $source, $allowed, $default);
            continue;
        }

        if ($type === 'dir') {
            $parsed[$key] = gridQueryEnum($query, $source, ['asc', 'desc'], (string) ($rule['default'] ?? 'asc'), true);
            continue;
        }

        $parsed[$key] = gridQueryString($query, $source, (string) ($rule['default'] ?? ''));
    }

    return $parsed;
}

/**
 * Read a trimmed string from query params.
 */
function gridQueryString(array $query, string $key, string $default = ''): string
{
    return trim((string) ($query[$key] ?? $default));
}

/**
 * Read and normalize a numeric query value.
 */
function gridQueryNumber(array $query, string $key, float $default = 0, ?float $min = null, ?float $max = null): float
{
    $value = trim((string) ($query[$key] ?? ''));
    if ($value === '' || !is_numeric($value)) {
        $number = $default;
    } else {
        $number = (float) $value;
    }

    if ($min !== null && $number < $min) {
        $number = $min;
    }
    if ($max !== null && $number > $max) {
        $number = $max;
    }

    return $number;
}

/**
 * Read and normalize a bool query value.
 */
function gridQueryBool(array $query, string $key, bool $default = false): bool
{
    if (!array_key_exists($key, $query)) {
        return $default;
    }
    return settingToBool(trim((string) $query[$key]), $default);
}

/**
 * Read and validate a date (Y-m-d) from query params.
 */
function gridQueryDate(array $query, string $key, string $default = ''): string
{
    $value = trim((string) ($query[$key] ?? $default));
    if ($value === '') {
        return '';
    }
    return preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) === 1 ? $value : '';
}

/**
 * Read and validate a UUID from query params.
 */
function gridQueryUuid(array $query, string $key, string $default = ''): string
{
    $value = strtolower(trim((string) ($query[$key] ?? $default)));
    if ($value === '') {
        return '';
    }

    return preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[1-8][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/', $value) === 1
        ? $value
        : '';
}

/**
 * Read a single enum value from query params.
 *
 * @param string[] $allowed
 */
function gridQueryEnum(array $query, string $key, array $allowed, string $default = '', bool $lowercase = false): string
{
    $value = trim((string) ($query[$key] ?? $default));
    if ($value === '') {
        return $default;
    }

    if ($lowercase) {
        $value = strtolower($value);
        $allowed = array_map(static fn (string $item): string => strtolower($item), $allowed);
    }

    return in_array($value, $allowed, true) ? $value : $default;
}

/**
 * Extract raw items from a comma-separated or array query parameter.
 *
 * @return list<mixed>
 */
function gridQueryCsvInput(array $query, string $key): array
{
    $value = $query[$key] ?? '';
    if (is_string($value)) {
        return $value === '' ? [] : explode(',', $value);
    }
    if (is_array($value)) {
        $items = [];
        array_walk_recursive($value, static function (mixed $item) use (&$items): void {
            $items[] = $item;
        });
        return $items;
    }

    return [];
}

/**
 * Normalize comma-separated positive IDs from query params.
 *
 * @return int[]
 */
function gridQueryCsvIds(array $query, string $key, int $max = 200): array
{
    if ($max <= 0) {
        return [];
    }

    $items = gridQueryCsvInput($query, $key);

    $ids = [];
    $seen = [];
    foreach ($items as $item) {
        $id = (int) trim((string) $item);
        if ($id <= 0 || isset($seen[$id])) {
            continue;
        }
        $seen[$id] = true;
        $ids[] = $id;
        if (count($ids) >= $max) {
            break;
        }
    }

    return $ids;
}

/**
 * Normalize comma-separated UUIDs from query params.
 *
 * @return string[]
 */
function gridQueryCsvUuids(array $query, string $key, int $max = 200): array
{
    if ($max <= 0) {
        return [];
    }

    $items = gridQueryCsvInput($query, $key);

    $uuids = [];
    $seen = [];
    foreach ($items as $item) {
        $uuid = gridQueryUuid(['v' => $item], 'v');
        if ($uuid === '' || isset($seen[$uuid])) {
            continue;
        }
        $seen[$uuid] = true;
        $uuids[] = $uuid;
        if (count($uuids) >= $max) {
            break;
        }
    }

    return $uuids;
}

/**
 * Normalize comma-separated strings from query params.
 *
 * @return string[]
 */
function gridQueryCsvStrings(array $query, string $key, int $max = 200, ?callable $sanitizer = null): array
{
    if ($max <= 0) {
        return [];
    }

    $items = gridQueryCsvInput($query, $key);

    $result = [];
    $seen = [];
    foreach ($items as $item) {
        $text = trim((string) $item);
        if ($text === '') {
            continue;
        }

        if ($sanitizer !== null) {
            $text = trim((string) $sanitizer($text));
            if ($text === '') {
                continue;
            }
        }

        if (isset($seen[$text])) {
            continue;
        }
        $seen[$text] = true;
        $result[] = $text;
        if (count($result) >= $max) {
            break;
        }
    }

    return $result;
}

/**
 * Normalize a mixed value (string "a||b" or list) to a clean label list.
 *
 * @return string[]
 */
function gridNormalizeLabelList(mixed $value, string $separator = '||'): array
{
    if (is_string($value)) {
        $items = $value === '' ? [] : explode($separator, $value);
    } elseif (is_array($value)) {
        $items = $value;
    } else {
        return [];
    }

    $items = array_map(static fn (mixed $item): string => trim((string) $item), $items);
    $items = array_values(array_filter($items, static fn (string $item): bool => $item !== ''));
    return $items;
}
