<?php

declare(strict_types=1);

namespace App\Models;

use App\Casts\PostgresIntArray;
use Carbon\CarbonInterface;
use Database\Factories\ProjectFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Collection;

/**
 * Проект (лид-канал) внутри тенанта.
 *
 * Tenant-aware модель с RLS: SELECT/INSERT/UPDATE/DELETE фильтруются
 * политикой `tenant_isolation` по `current_setting('app.current_tenant_id')`.
 *
 * Расширен в Plan 1/5 Task 1+10 для supplier integration: signal_type/identifier,
 * sms_senders/keyword, delivered_in_month, supplier_b{1,2,3}_project_id (FK на
 * SupplierProject — sharing-model между tenant'ами).
 *
 * Spec: docs/superpowers/specs/2026-05-10-supplier-integration-design.md §2.1
 * Источник: db/schema.sql v8.16 §4, table `projects`.
 *
 * @mixin IdeHelperProject
 */
class Project extends Model
{
    /** @use HasFactory<ProjectFactory> */
    use HasFactory;

    protected $fillable = [
        'tenant_id',
        'name',
        'tag',
        'type',
        'is_active',
        'paused_at',
        'daily_limit_target',
        'effective_daily_limit_today',
        'effective_limit_calculated_at',
        'region_mask',
        'region_mode',
        // Plan 6 (schema v8.20): Subject-level regions array (89 codes из resources/js/constants/regions.ts).
        // Источник истины с Plan 6+; region_mask/region_mode — DEPRECATED (Plan 6.5 cleanup).
        'regions',
        'delivery_days_mask',
        'assignment_strategy',
        'ttfr_target_minutes',
        // Supplier integration (Plan 1/5 Task 1+10):
        'signal_type',
        'signal_identifier',
        'sms_senders',
        'sms_keyword',
        'delivered_in_month',
        'supplier_b1_project_id',
        'supplier_b2_project_id',
        'supplier_b3_project_id',
        // Plan 2/5 Task 1 (schema v8.18): дневной счётчик доставленных лидов
        // (сбрасывается cron'ом в 00:00 МСК, используется LeadRouter'ом).
        'delivered_today',
        // Billing v2 Spec C: флаг точечной блокировки проекта по преfflight (NULL = не заблокирован).
        'preflight_blocked_at',
    ];

    protected function casts(): array
    {
        return [
            'is_active' => 'boolean',
            'paused_at' => 'datetime',
            'daily_limit_target' => 'integer',
            'effective_daily_limit_today' => 'integer',
            'region_mask' => 'integer',
            // Plan 6: Subject-level regions array (89 codes). Используется кастомный
            // PostgresIntArray cast — Laravel stock 'array' посылает JSON `[1,2,3]`,
            // что Postgres отвергает на INT[] (ожидает literal `{1,2,3}`).
            'regions' => PostgresIntArray::class,
            'delivery_days_mask' => 'integer',
            'ttfr_target_minutes' => 'integer',
            'effective_limit_calculated_at' => 'datetime',
            // Billing v2 Spec C: флаг преfflight-блокировки проекта.
            'preflight_blocked_at' => 'datetime',
            'created_at' => 'datetime',
            'updated_at' => 'datetime',
            // Supplier integration:
            'sms_senders' => 'array',
            'delivered_in_month' => 'integer',
            'delivered_today' => 'integer',
        ];
    }

    /** @return BelongsTo<Tenant, $this> */
    public function tenant(): BelongsTo
    {
        return $this->belongsTo(Tenant::class);
    }

    /** @return BelongsTo<SupplierProject, $this> */
    public function supplierB1(): BelongsTo
    {
        return $this->belongsTo(SupplierProject::class, 'supplier_b1_project_id');
    }

    /** @return BelongsTo<SupplierProject, $this> */
    public function supplierB2(): BelongsTo
    {
        return $this->belongsTo(SupplierProject::class, 'supplier_b2_project_id');
    }

    /** @return BelongsTo<SupplierProject, $this> */
    public function supplierB3(): BelongsTo
    {
        return $this->belongsTo(SupplierProject::class, 'supplier_b3_project_id');
    }

