<?php

declare(strict_types=1);

namespace App\Jobs;

use App\Models\ReportJob;
use App\Services\Reports\ReportGeneratorRegistry;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Throwable;

/**
 * Обёртка над генерацией отчёта. Меняет статус pending → processing → done|failed,
 * пишет file_path/file_size/generation_seconds.
 *
 * `tries=1` — retry-логика custom (CTO-6: 3 попытки за 7 дней через UI кнопку
 * "Повторить", не auto-retry queue worker'ом — иначе квота CTO-7 размывается).
 *
 * `expires_at` = NOW + 30 дней (default; при необходимости вынесем в
 * system_settings.report_retention_days).
 */
class GenerateReportJob implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    public int $tries = 1;

    public function __construct(
        public readonly int $reportJobId,
        public readonly int $tenantId,
    ) {}

    public function handle(ReportGeneratorRegistry $registry): void
    {
        // SET LOCAL inside a transaction establishes the tenant GUC for the
        // duration of this block — required by RLS on report_jobs for
        // crm_app_user (non-BYPASSRLS) on production.
        DB::transaction(function () use ($registry): void {
            DB::statement('SET LOCAL app.current_tenant_id = '.$this->tenantId);

            $job = ReportJob::query()->find($this->reportJobId);
            if ($job === null) {
                Log::warning('GenerateReportJob: report_job not found', ['id' => $this->reportJobId]);

                return;
            }

            if (! in_array($job->status, ReportJob::ACTIVE_STATUSES, true)) {
                // Уже terminal — повторный dispatch (например, Horizon retry) пропускаем.
                return;
            }

            $job->update(['status' => ReportJob::STATUS_PROCESSING]);

            $startedAt = microtime(true);
            try {
                $params = $job->parameters ?? [];
                $format = (string) ($params['format'] ?? 'csv');

                if (! $registry->isSupported($job->type, $format)) {
                    $this->markFailed($job, "Неподдерживаемая комбинация: {$job->type}/{$format}", $startedAt);

                    return;
                }

                $provider = $registry->provider($job->type);
                $formatter = $registry->formatter($format);

                $headers = $provider->headers();
                $rows = $provider->rows($job);
                $content = $formatter->format($headers, $rows);

                $relativePath = sprintf(
                    'reports/%d/%d.%s',
                    $job->tenant_id,
                    $job->id,
                    $formatter->fileExtension()
                );
                Storage::disk('local')->put($relativePath, $content);

                $job->update([
                    'status' => ReportJob::STATUS_DONE,
                    'file_path' => $relativePath,
                    'file_size' => strlen($content),
                    'generation_seconds' => max(1, (int) round(microtime(true) - $startedAt)),
                    'finished_at' => Carbon::now(),
                    'expires_at' => Carbon::now()->addDays(30),
                ]);
            } catch (Throwable $e) {
                $this->markFailed($job, mb_substr($e->getMessage(), 0, 1000), $startedAt);
                Log::error('GenerateReportJob failed', [
                    'id' => $this->reportJobId,
                    'exception' => $e,
                ]);
            }
        });
    }

    private function markFailed(ReportJob $job, string $message, float $startedAt): void
    {
        $job->update([
            'status' => ReportJob::STATUS_FAILED,
            'error_message' => $message,
            'generation_seconds' => max(1, (int) round(microtime(true) - $startedAt)),
            'finished_at' => Carbon::now(),
        ]);
    }
}
