<?php

declare(strict_types=1);

namespace Ost\OstCapabilityDataExport\Service;

final class ExportRequestFactory
{
    /**
     * @param array<string, mixed> $payload
     * @param string[] $allowedFields
     */
    public function fromPayload(array $payload, array $allowedFields, string $fallbackFilePrefix): ExportRequest
    {
        $entityValue = $payload['entity'] ?? null;
        $entity = is_scalar($entityValue) ? trim((string) $entityValue) : '';
        if ($entity === '') {
            throw new \InvalidArgumentException('Missing required field: entity');
        }

        $fields = $this->normalizeFields($payload['fields'] ?? null, $allowedFields);
        if ($fields === []) {
            $fields = array_slice($allowedFields, 0, 12);
        }

        $limit = $this->clampInt($payload['limit'] ?? null, 25, 1, 2000);
        $offset = $this->clampInt($payload['offset'] ?? null, 0, 0, 2000000);

        $ids = $this->normalizeStringList($payload['ids'] ?? null, 5000);
        $filters = $this->normalizeFilters($payload['filters'] ?? null, $allowedFields);

        $sortField = null;
        $sortFieldValue = $payload['sortField'] ?? null;
        $rawSortField = is_scalar($sortFieldValue) ? trim((string) $sortFieldValue) : '';
        if ($rawSortField !== '' && in_array($rawSortField, $allowedFields, true)) {
            $sortField = $rawSortField;
        }

        $sortDirectionValue = $payload['sortDirection'] ?? 'ASC';
        $sortDirection = is_scalar($sortDirectionValue) ? strtoupper(trim((string) $sortDirectionValue)) : 'ASC';
        if ($sortDirection !== 'DESC') {
            $sortDirection = 'ASC';
        }

        $delimiter = null;
        if (array_key_exists('delimiter', $payload)) {
            $delimiterValue = $payload['delimiter'];
            $delimiter = is_scalar($delimiterValue) ? trim((string) $delimiterValue) : null;
        }

        $includeBom = array_key_exists('includeBom', $payload)
            ? filter_var($payload['includeBom'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)
            : null;
        $includeHeader = array_key_exists('includeHeader', $payload)
            ? filter_var($payload['includeHeader'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)
            : null;

        $fileName = null;
        if (array_key_exists('fileName', $payload)) {
            $fileNameValue = $payload['fileName'];
            $candidate = is_scalar($fileNameValue) ? trim((string) $fileNameValue) : '';
            $sanitized = preg_replace('/[^a-zA-Z0-9._-]+/', '-', $candidate) ?? '';
            $sanitized = trim($sanitized, '-');
            if ($sanitized !== '') {
                $fileName = str_ends_with($sanitized, '.csv') ? $sanitized : ($sanitized . '.csv');
            }
        }

        if ($fileName === null) {
            $fileName = sprintf('%s-%s.csv', $fallbackFilePrefix, date('Ymd-His'));
        }

        $maxRows = array_key_exists('maxRows', $payload)
            ? $this->clampInt($payload['maxRows'], 2500, 1, 200000)
            : null;

        $batchSize = array_key_exists('batchSize', $payload)
            ? $this->clampInt($payload['batchSize'], 500, 50, 5000)
            : null;

        return new ExportRequest(
            $entity,
            $fields,
            $limit,
            $offset,
            $ids,
            $filters,
            $sortField,
            $sortDirection,
            $delimiter,
            $includeBom,
            $includeHeader,
            $fileName,
            $maxRows,
            $batchSize
        );
    }

    /**
     * @param mixed $value
     * @param string[] $allowedFields
     * @return string[]
     */
    private function normalizeFields(mixed $value, array $allowedFields): array
    {
        if (!is_array($value)) {
            return [];
        }

        $normalized = [];
        foreach ($value as $entry) {
            if (!is_scalar($entry)) {
                continue;
            }
            $candidate = trim((string) $entry);
            if ($candidate === '' || !in_array($candidate, $allowedFields, true)) {
                continue;
            }
            $normalized[] = $candidate;
            if (count($normalized) >= 80) {
                break;
            }
        }

        return array_values(array_unique($normalized));
    }

    /**
     * @param mixed $value
     * @return string[]
     */
    private function normalizeStringList(mixed $value, int $max): array
    {
        if (!is_array($value)) {
            return [];
        }

        $normalized = [];
        foreach ($value as $entry) {
            if (!is_scalar($entry)) {
                continue;
            }
            $candidate = trim((string) $entry);
            if ($candidate === '') {
                continue;
            }
            $normalized[] = $candidate;
            if (count($normalized) >= $max) {
                break;
            }
        }

        return array_values(array_unique($normalized));
    }

    /**
     * @param mixed $value
     * @param string[] $allowedFields
     * @return array<string, scalar>
     */
    private function normalizeFilters(mixed $value, array $allowedFields): array
    {
        if (!is_array($value)) {
            return [];
        }

        $filters = [];
        foreach ($value as $field => $filterValue) {
            if (!is_string($field) || !in_array($field, $allowedFields, true)) {
                continue;
            }
            if (!is_scalar($filterValue)) {
                continue;
            }
            $filters[$field] = $filterValue;
            if (count($filters) >= 25) {
                break;
            }
        }

        return $filters;
    }

    private function clampInt(mixed $value, int $fallback, int $min, int $max): int
    {
        if (!is_scalar($value) || !is_numeric((string) $value)) {
            return $fallback;
        }

        $int = (int) $value;
        if ($int < $min) {
            return $min;
        }
        if ($int > $max) {
            return $max;
        }

        return $int;
    }
}
