<?php

declare(strict_types=1);

namespace App\Services\Supplier\Import;

use App\Models\Project;
use App\Models\SupplierProject;
use App\Models\SupplierSyncLog;
use App\Services\Supplier\SupplierPortalClient;
use App\Services\Supplier\SupplierProjectGrouping;
use App\Support\SupplierRegions;
use Illuminate\Support\Facades\DB;

/**
 * Усыновление активных проектов поставщика (аккаунт lkomega) как проектов
 * Лидерры. Читает listProjects (read-only), группирует площадки B1/B2/B3 в один
 * проект, реверс-маппит регионы, считает лимит как сумму площадок.
 *
 * Spec: docs/superpowers/specs/2026-05-22-supplier-projects-import-lkomega-design.md
 */
class SupplierProjectImporter
{
    private const DB_CONNECTION = 'pgsql_supplier';

    public function __construct(
        private readonly SupplierPortalClient $client,
    ) {}

    /**
     * @return array{planned: list<array<string, mixed>>, skipped: list<array{reason: string, label: string}>}
     */
    public function buildPlan(int $tenantId): array
    {
        $rows = $this->client->listProjects();

        /** @var list<array{reason: string, label: string}> $skipped */
        $skipped = [];

        /** @var array<string, array<string, mixed>> $groups */
        $groups = [];

        foreach ($rows as $row) {
            if (($row['status'] ?? false) !== true) {
                continue;
            }

            $platform = SupplierImportMapper::platformFromSrc((string) ($row['src'] ?? ''));
            if ($platform === null) {
                $skipped[] = ['reason' => 'unsupported_source', 'label' => (string) ($row['name'] ?? $row['content'] ?? '?')];

                continue;
            }

            $signalType = SupplierImportMapper::signalTypeFromType((string) ($row['type'] ?? ''));
            if ($signalType === null) {
                $skipped[] = ['reason' => 'unsupported_type', 'label' => (string) ($row['name'] ?? '?')];

                continue;
            }

            if ($signalType === 'sms') {
                $parsed = SupplierImportMapper::parseSmsContent((string) ($row['content'] ?? ''));
                $sender = $parsed['sender'];
                if ($sender === '') {
                    $skipped[] = ['reason' => 'sms_unparseable', 'label' => (string) ($row['name'] ?? '?')];

                    continue;
                }
                $key = 'sms|'.$sender;
                if (! isset($groups[$key])) {
                    $groups[$key] = [
                        'signal_type' => 'sms',
                        'signal_identifier' => null,
                        'sms_senders' => [$sender],
                        'sms_keyword' => null,
                        'tag' => '',
                        'regions' => [],
                        'has_all_russia' => false,
                        'workdays_mask' => 0,
                        'daily_limit_target' => 0,
                        'platforms' => [],
                    ];
                }
                if ($parsed['keyword'] !== null && $parsed['keyword'] !== '' && $groups[$key]['sms_keyword'] === null) {
                    $groups[$key]['sms_keyword'] = $parsed['keyword'];
                }
                if (($row['regions_reverse'] ?? false) === true) {
                    $skipped[] = ['reason' => 'regions_exclude', 'label' => $sender];
                    $groups[$key]['__excluded'] = true;
                }
                $this->accumulateRow($groups[$key], $row, $platform);

                continue;
            }

            $identifier = (string) ($row['content'] ?? '');
            $key = $signalType.'|'.$identifier;

            if (! isset($groups[$key])) {
                $groups[$key] = [
                    'signal_type' => $signalType,
                    'signal_identifier' => $identifier,
                    'sms_senders' => [],
                    'sms_keyword' => null,
                    'tag' => '',
                    'regions' => [],
                    'has_all_russia' => false,
                    'workdays_mask' => 0,
                    'daily_limit_target' => 0,
                    'platforms' => [],
                ];
            }

            if (($row['regions_reverse'] ?? false) === true) {
                $skipped[] = ['reason' => 'regions_exclude', 'label' => $identifier];
                $groups[$key]['__excluded'] = true;
            }
            $this->accumulateRow($groups[$key], $row, $platform);
        }

        $planned = [];
        foreach ($groups as $g) {
            if (($g['__excluded'] ?? false) === true) {
                continue;
            }
            unset($g['__excluded']);
            unset($g['has_all_russia']);
            $g['delivery_days_mask'] = $g['workdays_mask'] === 0 ? 127 : $g['workdays_mask'];
            unset($g['workdays_mask']);
            if ($g['tag'] === '') {
                $g['tag'] = 'РФ';
            }
            $g['name'] = $this->deriveName($g);

            if ($this->projectExists($tenantId, $g)) {
                $skipped[] = ['reason' => 'already_exists', 'label' => $this->groupLabel($g)];

                continue;
            }
            $planned[] = $g;
        }

        return ['planned' => $planned, 'skipped' => $skipped];
    }

