<?php

declare(strict_types=1);

namespace App\Services\Project;

use App\Models\Project;
use App\Support\SystemSettings;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Facades\DB;

/**
 * Защита проекта от удаления/смены источника, пока поставщик crm.bp-gr.ru
 * может прислать по нему лиды по уже сделанному слепку.
 *
 * Slepok-час поставщика: 21:00 МСК (поставщик в 21:00 формирует заказ на завтра).
 * Grace: до следующего 21:00 МСК после pause + 24h на доставку хвоста.
 *
 * Spec: docs/superpowers/plans/2026-05-26-supplier-snapshot-guard.md
 */
class SupplierSnapshotGuard
{
    /** Час МСК, в который поставщик заказывает лиды на следующий день. */
    public const SUPPLIER_ORDER_HOUR_MSK = 21;

    /** Сколько часов после слепка летит хвост лидов (одни сутки). */
    public const TAIL_DELIVERY_HOURS = 24;

    public function computeGraceUntil(CarbonInterface $pausedAt): CarbonImmutable
    {
        $pausedMsk = CarbonImmutable::instance($pausedAt)->setTimezone('Europe/Moscow');

        $next21 = $pausedMsk->setTime(self::SUPPLIER_ORDER_HOUR_MSK, 0, 0);
        if ($pausedMsk->gte($next21)) {
            $next21 = $next21->addDay();
        }

        return $next21->addHours(self::TAIL_DELIVERY_HOURS);
    }

    public function isProtected(Project $project, ?CarbonImmutable $now = null): bool
    {
        $hasLinks = DB::table('project_supplier_links')
            ->where('project_id', $project->id)
            ->exists();
        if (! $hasLinks) {
            return false;
        }

        if ($project->is_active) {
            return true;
        }

        if ($project->paused_at === null) {
            return false;
        }

        $graceUntil = $this->computeGraceUntil($project->paused_at);
        $effectiveNow = $now ?? CarbonImmutable::now('Europe/Moscow');

        return $effectiveNow->lt($graceUntil);
    }

    /**
     * Slepok-sensitive поля проекта — изменения этих полей попадают в slepok №NЛ
     * (фиксируется в 18:00 МСК) и начинают действовать с N.21:00 МСК.
     *
     * Spec §4.2.5 — Task 2.7.
     */
    public const SLEPOK_SENSITIVE_FIELDS = [
        'is_active',
        'daily_limit_target',
        'delivery_days_mask',
        'regions',
        'signal_identifier',
        'sms_senders',
        'sms_keyword',
    ];

    /**
     * Возвращает момент, с которого правка `$field` вступит в силу:
     *   правка до 18:00 МСК → сегодня в 21:00 МСК;
     *   правка с 18:00 МСК и позже → завтра в 21:00 МСК.
     *
     * Возвращает null когда правка применяется немедленно:
     *   поле не slepok-sensitive (см. SLEPOK_SENSITIVE_FIELDS), либо
     *   проект не связан с поставщиком (нет project_supplier_links → нет slepok-риска).
     *
     * Используется ProjectService (Task 2.8) для прикрепления к UI-ответу
     * метки «изменения вступят в силу с DD.MM HH:MM».
     *
     * Spec §4.2.5.
     */
    public function appliesFrom(Project $project, string $field): ?CarbonImmutable
    {
        if (! in_array($field, self::SLEPOK_SENSITIVE_FIELDS, true)) {
            return null;
        }

        $hasLinks = DB::table('project_supplier_links')
            ->where('project_id', $project->id)
            ->exists();
        if (! $hasLinks) {
            return null;
        }

        $nowMsk = CarbonImmutable::now('Europe/Moscow');
        $todayCutoff = $nowMsk->setTime(18, 0, 0);

        if ($nowMsk->gte($todayCutoff)) {
            return $nowMsk->addDay()->setTime(21, 0, 0);
        }

        return $nowMsk->setTime(21, 0, 0);
    }

    /**
     * @param  'delete'|'change_source'  $action
     */
    public function assertCanMutateSource(Project $project, string $action): void
    {
        if (! $this->isProtected($project)) {
            return;
        }

        // Путь A (ADR 2026-06-25): при включённом матче по слепку смена источника безопасна —
        // раздача доводит хвост старого источника по слепку. Жёсткий запрет остаётся для delete
        // и для случая, когда новый матч ещё не включён (флаг routing_match_by_snapshot выкл).
        if ($action === 'change_source' && SystemSettings::bool('routing_match_by_snapshot', false)) {
            return;
        }

        // Time-aware: дата окончания хвоста считается по слепку (computeGraceUntil),
        // а не захардкожена «послезавтра»/«сегодня в 18:00» — иначе текст врёт после 18:00.
        $verb = $action === 'delete' ? 'Удалить' : 'Изменить источник';
        $graceDate = (new ProjectRuleMessages)->formatDate(
            $this->computeGraceUntil(CarbonImmutable::now('Europe/Moscow'))
        );
        $message = 'Мы уже начали сбор лидов по этому проекту — заказанные у поставщика лиды '
            .'будут приходить примерно до '.$graceDate.'. Чтобы остановить раньше, поставьте проект '
            .'на паузу: новые сборы прекратятся, а уже заказанные лиды дойдут. '
            .$verb.' можно будет после '.$graceDate.'.';

        throw new HttpResponseException(response()->json([
            'errors' => ['project' => [$message]],
        ], 422));
    }

    /**
     * Pure-вариант isProtected для presentation-слоя: считает состояние блокировки
     * источника БЕЗ DB-запроса (hasLinks передаётся снаружи — eager-loaded count).
     * Логику isProtected/computeGraceUntil не меняет — переиспользует computeGraceUntil.
     *
     * @return array{locked: bool, unlock_at: ?CarbonImmutable, projected: bool}
     *                                                                           projected=true → дата это прогноз «если поставить паузу сейчас» (проект активен).
     */
    public function lockState(
        bool $hasLinks,
        bool $isActive,
        ?CarbonInterface $pausedAt,
        ?CarbonImmutable $now = null,
    ): array {
        $now ??= CarbonImmutable::now('Europe/Moscow');

        if (! $hasLinks) {
            return ['locked' => false, 'unlock_at' => null, 'projected' => false];
        }
        if ($isActive) {
            // Паузы ещё нет — дата это прогноз «если поставить паузу сейчас».
            return ['locked' => true, 'unlock_at' => $this->computeGraceUntil($now), 'projected' => true];
        }
        if ($pausedAt === null) {
            return ['locked' => false, 'unlock_at' => null, 'projected' => false];
        }
        $graceUntil = $this->computeGraceUntil($pausedAt);
        if ($now->lt($graceUntil)) {
            return ['locked' => true, 'unlock_at' => $graceUntil, 'projected' => false];
        }

        return ['locked' => false, 'unlock_at' => null, 'projected' => false];
    }
}
