<?php

declare(strict_types=1);

namespace App\Services\Import;

use App\Models\Deal;
use App\Models\ImportLog;
use App\Models\ImportUnknownStatus;
use App\Models\Project;
use App\Services\MonthlyPartitionManager;
use App\Services\Pd\PdAuditLogger;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Throwable;

/**
 * Оркестрация исторической миграции лидов из CSV crm.bp-gr.ru (ТЗ §6).
 *
 * Идемпотентность — через webhook_dedup_keys (та же advisory-lock логика, что
 * ProcessWebhookJob). Баланс НЕ списывается: исторические данные не являются
 * новыми лидами (ТЗ §6.5) — фиксируется одна транзакция типа historical_import.
 */
final class HistoricalImportService
{
    public function __construct(
        private readonly MonthlyPartitionManager $partitions,
        private readonly StatusRuToSlugMapper $statusMapper,
        private readonly PdAuditLogger $pdLog,
    ) {}

    /**
     * @param  array<int, ParsedLeadRow>  $rows
     */
    public function import(int $tenantId, int $userId, ImportLog $log, array $rows): ImportResult
    {
        $dryRun = $log->dry_run;

        if ($rows === []) {
            return new ImportResult(0, 0, 0, [], []);
        }

        // Партиции deals под исторический диапазон дат CSV (один раз заранее).
        if (! $dryRun) {
            $dates = array_map(fn (ParsedLeadRow $r) => $r->receivedAt, $rows);
            $this->partitions->ensureRange(
                'deals',
                min($dates),
                max($dates),
            );
        }

        // Tenant-резолвленные переопределения неизвестных статусов.
        $overrides = $this->loadStatusOverrides($tenantId);

        $added = 0;
        $updated = 0;
        $skipped = 0;
        $unknown = [];
        $errors = [];

        foreach ($rows as $row) {
            $slug = $this->resolveStatus($row->statusRu, $overrides, $unknown);

            if ($dryRun) {
                $added++; // проекция: для dry-run не различаем add/update

                continue;
            }

            try {
                $wasCreated = $this->upsertRow($tenantId, $userId, $row, $slug, $log->id);
                $wasCreated ? $added++ : $updated++;
            } catch (Throwable $e) {
                $skipped++;
                $errors[] = ['source_crm_id' => $row->sourceCrmId, 'message' => $e->getMessage()];
                Log::warning('import.row_failed', ['source_crm_id' => $row->sourceCrmId, 'error' => $e->getMessage()]);
            }
        }

        if (! $dryRun) {
            $this->persistUnknownStatuses($tenantId, $log->id, $unknown);
            $this->recordHistoricalTransaction($tenantId, $added + $updated);
        }

        return new ImportResult($added, $updated, $skipped, $unknown, $errors);
    }

    /**
     * @return array<string, string> status_ru => slug (только resolved)
     */
    private function loadStatusOverrides(int $tenantId): array
    {
        return DB::transaction(function () use ($tenantId): array {
            DB::statement('SET LOCAL app.current_tenant_id = '.$tenantId);

            // Явный where(tenant_id) — defense-in-depth: queue worker на prod
            // (crm_supplier_worker) — BYPASSRLS, SET LOCAL не фильтрует
            // (00_create_roles.sql §5). Без фильтра — cross-tenant утечка маппинга.
            return ImportUnknownStatus::query()
                ->where('tenant_id', $tenantId)
                ->whereNotNull('mapped_to_slug')
                ->pluck('mapped_to_slug', 'status_ru')
                ->all();
        });
    }

    /**
     * Маппит статус: StatusRuToSlugMapper → tenant-override → fallback 'new'.
     * Неизвестный статус инкрементит счётчик в $unknown по ссылке.
     *
     * @param  array<string, string>  $overrides
     * @param  array<string, int>  $unknown
     */
    private function resolveStatus(string $statusRu, array $overrides, array &$unknown): string
    {
        $slug = $this->statusMapper->toSlug($statusRu);
        if ($slug !== null) {
            return $slug;
        }

        $key = trim($statusRu);
        if (isset($overrides[$key])) {
            return $overrides[$key];
        }

        $unknown[$key] = ($unknown[$key] ?? 0) + 1;

        return 'new';
    }

