<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Models\SystemSetting;
use App\Services\MonthlyPartitionManager;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;

/**
 * Удаляет устаревшие месячные партиции согласно retention-настройкам.
 *
 * Retention для каждой таблицы хранится в system_settings:
 *   key = 'partition_retention_months_<table>'
 *   value = количество месяцев (integer >= 1)
 *
 * Защита от опасных значений:
 *   - NULL / отсутствие ключа → пропустить таблицу (не дропать ничего)
 *   - 0 → пропустить (запрет стирания всего)
 *   - < 0 → пропустить
 *   - Минимальное значение, принятое к выполнению: 1 месяц
 *
 * Формат имени партиции: <table>_y<YYYY>_m<MM>
 * Партиция считается устаревшей, если её месяц < (текущий месяц − retention).
 *
 * Пример:
 *   сейчас = 2026-05, retention = 3
 *   cutoff  = 2026-02 (включительно; т.е. 2026-01 и старее — дропать)
 *   будет удалена: auth_log_y2026_m01, auth_log_y2025_m12, …
 *   НЕ будет удалена: auth_log_y2026_m02 (граница), и всё новее
 *
 * Hole #2, 23.05.2026.
 */
class PartitionsDropExpired extends Command
{
    /** @var string */
    protected $signature = 'partitions:drop-expired
                            {--dry-run : Перечислить партиции для удаления, не удалять}';

    /** @var string */
    protected $description = 'Удаляет устаревшие месячные партиции согласно system_settings (partition_retention_months_*)';

    public function handle(MonthlyPartitionManager $manager): int
    {
        $isDryRun = (bool) $this->option('dry-run');

        if ($isDryRun) {
            $this->line('<fg=yellow>Dry-run: партиции будут перечислены, но NOT удалены.</>');
        }

        $now = Carbon::now()->startOfMonth();
        $totalDropped = 0;
        $totalSkipped = 0;

        foreach (array_keys(MonthlyPartitionManager::PARTITIONED_TABLES) as $table) {
            $retention = $this->resolveRetention($table);

            if ($retention === null) {
                $this->line("  <fg=gray>skip</> {$table}: retention not configured");

                continue;
            }

            $partitions = $manager->listPartitions($table);

            if (empty($partitions)) {
                $this->line("  <fg=gray>skip</> {$table}: no partitions exist yet");

                continue;
            }

            $dropped = 0;

            foreach ($partitions as $partitionName) {
                $monthStart = $this->parsePartitionMonth($partitionName);

                if ($monthStart === null) {
                    // Имя не соответствует формату _yYYYY_mMM — не трогать (безопасность)
                    $this->warn("  ? {$partitionName}: unrecognised name format, skipping");
                    $totalSkipped++;

                    continue;
                }

                // Граница: всё строго старее (now - retention месяцев) — удалять.
                // Т.е. monthStart < cutoff, где cutoff = now - retention.
                $cutoff = $now->copy()->subMonths($retention);

                if (! $monthStart->lessThan($cutoff)) {
                    // Партиция ещё в пределах retention — оставить
                    continue;
                }

                if ($isDryRun) {
                    $this->line("  <fg=yellow>[dry-run] would drop</> {$partitionName}");
                } else {
                    $this->dropPartition($partitionName);
                    $this->line("  <fg=red>dropped</> {$partitionName}");
                }

                $dropped++;
                $totalDropped++;
            }

            if ($dropped === 0) {
                $this->line("  <fg=green>ok</> {$table}: all partitions within retention={$retention}mo");
            }
        }

        $this->newLine();

        if ($isDryRun) {
            $this->info("Dry-run complete: {$totalDropped} would be dropped, {$totalSkipped} skipped (unrecognised name).");
        } else {
            $this->info("Done: {$totalDropped} dropped, {$totalSkipped} skipped (unrecognised name).");
        }

        return self::SUCCESS;
    }

    /**
     * Читает retention для таблицы из system_settings.
     * Возвращает null, если настройка отсутствует или небезопасна (0 / отрицательная).
     */
    private function resolveRetention(string $table): ?int
    {
        $key = "partition_retention_months_{$table}";

        $setting = SystemSetting::find($key);

        if ($setting === null) {
            return null;
        }

        $value = (int) $setting->value;

        if ($value < 1) {
            // 0 или отрицательное — блокируем, не дропаем ничего
            $this->warn("  ! {$table}: retention value={$value} is invalid (<1), skipping");

            return null;
        }

        return $value;
    }

    /**
     * Парсит имя партиции вида <anything>_y<YYYY>_m<MM> и возвращает Carbon начала месяца.
     * Возвращает null, если имя не соответствует формату.
     */
    private function parsePartitionMonth(string $partitionName): ?Carbon
    {
        // Pattern: ends with _yYYYY_mMM (e.g. auth_log_y2026_m05)
        if (! preg_match('/_y(\d{4})_m(\d{2})$/', $partitionName, $m)) {
            return null;
        }

        $year = (int) $m[1];
        $month = (int) $m[2];

        if ($month < 1 || $month > 12) {
            return null;
        }

        return Carbon::create($year, $month, 1, 0, 0, 0)->startOfMonth();
    }

    /**
     * Удаляет партицию (DETACH + DROP TABLE).
     *
     * Безопасность: имя проверено регекспом в parsePartitionMonth
     * (только символы \w и _ — SQL injection невозможен).
     */
    private function dropPartition(string $partitionName): void
    {
        // DROP требует владения родителем — крутится через pgsql_supplier
        // (crm_supplier_worker — член владельца crm_migrator). См.
        // MonthlyPartitionManager::DDL_CONNECTION.
        DB::connection(MonthlyPartitionManager::DDL_CONNECTION)
            ->statement("DROP TABLE IF EXISTS {$partitionName}");
    }
}