    /**
     * Пишет план в БД: Project + supplier_projects (external_id с портала) + pivot.
     * НЕ обращается к порталу. Каждый проект — в своей транзакции.
     *
     * @param  array{planned: list<array<string, mixed>>, skipped: list<array{reason: string, label: string}>}  $plan
     * @return array{created_projects: int, created_supplier_projects: int, created_links: int}
     */
    public function commit(array $plan, int $tenantId): array
    {
        $createdProjects = 0;
        $createdSps = 0;
        $createdLinks = 0;

        $conn = DB::connection(self::DB_CONNECTION);

        foreach ($plan['planned'] as $item) {
            $writeItem = function () use ($item, $tenantId, &$createdProjects, &$createdSps, &$createdLinks): void {
                /** @var Project $project */
                $project = Project::on(self::DB_CONNECTION)->create([
                    'tenant_id' => $tenantId,
                    'name' => $item['name'],
                    'tag' => $item['tag'],
                    'is_active' => true,
                    'signal_type' => $item['signal_type'],
                    'signal_identifier' => $item['signal_identifier'],
                    'sms_senders' => $item['sms_senders'] !== [] ? $item['sms_senders'] : null,
                    'sms_keyword' => $item['sms_keyword'],
                    'regions' => $item['regions'],
                    'region_mode' => 'include',
                    'delivery_days_mask' => $item['delivery_days_mask'],
                    'daily_limit_target' => $item['daily_limit_target'],
                ]);
                $createdProjects++;

                // R-17 (Stage 4 §4.4.1): unified agnostic key — was per-platform divergence
                // for SMS (B3 used sender alone, B2 sender+keyword) creating orphan rows.
                $uniqueKey = SupplierProjectGrouping::buildUniqueKeyAgnostic($project);
                foreach ($item['platforms'] as $pl) {
                    $platform = (string) $pl['platform'];

                    /** @var SupplierProject $sp */
                    $sp = SupplierProject::on(self::DB_CONNECTION)->firstOrCreate(
                        ['platform' => $platform, 'unique_key' => $uniqueKey, 'subject_code' => null],
                        [
                            'signal_type' => $item['signal_type'],
                            'supplier_external_id' => (string) $pl['external_id'],
                            'current_limit' => (int) $pl['lim'],
                            'current_workdays' => $this->maskToList((int) $item['delivery_days_mask']),
                            'current_regions' => $item['regions'],
                            'sync_status' => 'ok',
                            'last_synced_at' => now(),
                        ],
                    );
                    if ($sp->wasRecentlyCreated) {
                        $createdSps++;
                        SupplierSyncLog::on(self::DB_CONNECTION)->create([
                            'supplier_project_id' => $sp->id,
                            'action' => 'create',
                            'http_status' => 200,
                            'created_at' => now(),
                        ]);
                    }

                    $inserted = DB::connection(self::DB_CONNECTION)->table('project_supplier_links')->insertOrIgnore([
                        'project_id' => $project->id,
                        'supplier_project_id' => $sp->id,
                        'platform' => $platform,
                        'subject_code' => null,
                    ]);
                    $createdLinks += $inserted;
                }
            };

            // Per-project atomicity (spec §8): сбой посреди группы не должен оставить
            // orphan-Project без supplier_projects/pivot. В проде оборачиваем в транзакцию.
            // Под тестовым харнессом (SharesSupplierPdo + DatabaseTransactions) общий PDO
            // уже в транзакции — повторный BEGIN бросил бы «already active», поэтому пишем
            // напрямую (внешняя транзакция теста сама откатится).
            if ($conn->getPdo()->inTransaction()) {
                $writeItem();
            } else {
                $conn->transaction($writeItem);
            }
        }

        return [
            'created_projects' => $createdProjects,
            'created_supplier_projects' => $createdSps,
            'created_links' => $createdLinks,
        ];
    }

