<?php

declare(strict_types=1);

/**
 * 本文件属于 DBErp 进销存系统。
 *
 * 版权所有 © 2026 北京珑大钜商科技有限公司。保留所有权利。
 *
 * 未经官方授权，不得删除、篡改或隐藏本文件中的版权声明、产品标识与来源信息。
 * 商业授权、官方更新与技术支持服务，请以珑大官方支持中心记录为准。
 *
 * @link https://support.loongdom.cn
 * @see  https://www.dberp.com.cn
 */

namespace App\Business\DataImport;

use App\Business\DataImport\Enums\DataImportType;
use Illuminate\Validation\ValidationException;
use Maatwebsite\Excel\Facades\Excel;
use RuntimeException;

final class DataImportFileReader
{
    public function __construct(
        private readonly DataImportTemplateRegistry $templateRegistry,
    ) {}

    public function read(DataImportType|DataImportTypeReference|string $importType, string $absolutePath, string $sourceFilename): DataImportFileReadResult
    {
        $importType = DataImportTypeReference::from($importType);

        if (! $this->templateRegistry->supports($importType)) {
            throw ValidationException::withMessages([
                'upload_file' => (string) __('dberp.data_import.errors.unsupported_import_type'),
            ]);
        }

        if (! is_file($absolutePath) || ! is_readable($absolutePath)) {
            throw ValidationException::withMessages([
                'upload_file' => (string) __('dberp.data_import.errors.file_not_readable'),
            ]);
        }

        $this->assertFileSizeWithinLimit($absolutePath);
        $this->assertAllowedExtension($sourceFilename);

        $matrix = $this->readMatrix($absolutePath, $sourceFilename);

        if ($matrix === []) {
            throw ValidationException::withMessages([
                'upload_file' => (string) __('dberp.data_import.errors.file_empty'),
            ]);
        }

        $headingRow = array_shift($matrix);
        $this->assertHeadingRow($importType, is_array($headingRow) ? $headingRow : []);

        $headings = $this->templateRegistry->headings($importType);
        $rows = [];
        $rowNumber = 1;

        foreach ($matrix as $rawRow) {
            $rowNumber++;

            if (! is_array($rawRow)) {
                continue;
            }

            $mapped = $this->mapRowToHeadings($headings, $rawRow);

            if ($this->templateRegistry->isExampleRow($importType, $mapped['values'])) {
                continue;
            }

            if ($this->rowIsBlank($mapped['values'])) {
                continue;
            }

            $rows[] = new DataImportRow(
                rowNumber: $rowNumber,
                values: $mapped['values'],
                rawValues: $mapped['raw'],
            );
        }

        $this->assertRowCountWithinLimit(count($rows));

        if ($rows === []) {
            throw ValidationException::withMessages([
                'upload_file' => (string) __('dberp.data_import.errors.no_data_rows'),
            ]);
        }

        return new DataImportFileReadResult(
            importType: $importType,
            sourceFilename: $sourceFilename,
            totalRows: count($rows),
            rows: $rows,
        );
    }

    private function assertFileSizeWithinLimit(string $absolutePath): void
    {
        $maxKilobytes = max(1, (int) config('dberp.data_import.max_upload_kilobytes', 10_240));
        $sizeKilobytes = (int) ceil(filesize($absolutePath) / 1024);

        if ($sizeKilobytes > $maxKilobytes) {
            throw ValidationException::withMessages([
                'upload_file' => (string) __('dberp.data_import.errors.file_too_large', [
                    'max_mb' => number_format($maxKilobytes / 1024, 1, '.', ''),
                ]),
            ]);
        }
    }

    private function assertAllowedExtension(string $sourceFilename): void
    {
        $extension = strtolower((string) pathinfo($sourceFilename, PATHINFO_EXTENSION));
        /** @var list<string> $allowed */
        $allowed = config('dberp.data_import.allowed_extensions', ['xlsx', 'csv']);

        if (! in_array($extension, $allowed, true)) {
            throw ValidationException::withMessages([
                'upload_file' => (string) __('dberp.data_import.errors.unsupported_file_type'),
            ]);
        }
    }

