<?php

namespace App\Domains\Auth\Services;

use App\Domains\Auth\Exceptions\AccountLockedException;
use App\Domains\Auth\Models\LoginAttempt;
use App\Domains\Auth\Models\ResetPasswordAttempt;
use App\Domains\Auth\Models\User;
use App\Domains\Authorization\Enums\PermissionCode;
use App\Domains\Authorization\Enums\RoleCode;
use App\Domains\Notification\Events\PasswordResetRequested;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;

class PasswordLoginService
{
    public function __construct(
        private readonly ResetPasswordAttemptService $resetPasswordAttemptService,
    ) {}

    /**
     * @throws AccountLockedException
     * @throws ValidationException
     */
    public function authenticate(
        string $email,
        string $password,
        string $tenantCode,
        ?string $ipAddress,
        ?string $userAgent,
    ): User {
        return $this->authenticateUser(
            $email,
            $password,
            $tenantCode,
            $ipAddress,
            $userAgent,
        );
    }

    /**
     * Authenticate a tenant-bound AdminApp user without requiring the caller
     * to know their tenant code beforehand.
     *
     * @throws AccountLockedException
     * @throws ValidationException
     */
    public function authenticateAdminApp(
        string $email,
        string $password,
        ?string $ipAddress,
        ?string $userAgent,
    ): User {
        return $this->authenticateUser(
            $email,
            $password,
            null,
            $ipAddress,
            $userAgent,
            RoleCode::AdminApp,
            true,
            null,
            PasswordResetRequested::CHANNEL_ADMINAPP,
        );
    }

    /**
     * Authenticate a tenant-bound user authorized to scan tickets.
     *
     * @throws AccountLockedException
     * @throws ValidationException
     */
    public function authenticateScanner(
        string $email,
        string $password,
        ?string $ipAddress,
        ?string $userAgent,
    ): User {
        return $this->authenticateUser(
            $email,
            $password,
            null,
            $ipAddress,
            $userAgent,
            null,
            true,
            PermissionCode::ScanTickets->value,
            PasswordResetRequested::CHANNEL_SCANNER,
        );
    }

    private function authenticateUser(
        string $email,
        string $password,
        ?string $tenantCode,
        ?string $ipAddress,
        ?string $userAgent,
        ?RoleCode $requiredRole = RoleCode::User,
        bool $requiresTenant = false,
        ?string $requiredPermission = null,
        string $passwordResetChannel = PasswordResetRequested::CHANNEL_STOREFRONT,
    ): User {
        $normalizedEmail = mb_strtolower(trim($email));
        $now = CarbonImmutable::now();

        /** @var array{outcome: string, user: User|null, locked_until: CarbonImmutable|null} $result */
        $result = DB::transaction(function () use (
            $normalizedEmail,
            $password,
            $tenantCode,
            $ipAddress,
            $userAgent,
            $now,
            $requiredRole,
            $requiresTenant,
            $requiredPermission,
            $passwordResetChannel,
        ): array {
            $user = User::query()
                ->where('email', $normalizedEmail)
                ->when(
                    $requiredRole !== null,
                    fn ($query) => $query->where('rol_codigo', $requiredRole->value),
                )
                ->when(
                    $requiredPermission !== null,
                    fn ($query) => $query->whereHas(
                        'role.permissions',
                        fn ($query) => $query->where('permisos.codigo', $requiredPermission)
                    ),
                )
                ->when(
                    $requiresTenant,
                    fn ($query) => $query->whereNotNull('tenant_codigo'),
                )
                ->lockForUpdate()
                ->first();
            $attemptTenantCode = $tenantCode ?? $user?->tenant_codigo;

            if ($user?->locked_until?->isFuture()) {
                $this->recordAttempt(
                    $user,
                    $normalizedEmail,
                    $attemptTenantCode,
                    LoginAttempt::OUTCOME_ACCOUNT_LOCKED,
                    $ipAddress,
                    $userAgent,
                );

                return [
                    'outcome' => LoginAttempt::OUTCOME_ACCOUNT_LOCKED,
                    'user' => $user,
                    'locked_until' => CarbonImmutable::instance($user->locked_until),
                ];
            }

            if ($user !== null && $user->locked_until !== null) {
                $user->forceFill([
                    'failed_login_attempts' => 0,
                    'last_failed_login_at' => null,
                    'locked_until' => null,
                ])->save();
            }

            if ($user === null || ! Hash::check($password, $user->password)) {
                if ($user !== null && $attemptTenantCode !== null) {
                    $this->registerFailure(
                        $user,
                        $now,
                        $attemptTenantCode,
                        $passwordResetChannel,
                    );
                }

                $outcome = $user?->locked_until?->isFuture()
                    ? LoginAttempt::OUTCOME_ACCOUNT_LOCKED
                    : LoginAttempt::OUTCOME_INVALID_CREDENTIALS;
                $this->recordAttempt(
                    $user,
                    $normalizedEmail,
                    $attemptTenantCode,
                    $outcome,
                    $ipAddress,
                    $userAgent,
                );

                return [
                    'outcome' => $outcome,
                    'user' => $user,
                    'locked_until' => $user?->locked_until === null
                        ? null
                        : CarbonImmutable::instance($user->locked_until),
                ];
            }

            $user->forceFill([
                'failed_login_attempts' => 0,
                'last_failed_login_at' => null,
                'locked_until' => null,
            ])->save();

            $this->recordAttempt(
                $user,
                $normalizedEmail,
                $attemptTenantCode,
                LoginAttempt::OUTCOME_SUCCESS,
                $ipAddress,
                $userAgent,
            );

            return [
                'outcome' => LoginAttempt::OUTCOME_SUCCESS,
                'user' => $user,
                'locked_until' => null,
            ];
        });

        if ($result['outcome'] === LoginAttempt::OUTCOME_ACCOUNT_LOCKED) {
            throw new AccountLockedException($result['locked_until']);
        }

        if ($result['outcome'] === LoginAttempt::OUTCOME_INVALID_CREDENTIALS) {
            throw ValidationException::withMessages([
                'email' => __('api.auth.invalid_credentials'),
            ]);
        }

        return $result['user'];
    }

