<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Mail\ImpersonationCodeMail;
use App\Mail\ImpersonationEndedMail;
use App\Models\ImpersonationToken;
use App\Models\Tenant;
use App\Models\User;
use App\Services\Pd\ImpersonationAuditService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;

/**
 * SaaS-admin impersonation flow (ТЗ §22.7 / Ю-1).
 *
 * Flow:
 *   1. Admin → POST /api/admin/impersonation/init {tenant_id, reason}.
 *      Backend генерирует 6-значный код, сохраняет hash в impersonation_tokens,
 *      отправляет plain код на tenant.contact_email. TTL 15 минут.
 *   2. Admin вводит код в админке → POST /api/admin/impersonation/verify {token_id, code}.
 *      Backend проверяет hash, expiry, attempts. На success — token.used_at = NOW(),
 *      возвращает session_id (создаётся отдельно — отдельный коммит).
 *   3. Admin завершает сессию → POST /api/admin/impersonation/end {token_id}.
 *      Backend ставит token.session_ended_at, отправляет email клиенту.
 *
 * NB: на MVP saas_admin_users auth не реализован, `requested_by` принимается
 * параметром. Production: middleware('auth:saas-admin') + auth()->id().
 *
 * Two-person approval (CTO-15 / Ю-9): для тенантов с
 * `pd_subject_request.processing_restricted=TRUE` ИЛИ
 * `chargeback_unrecovered_rub > 0` требуется second_approver_id роли compliance.
 * На MVP не реализовано — отдельный коммит после SaaS-admin auth.
 */
class ImpersonationController extends Controller
{
    private const TOKEN_TTL_MINUTES = 15;

    private const MAX_FAILED_ATTEMPTS = 5;

    private const SESSION_TTL_MINUTES = 60;

    /**
     * SaaS-admin — кросс-тенантная зона: запросы к impersonation_tokens / tenants
     * идут через BYPASSRLS-подключение pgsql_supplier (роль crm_supplier_worker).
     * Иначе на проде (роль crm_app_user, RLS on) без выставленного GUC
     * app.current_tenant_id запрос падает SQLSTATE 42704 — у saas-admin нет
     * tenant-контекста (middleware 'tenant' на /api/admin/* не висит). На dev
     * pgsql_supplier = fallback на postgres-superuser, поведение идентично.
     */
    private const DB_CONNECTION = 'pgsql_supplier';

    /** GET /api/admin/impersonation/active — активные сессии (used_at != null AND session_ended_at == null) */
    public function active(): JsonResponse
    {
        $rows = ImpersonationToken::on(self::DB_CONNECTION)
            ->whereNotNull('used_at')
            ->whereNull('session_ended_at')
            ->with(['tenant'])
            ->orderByDesc('used_at')
            ->limit(100)
            ->get(['id', 'tenant_id', 'requested_by', 'reason', 'sent_to_email', 'used_at', 'expires_at']);

        return response()->json([
            'sessions' => $rows->map(fn (ImpersonationToken $t) => [
                'token_id' => $t->id,
                'tenant_id' => $t->tenant_id,
                'tenant_name' => $t->tenant?->organization_name,
                'requested_by' => $t->requested_by,
                'reason' => $t->reason,
                'sent_to_email' => $t->sent_to_email,
                'used_at' => $t->used_at?->toIso8601String(),
                'expires_at' => $t->expires_at->toIso8601String(),
            ]),
        ]);
    }

    /** GET /api/admin/impersonation/recent — последние 20 завершённых */
    public function recent(): JsonResponse
    {
        $rows = ImpersonationToken::on(self::DB_CONNECTION)
            ->whereNotNull('used_at')
            ->whereNotNull('session_ended_at')
            ->with(['tenant'])
            ->orderByDesc('session_ended_at')
            ->limit(20)
            ->get(['id', 'tenant_id', 'requested_by', 'reason', 'used_at', 'session_ended_at']);

        return response()->json([
            'sessions' => $rows->map(fn (ImpersonationToken $t) => [
                'token_id' => $t->id,
                'tenant_id' => $t->tenant_id,
                'tenant_name' => $t->tenant?->organization_name,
                'requested_by' => $t->requested_by,
                'reason' => $t->reason,
                'used_at' => $t->used_at?->toIso8601String(),
                'session_ended_at' => $t->session_ended_at?->toIso8601String(),
                'duration_seconds' => $t->used_at && $t->session_ended_at
                    ? abs($t->session_ended_at->diffInSeconds($t->used_at))
                    : null,
            ]),
        ]);
    }