    /**
     * @param  list<string|null>  $headingRow
     */
    private function assertHeadingRow(DataImportType|DataImportTypeReference|string $importType, array $headingRow): void
    {
        $analysis = $this->templateRegistry->analyzeHeadingRow($importType, $headingRow);

        if ($analysis['forbidden'] !== []) {
            $columns = $this->templateRegistry->formatFieldKeysForDisplay($analysis['forbidden']);

            if (DataImportTypeReference::from($importType)->toEnum() === DataImportType::Product) {
                throw ValidationException::withMessages([
                    'upload_file' => (string) __('dberp.data_import.errors.forbidden_product_multi_spec_columns', [
                        'columns' => $columns,
                    ]),
                ]);
            }

            throw ValidationException::withMessages([
                'upload_file' => (string) __('dberp.data_import.errors.forbidden_heading_columns', [
                    'columns' => $columns,
                ]),
            ]);
        }

        if ($analysis['missing'] !== []) {
            throw ValidationException::withMessages([
                'upload_file' => (string) __('dberp.data_import.errors.heading_missing_columns', [
                    'columns' => $this->templateRegistry->formatFieldKeysForDisplay($analysis['missing']),
                ]),
            ]);
        }

        if ($analysis['extra'] !== []) {
            throw ValidationException::withMessages([
                'upload_file' => (string) __('dberp.data_import.errors.heading_extra_columns', [
                    'columns' => $this->templateRegistry->formatFieldKeysForDisplay($analysis['extra']),
                ]),
            ]);
        }

        if ($analysis['actual'] !== $analysis['expected']) {
            throw ValidationException::withMessages([
                'upload_file' => (string) __('dberp.data_import.errors.heading_order_mismatch'),
            ]);
        }
    }

    private function assertRowCountWithinLimit(int $rowCount): void
    {
        $maxRows = max(1, (int) config('dberp.data_import.max_rows', 5000));

        if ($rowCount > $maxRows) {
            throw ValidationException::withMessages([
                'upload_file' => (string) __('dberp.data_import.errors.too_many_rows', [
                    'max_rows' => $maxRows,
                ]),
            ]);
        }
    }

    /**
     * @return list<list<mixed>>
     */
    private function readMatrix(string $absolutePath, string $sourceFilename): array
    {
        $extension = strtolower((string) pathinfo($sourceFilename, PATHINFO_EXTENSION));

        if ($extension === 'csv') {
            return $this->readCsvMatrix($absolutePath);
        }

        return $this->readSpreadsheetMatrix($absolutePath);
    }

    /**
     * @return list<list<mixed>>
     */
    private function readCsvMatrix(string $absolutePath): array
    {
        $handle = fopen($absolutePath, 'rb');

        if ($handle === false) {
            throw new RuntimeException('Unable to open CSV file.');
        }

        $bom = fread($handle, 3);

        if ($bom !== "\xEF\xBB\xBF") {
            rewind($handle);
        }

        $matrix = [];

        while (($cells = fgetcsv($handle)) !== false) {
            $matrix[] = $cells;
        }

        fclose($handle);

        return $matrix;
    }

    /**
     * @return list<list<mixed>>
     */
    private function readSpreadsheetMatrix(string $absolutePath): array
    {
        $sheets = Excel::toArray(new DataImportSpreadsheetCapture, $absolutePath);
        $matrix = $sheets[0] ?? [];

        return is_array($matrix) ? $matrix : [];
    }

    /**
     * @param  list<string>  $headings
     * @param  list<mixed>  $rawRow
     * @return array{values: array<string, string|null>, raw: array<string, mixed>}
     */
    private function mapRowToHeadings(array $headings, array $rawRow): array
    {
        $values = [];
        $raw = [];

        foreach ($headings as $index => $heading) {
            $cell = $rawRow[$index] ?? null;
            $values[$heading] = $this->normalizeCell($cell);
            $raw[$heading] = $cell;
        }

        return [
            'values' => $values,
            'raw' => $raw,
        ];
    }

    /**
     * @param  array<string, string|null>  $values
     */
    private function rowIsBlank(array $values): bool
    {
        foreach ($values as $value) {
            if ($value !== null && $value !== '') {
                return false;
            }
        }

        return true;
    }

    private function normalizeCell(mixed $value): ?string
    {
        if ($value === null) {
            return null;
        }

        if (is_string($value)) {
            $trimmed = trim($value);

            return $trimmed === '' ? null : $trimmed;
        }

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

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

        if (is_float($value)) {
            if (floor($value) === $value) {
                return (string) (int) $value;
            }

            return rtrim(rtrim(sprintf('%.10F', $value), '0'), '.');
        }

        $string = trim((string) $value);

        return $string === '' ? null : $string;
    }
}