    /**
     * Маска дней (bit0=Пн) → list<int> [1..7].
     *
     * @return list<int>
     */
    private function maskToList(int $mask): array
    {
        $out = [];
        for ($i = 0; $i < 7; $i++) {
            if (($mask & (1 << $i)) !== 0) {
                $out[] = $i + 1;
            }
        }

        return $out;
    }

    /**
     * @param  array<string, mixed>  $group
     * @param  array<string, mixed>  $row
     */
    private function accumulateRow(array &$group, array $row, string $platform): void
    {
        $lim = (int) ($row['lim'] ?? 0);
        $group['daily_limit_target'] += $lim;
        $group['platforms'][] = [
            'platform' => $platform,
            'external_id' => (int) ($row['id'] ?? 0),
            'lim' => $lim,
        ];

        $rowTag = trim((string) ($row['tag'] ?? ''));
        if ($group['tag'] === '' && $rowTag !== '' && $rowTag !== 'РФ') {
            $group['tag'] = $rowTag;
        }

        $group['workdays_mask'] |= SupplierImportMapper::workdaysToMask((array) ($row['workdays'] ?? []));

        if (! $group['has_all_russia']) {
            $gibdd = SupplierImportMapper::parseGibddRegions(
                is_string($row['regions'] ?? null) ? $row['regions'] : ''
            );
            if ($gibdd === []) {
                $group['has_all_russia'] = true;
                $group['regions'] = [];
            } else {
                $liderra = SupplierRegions::mapFromSupplier($gibdd);
                $group['regions'] = array_values(array_unique(array_merge($group['regions'], $liderra)));
                sort($group['regions']);
            }
        }
    }

    /**
     * @param  array<string, mixed>  $group
     */
    private function projectExists(int $tenantId, array $group): bool
    {
        $query = Project::on('pgsql_supplier')
            ->where('tenant_id', $tenantId)
            ->where('signal_type', $group['signal_type']);

        if ($group['signal_type'] === 'sms') {
            $sender = $group['sms_senders'][0] ?? '';
            $keyword = $group['sms_keyword'];

            return $query
                ->whereJsonContains('sms_senders', $sender)
                ->where(fn ($q) => $keyword === null ? $q->whereNull('sms_keyword') : $q->where('sms_keyword', $keyword))
                ->exists();
        }

        return $query->where('signal_identifier', $group['signal_identifier'])->exists();
    }

    /**
     * @param  array<string, mixed>  $group
     */
    private function groupLabel(array $group): string
    {
        return $group['signal_type'] === 'sms'
            ? (string) ($group['sms_senders'][0] ?? '?')
            : (string) ($group['signal_identifier'] ?? '?');
    }

    /**
     * @param  array<string, mixed>  $group
     */
    private function deriveName(array $group): string
    {
        $tag = trim((string) $group['tag']);
        $identifier = $group['signal_type'] === 'sms'
            ? (string) ($group['sms_senders'][0] ?? '')
            : (string) ($group['signal_identifier'] ?? '');

        // projects has UNIQUE(tenant_id, name): несколько групп с одинаковым тегом
        // («КРК» приходит на десятки разных телефонов) обязаны иметь разные имена.
        // Поэтому комбинируем тег + идентификатор. «РФ» — placeholder тега, не часть имени.
        $tagPart = ($tag !== '' && $tag !== 'РФ') ? $tag : '';
        if ($tagPart !== '' && $identifier !== '') {
            $name = $tagPart.' · '.$identifier;
        } elseif ($tagPart !== '') {
            $name = $tagPart;
        } elseif ($identifier !== '') {
            $name = $identifier;
        } else {
            $name = 'проект';
        }

        return mb_substr($name, 0, 255);
    }
}