    /**
     * @return BelongsToMany<SupplierProject, $this>
     */
    public function supplierProjects(): BelongsToMany
    {
        return $this->belongsToMany(SupplierProject::class, 'project_supplier_links')
            ->withPivot(['platform', 'subject_code']);
    }

    /**
     * Активные проекты, у которых сегодняшний день включён в delivery_days_mask.
     *
     * delivery_days_mask — битовая маска: bit 0 = понедельник, bit 6 = воскресенье
     * (ISO day-of-week минус 1). Для ISO=1 (Mon) = 1<<0 = 1; для ISO=7 (Sun) = 1<<6 = 64.
     *
     * @param  Builder<Project>  $query
     * @return Builder<Project>
     */
    public function scopeActiveOnDay(Builder $query, int $isoDayOfWeek): Builder
    {
        $bit = 1 << ($isoDayOfWeek - 1);

        return $query->where('is_active', true)
            ->whereRaw('(delivery_days_mask & ?) <> 0', [$bit]);
    }

    /**
     * @param  Builder<Project>  $query
     * @return Builder<Project>
     */
    public function scopeForSignal(Builder $query, string $signalType, string $identifier): Builder
    {
        return $query->where('signal_type', $signalType)->where('signal_identifier', $identifier);
    }

    /**
     * Все связанные SupplierProject из eager-loaded BelongsTo отношений.
     *
     * Используется внутри aggregateSyncStatus(), aggregateLastSyncedAt(),
     * getSupplierLinks() — устраняет N+1 (каждый из трёх методов вызывал
     * SupplierProject::find() независимо; теперь читает из уже загруженных
     * $this->supplierB1 / supplierB2 / supplierB3).
     *
     * Требует eager-load: Project::with(['supplierB1', 'supplierB2', 'supplierB3']).
     *
     * @return Collection<int, SupplierProject>
     */
    private function resolvedSupplierProjects(): Collection
    {
        return collect([$this->supplierB1, $this->supplierB2, $this->supplierB3])->filter()->values();
    }

    /**
     * Агрегированный статус синхронизации по всем связанным SupplierProject.
     *
     * Логика: если нет ни одного — pending; если есть failed — failed;
     * если есть pending — pending; иначе — ok.
     *
     * Читает из eager-loaded отношений (см. resolvedSupplierProjects()).
     */
    public function aggregateSyncStatus(): string
    {
        $statuses = $this->resolvedSupplierProjects()->pluck('sync_status');

        if ($statuses->isEmpty()) {
            return 'pending';
        }
        if ($statuses->contains('failed')) {
            return 'failed';
        }
        if ($statuses->contains('pending')) {
            return 'pending';
        }

        return 'ok';
    }

    /**
     * Минимальная дата последней синхронизации по всем связанным SupplierProject.
     *
     * Использует sortBy по timestamp вместо Collection::min() на Carbon-объектах
     * (min() сравнивает строковое представление, что ненадёжно для Carbon).
     *
     * Читает из eager-loaded отношений (см. resolvedSupplierProjects()).
     */
    public function aggregateLastSyncedAt(): ?string
    {
        $ts = $this->resolvedSupplierProjects()
            ->pluck('last_synced_at')
            ->filter()
            ->sortBy(fn (CarbonInterface $c) => $c->timestamp)
            ->first();

        return $ts?->toIso8601String();
    }

    /**
     * Массив ссылок на связанные SupplierProject (для show endpoint).
     *
     * Читает из eager-loaded отношений (см. resolvedSupplierProjects()).
     *
     * @return array<int, array{platform: string, supplier_project_id: int, sync_status: string|null, last_synced_at: string|null}>
     */
    public function getSupplierLinks(): array
    {
        return collect(['b1' => $this->supplierB1, 'b2' => $this->supplierB2, 'b3' => $this->supplierB3])
            ->filter()
            ->map(fn (SupplierProject $sp, string $platform) => [
                'platform' => $platform,
                'supplier_project_id' => $sp->id,
                'sync_status' => $sp->sync_status,
                'last_synced_at' => $sp->last_synced_at?->toIso8601String(),
            ])
            ->values()
            ->all();
    }
}
