<?php

namespace App\Domain\Identity\Services;

use App\Domain\Identity\DTOs\AuthResult;
use App\Domain\Identity\Models\LoginHistory;
use App\Domain\Shared\Helpers\CredentialNormalizer;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;

class AuthService
{
    private const MAX_FAILED_ATTEMPTS = 5;
    private const LOCKOUT_MINUTES = 30;

    public function attempt(string $identifier, string $password, string $ip, ?string $userAgent = null): AuthResult
    {
        $identifier = trim($identifier);

        $user = filter_var($identifier, FILTER_VALIDATE_EMAIL)
            ? $this->findByEmail($identifier)
            : $this->findByPhone($identifier);

        if (!$user) {
            return new AuthResult(success: false, reason: 'invalid_credentials');
        }

        // Check if account is locked
        if ($user->isLockedOut()) {
            $this->recordLogin($user, $ip, $userAgent, 'locked', 'Account locked');
            $minutesRemaining = now()->diffInMinutes($user->locked_until);
            return new AuthResult(success: false, reason: 'locked', minutesRemaining: $minutesRemaining);
        }

        // Check account status
        if ($user->status !== 'active') {
            $this->recordLogin($user, $ip, $userAgent, 'blocked', "Account status: {$user->status}");
            return new AuthResult(success: false, reason: 'inactive', status: $user->status);
        }

        // Verify password
        if (!Hash::check($password, $user->password)) {
            $user->increment('failed_login_attempts');

            // Lock if threshold reached
            if ($user->failed_login_attempts >= self::MAX_FAILED_ATTEMPTS) {
                $user->update(['locked_until' => now()->addMinutes(self::LOCKOUT_MINUTES)]);
                $this->recordLogin($user, $ip, $userAgent, 'locked', 'Max attempts exceeded');
                return new AuthResult(success: false, reason: 'locked', minutesRemaining: self::LOCKOUT_MINUTES);
            }

            $this->recordLogin($user, $ip, $userAgent, 'failed', 'Invalid password');
            $attemptsRemaining = self::MAX_FAILED_ATTEMPTS - $user->failed_login_attempts;
            return new AuthResult(success: false, reason: 'invalid_credentials', attemptsRemaining: $attemptsRemaining);
        }

        // SUCCESS
        $user->update([
            'failed_login_attempts' => 0,
            'locked_until' => null,
            'last_login_at' => now(),
            'last_login_ip' => $ip,
            'login_count' => $user->login_count + 1,
        ]);

        $this->recordLogin($user, $ip, $userAgent, 'success');

        return new AuthResult(success: true, user: $user);
    }

    /**
     * Find a user by email, case-insensitively.
     *
     * Mail addresses are not case-sensitive, but Postgres '=' is, so a row
     * stored as 'Km...@gmail.com' was unreachable to anyone whose keyboard
     * lowercases the field. Ordering makes the result deterministic where a
     * case-duplicate pair already exists: an exact match wins, then the
     * account actually being used, so nobody is dropped into a stale
     * duplicate. Those pairs still need a manual merge.
     */
    private function findByEmail(string $email): ?User
    {
        return User::whereRaw('LOWER(email) = ?', [CredentialNormalizer::email($email)])
            ->orderByRaw('CASE WHEN email = ? THEN 0 ELSE 1 END', [$email])
            ->orderByRaw('last_login_at IS NULL')
            ->orderByDesc('last_login_at')
            ->orderByDesc('id')
            ->first();
    }

    /**
     * Find a user by phone, tolerating the formats people actually type.
     *
     * Matches every stored shape of the number rather than the canonical
     * form alone, so this keeps working on rows written before the
     * normalisation migration ran.
     */
    private function findByPhone(string $phone): ?User
    {
        $variants = CredentialNormalizer::phoneVariants($phone);

        if ($variants === []) {
            return null;
        }

        $candidates = User::whereIn('phone', $variants)
            ->orderByRaw('last_login_at IS NULL')
            ->orderByDesc('last_login_at')
            ->orderByDesc('id')
            ->get();

        if ($candidates->isEmpty()) {
            return null;
        }

        // 2026_08_30_000004 deliberately left duplicate phone numbers in place
        // — it logged the ids that needed a manual merge rather than failing —
        // so ->first() on this column is an account-selection primitive.
        //
        // Two accounts for the same PERSON are a duplicate: picking the one in
        // active use is right, and the merge screen exists to clear the pair.
        // Two accounts for DIFFERENT people sharing a number is not a
        // duplicate, and choosing between them silently would sign someone into
        // a stranger's account. That case refuses.
        $people = $candidates->pluck('person_id')->filter()->unique();

        if ($people->count() > 1) {
            Log::warning('Ambiguous phone login refused: one number, several people', [
                'user_ids' => $candidates->pluck('id')->all(),
            ]);

            return null;
        }

        return $candidates->first();
    }

    private function recordLogin(User $user, string $ip, ?string $userAgent, string $status, ?string $reason = null): void
    {
        LoginHistory::create([
            'user_id' => $user->id,
            'academy_id' => $user->academy_id,
            'ip_address' => $ip,
            'user_agent' => $userAgent,
            'status' => $status,
            'failed_reason' => $reason,
        ]);
    }
}
