<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Jobs\GenerateReportJob;
use App\Models\ReportJob;
use App\Models\User;
use App\Services\Pd\PdAuditLogger;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use Illuminate\Validation\Rule;
use Symfony\Component\HttpFoundation\Response;

/**
 * Reports API (schema §13.5). Все endpoint'ы под `auth:sanctum`.
 *
 * Квота CTO-7: на тенант не более 3 одновременных pending+processing
 * (DoS-защита). Превышение → 422 «лимит исчерпан».
 *
 * Retry — CTO-6: 3 попытки на исходную задачу, окно 7 дней, только owner.
 * Cancel — только pending (running выполняется sync на dev).
 * Delete — terminal jobs (done|failed), также удаляет файл из storage.
 * CTO-10: по истечении expires_at файл удаляется cron'ом
 * `reports:cleanup-expired`, status='done' остаётся, file_path=NULL.
 */
class ReportJobController extends Controller
{
    public const MAX_ACTIVE_JOBS_PER_TENANT = 3;

    public const RETRY_MAX_ATTEMPTS = 3;

    public const RETRY_WINDOW_DAYS = 7;

    /**
     * GET /api/reports/jobs?status=&limit=&offset=
     */
    public function index(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'status' => ['nullable', 'string', Rule::in([
                ReportJob::STATUS_PENDING,
                ReportJob::STATUS_PROCESSING,
                ReportJob::STATUS_DONE,
                ReportJob::STATUS_FAILED,
            ])],
            'limit' => 'nullable|integer|min:1|max:200',
            'offset' => 'nullable|integer|min:0',
        ]);

        /** @var User $user */
        $user = $request->user();
        $limit = (int) ($validated['limit'] ?? 50);
        $offset = (int) ($validated['offset'] ?? 0);

        return DB::transaction(function () use ($user, $validated, $limit, $offset): JsonResponse {
            DB::statement('SET LOCAL app.current_tenant_id = '.(int) $user->tenant_id);

            $query = ReportJob::query()->where('tenant_id', $user->tenant_id);

            if (isset($validated['status'])) {
                $query->where('status', $validated['status']);
            }

            $total = (clone $query)->count();
            $items = $query->orderByDesc('created_at')->orderByDesc('id')
                ->limit($limit)->offset($offset)->get();

            $base = ReportJob::query()->where('tenant_id', $user->tenant_id);
            $counts = [
                'pending' => (clone $base)->where('status', ReportJob::STATUS_PENDING)->count(),
                'processing' => (clone $base)->where('status', ReportJob::STATUS_PROCESSING)->count(),
                'done' => (clone $base)->where('status', ReportJob::STATUS_DONE)->count(),
                'failed' => (clone $base)->where('status', ReportJob::STATUS_FAILED)->count(),
            ];

            return response()->json([
                'jobs' => $items->map(fn (ReportJob $j) => $this->toResource($j))->all(),
                'total' => $total,
                'limit' => $limit,
                'offset' => $offset,
                'counts' => $counts,
                'quota' => [
                    'active' => $counts['pending'] + $counts['processing'],
                    'max_active' => self::MAX_ACTIVE_JOBS_PER_TENANT,
                ],
            ]);
        });
    }

    /**
     * GET /api/reports/jobs/{id}
     */
    public function show(Request $request, int $id): JsonResponse
    {
        /** @var User $user */
        $user = $request->user();

        return DB::transaction(function () use ($user, $id): JsonResponse {
            DB::statement('SET LOCAL app.current_tenant_id = '.(int) $user->tenant_id);

            $job = ReportJob::query()
                ->where('id', $id)
                ->where('tenant_id', $user->tenant_id)
                ->first();

            if ($job === null) {
                return response()->json(['message' => 'Отчёт не найден.'], 404);
            }

            return response()->json(['job' => $this->toResource($job)]);
        });
    }

    /**
     * POST /api/reports/jobs {type, format, parameters: {...}}
     */
    public function store(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'type' => ['required', 'string', Rule::in(ReportJob::TYPES)],
            'format' => ['required', 'string', Rule::in(ReportJob::FORMATS)],
            'parameters' => 'required|array',
            'parameters.date_from' => 'required|date',
            'parameters.date_to' => 'required|date|after_or_equal:parameters.date_from',
            'parameters.project_id' => 'nullable|integer|min:1',
            'parameters.manager_id' => 'nullable|integer|min:1',
        ]);

        /** @var User $user */
        $user = $request->user();

        return DB::transaction(function () use ($user, $validated): JsonResponse {
            DB::statement('SET LOCAL app.current_tenant_id = '.(int) $user->tenant_id);

            // CTO-7: квота 3 одновременных pending+processing на тенант.
            $activeCount = ReportJob::query()
                ->where('tenant_id', $user->tenant_id)
                ->whereIn('status', ReportJob::ACTIVE_STATUSES)
                ->count();

            if ($activeCount >= self::MAX_ACTIVE_JOBS_PER_TENANT) {
                return response()->json([
                    'message' => 'Лимит активных отчётов исчерпан.',
                    'errors' => ['_quota' => [
                        sprintf(
                            'Максимум %d одновременных отчётов на тенант. Дождитесь завершения или удалите ненужные.',
                            self::MAX_ACTIVE_JOBS_PER_TENANT
                        ),
                    ]],
                ], 422);
            }

            $job = ReportJob::create([
                'tenant_id' => $user->tenant_id,
                'user_id' => $user->id,
                'type' => $validated['type'],
                'parameters' => [
                    'format' => $validated['format'],
                    'date_from' => $validated['parameters']['date_from'],
                    'date_to' => $validated['parameters']['date_to'],
                    'project_id' => $validated['parameters']['project_id'] ?? null,
                    'manager_id' => $validated['parameters']['manager_id'] ?? null,
                ],
                'status' => ReportJob::STATUS_PENDING,
            ]);

            // Sync queue на dev — Job выполняется немедленно.
            // На prod queue.driver=redis/database — async через worker.
            GenerateReportJob::dispatch($job->id, (int) $user->tenant_id);

            return response()->json([
                'job' => $this->toResource($job->fresh()),
            ], 201);
        });
    }

    /**
     * POST /api/reports/jobs/{id}/retry — CTO-6: 3 попытки за 7 дней, только owner.
     * Создаёт НОВЫЙ ReportJob (parameters.retry_count = old + 1) + dispatch.
     * Старый failed-job остаётся для истории.
     */
    public function retry(Request $request, int $id): JsonResponse
    {
        /** @var User $user */
        $user = $request->user();

        return DB::transaction(function () use ($user, $id): JsonResponse {
            DB::statement('SET LOCAL app.current_tenant_id = '.(int) $user->tenant_id);

            $original = ReportJob::query()
                ->where('id', $id)
                ->where('tenant_id', $user->tenant_id)
                ->first();

            if ($original === null) {
                return response()->json(['message' => 'Отчёт не найден.'], 404);
            }

            if ($original->user_id !== $user->id) {
                return response()->json(['message' => 'Повторно запустить может только владелец отчёта.'], 403);
            }

            if ($original->status !== ReportJob::STATUS_FAILED) {
                return response()->json([
                    'message' => 'Повтор доступен только для отчётов со статусом «failed».',
                    'errors' => ['status' => ['Текущий статус: '.$original->status]],
                ], 422);
            }

            $retryCount = (int) ($original->parameters['retry_count'] ?? 0);
            if ($retryCount >= self::RETRY_MAX_ATTEMPTS - 1) {
                return response()->json([
                    'message' => 'Достигнут максимум попыток.',
                    'errors' => ['_retry' => [sprintf('Максимум %d попыток (CTO-6).', self::RETRY_MAX_ATTEMPTS)]],
                ], 422);
            }

            $cutoff = Carbon::now()->subDays(self::RETRY_WINDOW_DAYS);
            if ($original->created_at !== null && $original->created_at->lt($cutoff)) {
                return response()->json([
                    'message' => 'Окно повторов истекло.',
                    'errors' => ['_retry' => [sprintf('Повтор доступен в течение %d дней с создания.', self::RETRY_WINDOW_DAYS)]],
                ], 422);
            }

            // Квота тоже учитывается для retry — иначе можно «обойти» CTO-7 через retry-spam.
            $activeCount = ReportJob::query()
                ->where('tenant_id', $user->tenant_id)
                ->whereIn('status', ReportJob::ACTIVE_STATUSES)
                ->count();
            if ($activeCount >= self::MAX_ACTIVE_JOBS_PER_TENANT) {
                return response()->json([
                    'message' => 'Лимит активных отчётов исчерпан.',
                    'errors' => ['_quota' => [sprintf('Максимум %d одновременных отчётов на тенант.', self::MAX_ACTIVE_JOBS_PER_TENANT)]],
                ], 422);
            }

            $params = $original->parameters ?? [];
            $params['retry_count'] = $retryCount + 1;
            $params['retry_of'] = $original->id;

            $newJob = ReportJob::create([
                'tenant_id' => $user->tenant_id,
                'user_id' => $user->id,
                'type' => $original->type,
                'parameters' => $params,
                'status' => ReportJob::STATUS_PENDING,
            ]);

            GenerateReportJob::dispatch($newJob->id, (int) $user->tenant_id);

            return response()->json([
                'job' => $this->toResource($newJob->fresh()),
            ], 201);
        });
    }

    /**
     * POST /api/reports/jobs/{id}/cancel — отменить pending (running уже работает).
     */
    public function cancel(Request $request, int $id): JsonResponse
    {
        /** @var User $user */
        $user = $request->user();

        return DB::transaction(function () use ($user, $id): JsonResponse {
            DB::statement('SET LOCAL app.current_tenant_id = '.(int) $user->tenant_id);

            $job = ReportJob::query()
                ->where('id', $id)
                ->where('tenant_id', $user->tenant_id)
                ->first();

            if ($job === null) {
                return response()->json(['message' => 'Отчёт не найден.'], 404);
            }

            if ($job->user_id !== $user->id) {
                return response()->json(['message' => 'Отменить может только владелец отчёта.'], 403);
            }

            if ($job->status !== ReportJob::STATUS_PENDING) {
                return response()->json([
                    'message' => 'Отменить можно только pending-задачу.',
                    'errors' => ['status' => ['Текущий статус: '.$job->status]],
                ], 422);
            }

            $job->update([
                'status' => ReportJob::STATUS_FAILED,
                'error_message' => 'Отменено пользователем.',
                'finished_at' => Carbon::now(),
            ]);

            return response()->json(['job' => $this->toResource($job->fresh())]);
        });
    }

    /**
     * DELETE /api/reports/jobs/{id} — удалить terminal job + файл.
     */
    public function destroy(Request $request, int $id, PdAuditLogger $pdLog): JsonResponse
    {
        /** @var User $user */
        $user = $request->user();

        return DB::transaction(function () use ($user, $id, $request, $pdLog): JsonResponse {
            DB::statement('SET LOCAL app.current_tenant_id = '.(int) $user->tenant_id);

            $job = ReportJob::query()
                ->where('id', $id)
                ->where('tenant_id', $user->tenant_id)
                ->first();

            if ($job === null) {
                return response()->json(['message' => 'Отчёт не найден.'], 404);
            }

            if ($job->user_id !== $user->id) {
                return response()->json(['message' => 'Удалить может только владелец отчёта.'], 403);
            }

            if (! in_array($job->status, ReportJob::TERMINAL_STATUSES, true)) {
                return response()->json([
                    'message' => 'Удалить можно только завершённую задачу (done|failed).',
                    'errors' => ['status' => ['Текущий статус: '.$job->status]],
                ], 422);
            }

            if ($job->file_path !== null) {
                Storage::disk('local')->delete($job->file_path);
                $pdLog->record(
                    action: 'deleted',
                    subjectType: 'lead',
                    subjectId: null,
                    purpose: 'report_file_'.$job->id,
                    tenantId: (int) $job->tenant_id,
                    actorTenantUserId: (int) $user->id,
                    actorAdminUserId: null,
                    ip: $request->ip(),
                );
            }
            $job->delete();

            return response()->json(['message' => 'Удалено.']);
        });
    }

    /**
     * GET /api/reports/jobs/{id}/file?tenant=&expires=&signature= — скачать
     * готовый файл отчёта (F2, OPEN-И-20).
     *
     * Под `signed`-middleware (не auth:sanctum): подпись URL = capability-token.
     * `tenant` в подписи нужен для RLS-контекста (нет авторизованного user'а).
     * Подпись покрывает все query-параметры — `tenant`/`id` подделать нельзя.
     */
    public function download(Request $request, int $id): Response
    {
        $tenantId = (int) $request->query('tenant', '0');

        return DB::transaction(function () use ($id, $tenantId): Response {
            DB::statement('SET LOCAL app.current_tenant_id = '.$tenantId);

            $job = ReportJob::query()
                ->where('id', $id)
                ->where('tenant_id', $tenantId)
                ->first();

            if ($job === null) {
                return response()->json(['message' => 'Отчёт не найден.'], 404);
            }

            if ($job->status !== ReportJob::STATUS_DONE
                || $job->file_path === null
                || ($job->expires_at !== null && $job->expires_at->isPast())) {
                return response()->json(['message' => 'Файл отчёта недоступен или истёк.'], 410);
            }

            if (! Storage::disk('local')->exists($job->file_path)) {
                return response()->json(['message' => 'Файл отчёта не найден в хранилище.'], 404);
            }

            $extension = pathinfo($job->file_path, PATHINFO_EXTENSION);

            return Storage::disk('local')->download(
                $job->file_path,
                sprintf('report-%d.%s', $job->id, $extension)
            );
        });
    }

    /**
     * Signed URL (24 ч) на скачивание файла. NULL для не-готовых job'ов или
     * после истечения retention (file_path обнулён cron'ом reports:cleanup-expired).
     */
    private function downloadUrl(ReportJob $job): ?string
    {
        if ($job->status !== ReportJob::STATUS_DONE
            || $job->file_path === null
            || ($job->expires_at !== null && $job->expires_at->isPast())) {
            return null;
        }

        return URL::temporarySignedRoute(
            'reports.download',
            Carbon::now()->addHours(24),
            ['id' => $job->id, 'tenant' => $job->tenant_id],
        );
    }

    /** @return array<string, mixed> */
    private function toResource(ReportJob $job): array
    {
        return [
            'id' => $job->id,
            'type' => $job->type,
            'parameters' => $job->parameters,
            'status' => $job->status,
            'file_path' => $job->file_path,
            'file_size' => $job->file_size,
            'generation_seconds' => $job->generation_seconds,
            'error_message' => $job->error_message,
            'created_at' => $job->created_at?->toIso8601String(),
            'finished_at' => $job->finished_at?->toIso8601String(),
            'expires_at' => $job->expires_at?->toIso8601String(),
            'is_expired' => $job->expires_at !== null && $job->expires_at->isPast(),
            'retry_count' => (int) ($job->parameters['retry_count'] ?? 0),
            'retry_max' => self::RETRY_MAX_ATTEMPTS,
            'download_url' => $this->downloadUrl($job),
        ];
    }
}
