<?php

declare(strict_types=1);

namespace Ost\OstCapabilityDataExport\Service;

use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\DefinitionInstanceRegistry;
use Shopware\Core\Framework\DataAbstractionLayer\Entity;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
use Shopware\Core\System\SystemConfig\SystemConfigService;

final class EntityExportService
{
    public function __construct(
        private readonly DefinitionInstanceRegistry $definitionRegistry,
        private readonly EntityMetadataService $metadataService,
        private readonly CsvWriter $csvWriter,
        private readonly SystemConfigService $systemConfigService
    ) {
    }

    /**
     * @return array{headers: string[], rows: array<int, array<string, mixed>>}
     */
    public function preview(ExportRequest $request, Context $context): array
    {
        $headers = $request->fields();
        $rows = $this->fetchRows($request, $context, max(1, min($request->limit(), 50)));

        return [
            'headers' => $headers,
            'rows' => $rows,
        ];
    }

    /**
     * @return array{csv: string, fileName: string}
     */
    public function export(ExportRequest $request, Context $context): array
    {
        $rows = $this->collectRows($request, $context);

        $delimiter = $this->resolveDelimiter($request->delimiter());
        $includeBom = $request->includeBom() ?? $this->defaultIncludeBom();
        $includeHeader = $request->includeHeader() ?? true;

        $csv = $this->csvWriter->render($request->fields(), $rows, $delimiter, $includeBom, $includeHeader);

        return [
            'csv' => $csv,
            'fileName' => $this->resolveFileName($request->fileName()),
        ];
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function collectRows(ExportRequest $request, Context $context): array
    {
        $maxRows = $request->maxRows() ?? $this->defaultMaxRows();
        $batchSize = $request->batchSize() ?? $this->defaultBatchSize();

        $collected = [];
        $offset = $request->offset();

        while (count($collected) < $maxRows) {
            $remaining = $maxRows - count($collected);
            $limit = min($batchSize, $remaining);

            $rows = $this->fetchRows($request, $context, $limit, $offset);
            if ($rows === []) {
                break;
            }

            foreach ($rows as $row) {
                $collected[] = $row;
                if (count($collected) >= $maxRows) {
                    break 2;
                }
            }

            $offset += $limit;
            if (count($rows) < $limit) {
                break;
            }
        }

        return $collected;
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function fetchRows(ExportRequest $request, Context $context, int $limit, ?int $offsetOverride = null): array
    {
        $limit = max(1, min($limit, 5000));
        $offset = $offsetOverride ?? $request->offset();

        $allowedFields = $this->metadataService->listExportableFields($request->entity());
        $headers = array_values(array_filter(
            $request->fields(),
            static fn (string $field): bool => in_array($field, $allowedFields, true)
        ));

        if ($headers === []) {
            throw new \InvalidArgumentException('No exportable fields selected.');
        }

        $repository = $this->definitionRegistry->getRepository($request->entity());

        $criteria = new Criteria();
        $criteria->setLimit($limit);
        $criteria->setOffset($offset);

        if ($request->ids() !== []) {
            $criteria->addFilter(new EqualsAnyFilter('id', $request->ids()));
        }

        foreach ($request->filters() as $field => $value) {
            if (!in_array($field, $allowedFields, true)) {
                continue;
            }
            $criteria->addFilter(new EqualsFilter($field, $value));
        }

        if ($request->sortField() !== null && in_array($request->sortField(), $allowedFields, true)) {
            $criteria->addSorting(new FieldSorting($request->sortField(), $request->sortDirection()));
        }

        $result = $repository->search($criteria, $context);

        $rows = [];
        foreach ($result->getEntities() as $entity) {
            if (!$entity instanceof Entity) {
                continue;
            }

            $row = [];
            foreach ($headers as $header) {
                $row[$header] = $this->normalizeValue($entity->get($header));
            }
            $rows[] = $row;
        }

        return $rows;
    }

    private function normalizeValue(mixed $value): string
    {
        if ($value instanceof \DateTimeInterface) {
            return $value->format(DATE_ATOM);
        }

        if (is_bool($value)) {
            return $value ? '1' : '0';
        }

        if (is_scalar($value)) {
            return (string) $value;
        }

        if (is_array($value)) {
            $json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

            return is_string($json) ? $json : '';
        }

        if ($value instanceof \Stringable) {
            return (string) $value;
        }

        return '';
    }

    private function resolveDelimiter(?string $requested): string
    {
        $value = $requested;
        if ($value === null || trim($value) === '') {
            $configured = $this->systemConfigService->get('OstCapabilityDataExport.config.defaultDelimiter');
            $value = is_string($configured) ? $configured : ';';
        }

        $trimmed = trim($value);

        return ($trimmed === ';' || $trimmed === ',' || $trimmed === "\t") ? $trimmed : ';';
    }

    private function defaultIncludeBom(): bool
    {
        $configured = $this->systemConfigService->get('OstCapabilityDataExport.config.defaultIncludeBom');

        return filter_var($configured, FILTER_VALIDATE_BOOLEAN);
    }

    private function defaultBatchSize(): int
    {
        $configured = $this->systemConfigService->get('OstCapabilityDataExport.config.defaultBatchSize');
        if (is_numeric($configured)) {
            $value = (int) $configured;
            if ($value >= 50 && $value <= 5000) {
                return $value;
            }
        }

        return 500;
    }

    private function defaultMaxRows(): int
    {
        $configured = $this->systemConfigService->get('OstCapabilityDataExport.config.defaultMaxRows');
        if (is_numeric($configured)) {
            $value = (int) $configured;
            if ($value >= 100 && $value <= 500000) {
                return $value;
            }
        }

        return 25000;
    }

    private function resolveFileName(?string $requested): string
    {
        $candidate = $requested !== null ? trim($requested) : '';
        if ($candidate === '') {
            return sprintf('ost-capability-data-export-%s.csv', date('Ymd-His'));
        }

        return str_ends_with($candidate, '.csv') ? $candidate : ($candidate . '.csv');
    }
}