    /** POST /api/admin/impersonation/init */
    public function init(Request $request, ImpersonationAuditService $audit): JsonResponse
    {
        $tenantId = (int) $request->input('tenant_id');
        $requestedBy = (int) $request->input('requested_by'); // TODO: $request->user()->id когда saas-admin auth готов
        $reason = $request->string('reason')->toString();

        if (mb_strlen($reason) < 30) {
            return response()->json([
                'message' => 'Основание (reason) должно быть не короче 30 символов.',
                'errors' => ['reason' => ['Минимум 30 символов.']],
            ], 422);
        }

        $tenant = Tenant::on(self::DB_CONNECTION)->find($tenantId);
        if (! $tenant) {
            return response()->json(['message' => 'Тенант не найден.'], 404);
        }

        // 6-значный код. Числа от 100000 до 999999.
        $plainCode = (string) random_int(100_000, 999_999);

        $token = ImpersonationToken::on(self::DB_CONNECTION)->create([
            'tenant_id' => $tenant->id,
            'requested_by' => $requestedBy,
            'code_hash' => Hash::make($plainCode),
            'reason' => $reason,
            'sent_to_email' => (string) $tenant->contact_email,
            'expires_at' => now()->addMinutes(self::TOKEN_TTL_MINUTES),
        ]);

        $audit->recordInit($token, adminId: $requestedBy, ip: $request->ip());

        try {
            Mail::to((string) $tenant->contact_email)
                ->queue(new ImpersonationCodeMail($plainCode, (string) $tenant->contact_email));
        } catch (\Throwable $e) {
            Log::warning('impersonation init: не удалось поставить письмо с кодом: '.$e->getMessage());
        }
        $payload = [
            'token_id' => $token->id,
            'expires_at' => $token->expires_at->toIso8601String(),
            'sent_to_email' => $token->sent_to_email,
        ];

        // Audit-fix A2: plain-код возвращается в API-ответе ТОЛЬКО на dev/testing
        // (для тестов и локальной разработки). На prod код уходит исключительно
        // в email клиента — env-guard исключает захват impersonation-сессии
        // через чтение ответа init.
        if (app()->environment('local', 'testing')) {
            $payload['_dev_plain_code'] = $plainCode;
        }

        return response()->json($payload);
    }

    /** POST /api/admin/impersonation/verify */
    public function verify(Request $request, ImpersonationAuditService $audit): JsonResponse
    {
        $tokenId = (int) $request->input('token_id');
        $code = $request->string('code')->toString();

        $token = ImpersonationToken::on(self::DB_CONNECTION)->find($tokenId);
        if (! $token) {
            return response()->json(['message' => 'Токен не найден.'], 404);
        }

        if (! $token->isUsable()) {
            $reason = $token->isExpired() ? 'expired'
                : ($token->used_at !== null ? 'used'
                : ($token->invalidated_at !== null ? 'invalidated'
                : 'too_many_attempts'));

            return response()->json([
                'message' => 'Токен недействителен.',
                'reason' => $reason,
            ], 422);
        }

        if (! Hash::check($code, $token->code_hash)) {
            // increment атомарен на уровне SQL, а isUsable() независимо гейтит
            // failed_attempts >= 5 — поэтому отдельная транзакция не нужна
            // (и ломала бы общий PDO в тестах под SharesSupplierPdo).
            $token->increment('failed_attempts');
            if ($token->failed_attempts >= self::MAX_FAILED_ATTEMPTS) {
                $token->update(['invalidated_at' => now()]);
            }

            return response()->json([
                'message' => 'Неверный код.',
                'attempts_remaining' => max(0, self::MAX_FAILED_ATTEMPTS - $token->failed_attempts),
            ], 422);
        }

        // Success: целевой пользователь тенанта = самый ранний активный.
        $targetUser = User::on(self::DB_CONNECTION)
            ->where('tenant_id', $token->tenant_id)
            ->where('is_active', true)
            ->orderBy('id')
            ->first();

        if ($targetUser === null) {
            return response()->json(['message' => 'У тенанта нет активного пользователя для входа.'], 422);
        }

        // Машинный ключ для ИИ: lpimp_<id>_<secret>. Храним только хеш секрета.
        $secret = Str::random(48);
        $machineToken = 'lpimp_'.$token->id.'_'.$secret;

        $token->update([
            'used_at' => now(),
            'session_token_hash' => Hash::make($secret),
        ]);

        // Путь человека: логиним браузер целевым пользователем + маркер impersonation в сессию.
        Auth::login($targetUser);
        $request->session()->put('impersonation', [
            'token_id' => $token->id,
            'tenant_id' => $token->tenant_id,
            'target_user_id' => $targetUser->id,
            'started_at' => now()->toIso8601String(),
        ]);

        $audit->recordVerify($token, adminId: (int) $token->requested_by, ip: $request->ip());

        return response()->json([
            'token_id' => $token->id,
            'tenant_id' => $token->tenant_id,
            'used_at' => $token->used_at->toIso8601String(),
            'expires_at' => $token->sessionExpiresAt(self::SESSION_TTL_MINUTES)->toIso8601String(),
            'machine_token' => $machineToken,
            'message' => 'Impersonation начат. Сессия активна 1 час.',
        ]);
    }