    /**
     * Идемпотентный upsert одной строки в собственной транзакции.
     * Возвращает true — создана новая сделка, false — обновлена существующая.
     */
    private function upsertRow(int $tenantId, int $userId, ParsedLeadRow $row, string $slug, int $importLogId): bool
    {
        return DB::transaction(function () use ($tenantId, $userId, $row, $slug, $importLogId): bool {
            DB::statement('SET LOCAL app.current_tenant_id = '.$tenantId);

            $project = Project::firstOrCreate(
                ['tenant_id' => $tenantId, 'name' => $row->projectName],
                ['tag' => $row->projectTag, 'type' => 'import'],
            );

            // advisory lock (tenant_id, source_crm_id) — сериализует upsert (§6.5).
            $lockKey = (($tenantId & 0xFFFFFFFF) << 32) | ($row->sourceCrmId & 0xFFFFFFFF);
            DB::statement('SELECT pg_advisory_xact_lock(?)', [$lockKey]);

            $existing = DB::selectOne(
                'SELECT deal_id, deal_received_at FROM webhook_dedup_keys WHERE tenant_id = ? AND source_crm_id = ?',
                [$tenantId, $row->sourceCrmId],
            );

            if ($existing !== null) {
                $deal = Deal::query()
                    ->where('id', $existing->deal_id)
                    ->where('received_at', $existing->deal_received_at)
                    ->firstOrFail();
                // §6.5 стадия 3a: для исторической миграции status перезаписывается.
                $deal->update([
                    'status' => $slug,
                    'contact_name' => $row->contactName,
                    'comment' => $row->comment,
                ]);

                return false;
            }

            $deal = Deal::create([
                'tenant_id' => $tenantId,
                'source_crm_id' => $row->sourceCrmId,
                'project_id' => $project->id,
                'phone' => $row->phone,
                'status' => $slug,
                'contact_name' => $row->contactName,
                'comment' => $row->comment,
                'received_at' => $row->receivedAt,
            ]);

            DB::table('webhook_dedup_keys')->insert([
                'tenant_id' => $tenantId,
                'source_crm_id' => $row->sourceCrmId,
                'deal_id' => $deal->id,
                'deal_received_at' => $deal->received_at,
                'created_at' => now(),
            ]);

            $this->pdLog->record(
                action: 'created',
                subjectType: 'lead',
                subjectId: $deal->id,
                purpose: 'lead_create_import_'.$importLogId,
                tenantId: $tenantId,
                actorTenantUserId: $userId,
                actorAdminUserId: null,
                ip: null,
            );

            return true;
        });
    }

    /**
     * upsert import_unknown_statuses: инкремент occurrences, маппинг не трогаем.
     *
     * @param  array<string, int>  $unknown
     */
    private function persistUnknownStatuses(int $tenantId, int $importLogId, array $unknown): void
    {
        if ($unknown === []) {
            return;
        }

        DB::transaction(function () use ($tenantId, $importLogId, $unknown): void {
            DB::statement('SET LOCAL app.current_tenant_id = '.$tenantId);

            foreach ($unknown as $statusRu => $count) {
                // Явный where(tenant_id) — defense-in-depth под BYPASSRLS queue worker
                // (00_create_roles.sql §5): иначе increment мог бы попасть в строку
                // чужого тенанта с тем же status_ru.
                $existing = ImportUnknownStatus::query()
                    ->where('tenant_id', $tenantId)
                    ->where('status_ru', $statusRu)
                    ->first();

                if ($existing !== null) {
                    $existing->increment('occurrences', $count);

                    continue;
                }

                ImportUnknownStatus::create([
                    'tenant_id' => $tenantId,
                    'import_log_id' => $importLogId,
                    'status_ru' => $statusRu,
                    'occurrences' => $count,
                ]);
            }
        });
    }

    /**
     * Одна информационная транзакция historical_import (баланс не меняется, ТЗ §6.5).
     */
    private function recordHistoricalTransaction(int $tenantId, int $count): void
    {
        if ($count === 0) {
            return;
        }

        DB::transaction(function () use ($tenantId, $count): void {
            DB::statement('SET LOCAL app.current_tenant_id = '.$tenantId);

            DB::table('balance_transactions')->insert([
                'tenant_id' => $tenantId,
                'type' => 'historical_import',
                'amount_rub' => 0,
                'amount_leads' => 0,
                'description' => "Импортировано {$count} исторических сделок (баланс не списан)",
                'created_at' => now(),
            ]);
        });
    }
}
