<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Deal;
use App\Services\Pd\PdAuditLogger;
use App\Support\CsvFormulaGuard;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Common\Entity\Style\Style;
use OpenSpout\Writer\CSV\Options as CsvOptions;
use OpenSpout\Writer\CSV\Writer as CsvWriter;
use OpenSpout\Writer\XLSX\Writer as XlsxWriter;
use Symfony\Component\HttpFoundation\StreamedResponse;

/**
 * Экспорт сделок в CSV / XLSX через OpenSpout streaming.
 *
 * Редизайн «Сделки» (2026-05-17, Task A5): экспорт по ДИАПАЗОНУ ДАТ поставки
 * (received_at), не по списку id. Окно задаётся received_from/received_to;
 * оба опциональны (пусто = весь период). Колонки соответствуют таблице
 * страницы (без чекбокса и без «Напоминание» — экспорт = дамп лидов).
 *
 * RLS-обёртка SET LOCAL внутри транзакции (PgBouncer-safe).
 *
 * J1 (Sprint 3F): auth:sanctum+tenant, tenant_id из auth()->user().
 *
 * O-perf-05: streaming устраняет memory pressure. OpenSpout пишет
 * в php://output постранично через Writer + Row::fromValues и chunkById(500)
 * по сделкам — пик памяти O(1) от размера экспорта.
 */
class DealExportController extends Controller
{
    /** Заголовки — общие для CSV и XLSX. */
    private const HEADERS = ['Телефон', 'Источник', 'Город', 'Статус', 'Комментарий', 'Поставлен'];

    /** signal_type → русская метка для колонки «Источник». */
    private const SIGNAL_LABELS = ['call' => 'Звонки', 'site' => 'Сайт', 'sms' => 'СМС'];

    public function export(Request $request): StreamedResponse
    {
        $validated = $request->validate([
            'received_from' => 'nullable|date',
            'received_to' => 'nullable|date',
            'format' => 'nullable|string|in:csv,xlsx',
        ]);

        $tenantId = (int) $request->user()->tenant_id;
        $format = $validated['format'] ?? 'csv';
        $from = isset($validated['received_from']) && $validated['received_from'] !== ''
            ? Carbon::parse($validated['received_from'])->startOfDay() : null;
        $to = isset($validated['received_to']) && $validated['received_to'] !== ''
            ? Carbon::parse($validated['received_to'])->addDay()->startOfDay() : null;

        app(PdAuditLogger::class)->record(
            action: 'exported',
            subjectType: 'lead',
            subjectId: null,
            purpose: 'deals_export_'.$format,
            tenantId: $tenantId,
            actorTenantUserId: (int) $request->user()->id,
            actorAdminUserId: null,
            ip: $request->ip(),
        );

        $filename = 'deals_export_'.now()->format('Y-m-d').'.'.$format;
        $headers = $format === 'xlsx'
            ? [
                'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                'Content-Disposition' => 'attachment; filename="'.$filename.'"',
            ]
            : [
                'Content-Type' => 'text/csv; charset=utf-8',
                'Content-Disposition' => 'attachment; filename="'.$filename.'"',
            ];

        return new StreamedResponse(function () use ($tenantId, $format, $from, $to) {
            // RLS-контекст должен быть установлен внутри транзакции на момент
            // фактического SELECT. StreamedResponse callback вызывается уже
            // после Laravel-response pipeline'а, поэтому открываем транзакцию
            // прямо здесь.
            DB::transaction(function () use ($tenantId, $format, $from, $to) {
                DB::statement('SET LOCAL app.current_tenant_id = '.$tenantId);

                $statusNames = DB::table('lead_statuses')->pluck('name_ru', 'slug');

                $writer = $this->openWriter($format);
                $writer->openToFile('php://output');

                // Заголовок: для XLSX — bold через Style + имя листа «Сделки».
                // Для CSV — OpenSpout сам пишет UTF-8 BOM (SHOULD_ADD_BOM=true
                // в Options) и `;`-разделитель из конструктора.
                if ($format === 'xlsx') {
                    /** @var XlsxWriter $writer */
                    $writer->getCurrentSheet()->setName('Сделки');
                    $writer->addRow(Row::fromValuesWithStyle(self::HEADERS, (new Style)->withFontBold(true)));
                } else {
                    $writer->addRow(Row::fromValues(self::HEADERS));
                }

                $query = Deal::query()
                    ->where('tenant_id', $tenantId)
                    ->with('project:id,name,signal_type')
                    ->orderByDesc('received_at');

                if ($from !== null) {
                    $query->where('received_at', '>=', $from);
                }
                if ($to !== null) {
                    $query->where('received_at', '<', $to);
                }

                // chunkById(500) — keyset-friendly; deals.id — BIGSERIAL (unique),
                // корректно для чанкинга даже при партиционированной PK (id, received_at).
                $query->chunkById(500, function ($deals) use ($writer, $statusNames) {
                    foreach ($deals as $deal) {
                        /** @var Deal $deal */
                        $signal = $deal->project?->signal_type;
                        $source = trim(($deal->project?->name ?? '—').' · '
                            .(self::SIGNAL_LABELS[$signal] ?? '—'));
                        // F-CSV: свободный текст (телефон/источник/город/статус/
                        // комментарий) экранируем от formula-инъекции. Дата —
                        // системная, не экранируется.
                        $writer->addRow(Row::fromValues([
                            CsvFormulaGuard::neutralize((string) $deal->phone),
                            CsvFormulaGuard::neutralize($source),
                            CsvFormulaGuard::neutralize((string) ($deal->city ?? '')),
                            CsvFormulaGuard::neutralize((string) ($statusNames[$deal->status] ?? $deal->status)),
                            CsvFormulaGuard::neutralize((string) ($deal->comment ?? '')),
                            $deal->received_at?->toDateTimeString() ?? '',
                        ]));
                    }
                }, 'id');

                $writer->close();
            });
        }, 200, $headers);
    }

    private function openWriter(string $format): CsvWriter|XlsxWriter
    {
        if ($format === 'xlsx') {
            return new XlsxWriter;
        }

        // CSV: ;-разделитель + UTF-8 BOM (Excel-friendly RU-локаль).
        return new CsvWriter(new CsvOptions(
            FIELD_DELIMITER: ';',
            FIELD_ENCLOSURE: '"',
            SHOULD_ADD_BOM: true,
        ));
    }
}