    /** POST /api/admin/impersonation/end */
    public function end(Request $request, ImpersonationAuditService $audit): JsonResponse
    {
        $tokenId = (int) $request->input('token_id');

        $token = ImpersonationToken::on(self::DB_CONNECTION)->find($tokenId);
        if (! $token) {
            return response()->json(['message' => 'Токен не найден.'], 404);
        }

        if ($token->used_at === null) {
            return response()->json([
                'message' => 'Сессия impersonation не была активирована.',
            ], 422);
        }

        if ($token->session_ended_at !== null) {
            return response()->json([
                'message' => 'Сессия уже завершена.',
            ], 422);
        }

        $token->update(['session_ended_at' => now()]);

        $audit->recordEnd($token, adminId: (int) $token->requested_by, ip: $request->ip());

        try {
            Mail::to((string) $token->sent_to_email)
                ->queue(new ImpersonationEndedMail((string) $token->sent_to_email));
        } catch (\Throwable $e) {
            Log::warning('impersonation end mail: '.$e->getMessage());
        }

        return response()->json([
            'token_id' => $token->id,
            'session_ended_at' => $token->session_ended_at->toIso8601String(),
            'message' => 'Impersonation завершён.',
        ]);
    }

    /**
     * POST /api/impersonation/leave — завершить свою impersonation-сессию из кабинета.
     *
     * Маркер `impersonation` из сессии НЕ удаляется здесь намеренно:
     * ImpersonationContext (global web middleware) на следующем запросе
     * обнаружит isSessionActive()=false и вернёт 401 явно, не доходя до auth:sanctum.
     * Это обеспечивает корректный 401 как в реальном браузере, так и в тест-среде
     * (где Auth::guard('web')->logout() может не повлиять на кэш sanctum-guard).
     */
    public function leave(Request $request, ImpersonationAuditService $audit): JsonResponse
    {
        $marker = $request->session()->get('impersonation');
        if ($marker === null) {
            return response()->json(['message' => 'Сессия impersonation не активна.'], 422);
        }

        $token = ImpersonationToken::on(self::DB_CONNECTION)->find($marker['token_id']);
        if ($token !== null && $token->session_ended_at === null) {
            $token->update(['session_ended_at' => now()]);
            $audit->recordEnd($token, adminId: (int) $token->requested_by, ip: $request->ip());
            try {
                Mail::to((string) $token->sent_to_email)
                    ->queue(new ImpersonationEndedMail((string) $token->sent_to_email));
            } catch (\Throwable $e) {
                Log::warning('impersonation leave mail: '.$e->getMessage());
            }
        }

        return response()->json(['message' => 'Вы вышли из режима поддержки.']);
    }
}
