<?php

declare(strict_types=1);

namespace App\Services\Import;

use Carbon\CarbonImmutable;
use Throwable;

/**
 * Парсер CSV-выгрузки лидов из crm.bp-gr.ru (ТЗ §6.2/§6.3).
 *
 * Формат: UTF-8 с BOM, разделитель — запятая, дата `Y/m/d H:i:s`,
 * телефон `7XXXXXXXXXX`. Заголовок:
 *   id,Проект,Тег проекта,Телефон,Создано,Напоминание,Комментарий,Состояние,Имя
 *
 * Невалидные строки не роняют парсинг — собираются в errors[].
 * Файл целиком загружается в память (MVP: ожидаемый объём — единицы тысяч строк).
 */
final class CsvLeadsParser
{
    private const EXPECTED_COLUMNS = 9;

    private const DATE_FORMAT = 'Y/m/d H:i:s';

    public function parse(string $content): CsvParseResult
    {
        // Срезаем UTF-8 BOM.
        if (str_starts_with($content, "\xEF\xBB\xBF")) {
            $content = substr($content, 3);
        }

        // FN-ENC (приёмка 22.06): один битый UTF-8-байт в выгрузке ронял весь импорт
        // на INSERT (PG: invalid byte sequence for encoding UTF8). str_getcsv пропускает
        // любые байты, и невалидная последовательность доходила до БД. Чистим невалидный
        // UTF-8 ДО парсинга — битый байт убирается, строка импортируется, очередь не падает.
        $content = mb_convert_encoding($content, 'UTF-8', 'UTF-8');

        $lines = preg_split('/\r\n|\r|\n/', trim($content)) ?: [];
        $rows = [];
        $errors = [];

        // Строка 1 — заголовок, пропускаем. dataLine — абсолютный номер строки файла (заголовок = 1).
        foreach (array_slice($lines, 1) as $index => $rawLine) {
            $dataLine = $index + 2; // +2: пропущен заголовок (index 0 → строка 2)

            if (trim($rawLine) === '') {
                continue;
            }

            $cells = str_getcsv($rawLine);

            if (count($cells) < self::EXPECTED_COLUMNS) {
                $errors[] = ['line' => $dataLine, 'message' => 'Ожидалось '.self::EXPECTED_COLUMNS.' колонок, получено '.count($cells)];

                continue;
            }

            $parsed = $this->parseRow($cells, $dataLine, $errors);
            if ($parsed !== null) {
                $rows[] = $parsed;
            }
        }

        return new CsvParseResult($rows, $errors);
    }

    /**
     * @param  array<int, string>  $cells
     * @param  array<int, array{line: int, message: string}>  $errors
     */
    private function parseRow(array $cells, int $dataLine, array &$errors): ?ParsedLeadRow
    {
        [$id, $project, $tag, $phone, $createdAt, $reminder, $comment, $status, $name] = $cells;

        $phone = trim($phone);
        if (preg_match('/^7\d{10}$/', $phone) !== 1) {
            $errors[] = ['line' => $dataLine, 'message' => "Невалидный телефон: '{$phone}'"];

            return null;
        }

        $receivedAt = $this->parseDate($createdAt);
        if ($receivedAt === null) {
            $errors[] = ['line' => $dataLine, 'message' => "Невалидная дата 'Создано': '{$createdAt}'"];

            return null;
        }

        $reminderAt = trim($reminder) === '' ? null : $this->parseDate($reminder);
        if (trim($reminder) !== '' && $reminderAt === null) {
            $errors[] = ['line' => $dataLine, 'message' => "Невалидная дата 'Напоминание': '{$reminder}'"];

            return null;
        }

        $status = trim($status);
        if ($status === '') {
            $errors[] = ['line' => $dataLine, 'message' => 'Пустое поле «Состояние»'];

            return null;
        }

        // Префикс B[123]_ из названия проекта срезается (паритет с RouteSupplierLeadJob парсером).
        $projectName = (string) preg_replace('/^B[123]_/', '', trim($project));
        if ($projectName === '') {
            $errors[] = ['line' => $dataLine, 'message' => 'Пустое название проекта'];

            return null;
        }

        return new ParsedLeadRow(
            sourceCrmId: (int) trim($id),
            projectName: $projectName,
            projectTag: trim($tag) === '' ? null : trim($tag),
            phone: $phone,
            receivedAt: $receivedAt,
            reminderAt: $reminderAt,
            comment: trim($comment) === '' ? null : trim($comment),
            statusRu: $status,
            contactName: trim($name) === '' ? null : trim($name),
        );
    }

    private function parseDate(string $value): ?CarbonImmutable
    {
        try {
            $date = CarbonImmutable::createFromFormat(self::DATE_FORMAT, trim($value));
        } catch (Throwable) {
            return null;
        }

        // createFromFormat возвращает false при несовпадении формата.
        return $date instanceof CarbonImmutable ? $date : null;
    }
}