    private function registerFailure(
        User $user,
        CarbonImmutable $now,
        string $tenantCode,
        string $passwordResetChannel,
    ): void {
        $windowMinutes = max(1, (int) config('login-security.attempt_window_minutes'));
        $maxAttempts = max(1, (int) config('login-security.max_attempts'));
        $lockMinutes = max(1, (int) config('login-security.lock_minutes'));

        $withinAttemptWindow = $user->last_failed_login_at !== null
            && $user->last_failed_login_at->gte($now->subMinutes($windowMinutes));
        $attempts = $withinAttemptWindow
            ? $user->failed_login_attempts + 1
            : 1;

        $previousAttempts = $user->failed_login_attempts;

        $user->forceFill([
            'failed_login_attempts' => $attempts,
            'last_failed_login_at' => $now,
            'locked_until' => $attempts >= $maxAttempts
                ? $now->addMinutes($lockMinutes)
                : null,
        ])->save();

        if ($attempts >= $maxAttempts && $previousAttempts < $maxAttempts) {
            try {
                if ($passwordResetChannel === PasswordResetRequested::CHANNEL_ADMINAPP) {
                    $this->resetPasswordAttemptService->createForAdminAppEmail(
                        $user->email,
                        ResetPasswordAttempt::REASON_ACCOUNT_LOCKED,
                    );
                } elseif ($passwordResetChannel === PasswordResetRequested::CHANNEL_SCANNER) {
                    $this->resetPasswordAttemptService->createForScannerEmail(
                        $user->email,
                        ResetPasswordAttempt::REASON_ACCOUNT_LOCKED,
                    );
                } else {
                    $this->resetPasswordAttemptService->createForEmail(
                        $user->email,
                        $tenantCode,
                        ResetPasswordAttempt::REASON_ACCOUNT_LOCKED,
                    );
                }
            } catch (\Throwable $e) {
                Log::error('Failed to trigger reset password on account lock', [
                    'user_id' => $user->id,
                    'exception' => $e,
                ]);
            }
        }
    }

    private function recordAttempt(
        ?User $user,
        string $normalizedEmail,
        ?string $tenantCode,
        string $outcome,
        ?string $ipAddress,
        ?string $userAgent,
    ): void {
        LoginAttempt::query()->create([
            'user_id' => $user?->getKey(),
            'email_fingerprint' => hash_hmac(
                'sha256',
                $normalizedEmail,
                (string) config('app.key'),
            ),
            'tenant_codigo' => $tenantCode,
            'outcome' => $outcome,
            'ip_address' => $ipAddress,
            'user_agent' => $userAgent === null
                ? null
                : mb_substr($userAgent, 0, 1024),
        ]);
    }
}
