<?php
declare(strict_types=1);

namespace App\Auth;

use App\Mailer\EmailMessage;
use App\Mailer\MailerInterface;
use App\Support\Translator;
use App\Support\FileLogger;
use App\Support\Hooks;
use App\Support\CoreHooks;
use App\Support\TranslatableException;
use App\Users\TrustService;
use InvalidArgumentException;
use PDO;
use RuntimeException;
use Throwable;

final class AuthService
{
    private const EMAIL_VERIFICATION_TTL = 86400;
    private const PASSWORD_RESET_TTL = 3600;
    private const DEFAULT_LOCALE = 'ar';
    private bool $mailDeliveryFailed = false;

    public function __construct(
        private readonly PDO $pdo,
        private readonly MailerInterface $mailer,
        private readonly bool $requireEmailVerification = true,
        private readonly ?Translator $translator = null,
        private readonly string $publicBaseUrl = '',
        private readonly string $siteName = '',
    ) {
    }

    public function mailDeliveryFailed(): bool
    {
        return $this->mailDeliveryFailed;
    }

    public function register(string $name, string $email, string $password, string $locale = 'ar'): int
    {
        $name = trim($name);
        $email = $this->normalizeEmail($email);
        $password = trim($password);
        $locale = $this->normalizeLocale($locale);

        if ($name === '') {
            throw new TranslatableException('auth.validation_name_required');
        }

        if ($email === null) {
            throw new TranslatableException('auth.validation_email_required');
        }

        if (strlen($password) < 8) {
            throw new TranslatableException('auth.validation_password_min');
        }

        $this->pdo->beginTransaction();

        try {
            if ($this->findUserIdByEmail($email) !== null) {
                throw new TranslatableException('auth.validation_email_exists');
            }

            $statement = $this->pdo->prepare(
                'INSERT INTO users (
                    name,
                    email,
                    role,
                    password_hash,
                    preferred_locale,
                    email_verified_at,
                    created_at,
                    updated_at
                 ) VALUES (
                    :name,
                    :email,
                    :role,
                    :password_hash,
                    :preferred_locale,
                    :email_verified_at,
                    NOW(),
                    NOW()
                 )'
            );
            $statement->execute([
                'name' => $name,
                'email' => $email,
                'role' => 'member',
                'password_hash' => password_hash($password, PASSWORD_DEFAULT),
                'preferred_locale' => $locale,
                'email_verified_at' => $this->requireEmailVerification ? null : gmdate('Y-m-d H:i:s'),
            ]);

            $userId = (int) $this->pdo->lastInsertId();
            $username = $this->generatedUsername($userId, $email);
            $usernameUpdate = $this->pdo->prepare('UPDATE users SET username = :username WHERE id = :id');
            $usernameUpdate->execute(['username' => $username, 'id' => $userId]);
            $rawToken = null;
            if ($this->requireEmailVerification) {
                $rawToken = $this->generateToken();
                $this->storeToken($userId, 'email_verification', $rawToken, self::EMAIL_VERIFICATION_TTL);
            }

            $this->pdo->commit();
        } catch (Throwable $throwable) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }

            throw $throwable;
        }

        Hooks::emit(CoreHooks::USER_REGISTERED, [
            'user_id' => $userId,
            'locale' => $locale,
            'email_verification_required' => $this->requireEmailVerification,
        ]);

        if (is_string($rawToken) && $rawToken !== '') {
            $verificationUrl = $this->publicUrl('verify-email', ['token' => $rawToken]);
            $this->sendMail(new EmailMessage(
                $email,
                $this->message('email.verify_subject', ':site email verification', ['site' => $this->siteName]),
                $this->message('email.verify_body', 'Use this one-time verification token: :token', [
                    'token' => $rawToken,
                    'url' => $verificationUrl !== '' ? $verificationUrl : $rawToken,
                ])
            ));
        }

        return $userId;
    }

    public function resendVerification(string $email, string $locale = 'ar'): bool
    {
        $email = $this->normalizeEmail($email);
        if ($email === null) return true;
        $statement = $this->pdo->prepare('SELECT id, preferred_locale, email_verified_at FROM users WHERE email = :email LIMIT 1');
        $statement->execute(['email' => $email]);
        $user = $statement->fetch(PDO::FETCH_ASSOC);
        if (!is_array($user) || $user['email_verified_at'] !== null) return true;
        $rawToken = $this->generateToken();
        $this->storeToken((int) $user['id'], 'email_verification', $rawToken, self::EMAIL_VERIFICATION_TTL);
        $verificationUrl = $this->publicUrl('verify-email', ['token' => $rawToken]);
        $this->sendMail(new EmailMessage(
            $email,
            $this->message('email.verify_subject', ':site email verification', ['site' => $this->siteName]),
            $this->message('email.verify_body', 'Use this one-time verification token: :token', ['token' => $rawToken, 'url' => $verificationUrl !== '' ? $verificationUrl : $rawToken])
        ));
        return true;
    }

    public function verifyEmail(string $rawToken): bool
    {
        $rawToken = trim($rawToken);

        if ($rawToken === '') {
            return false;
        }

        $this->pdo->beginTransaction();

        try {
            $token = $this->lockActiveToken($rawToken, 'email_verification');

            if ($token === null) {
                $this->pdo->rollBack();

                return false;
            }

            if (!$this->markTokenUsed((int) $token['id'])) {
                $this->pdo->rollBack();

                return false;
            }

            $updateUser = $this->pdo->prepare(
                'UPDATE users
                 SET email_verified_at = NOW(),
                     updated_at = NOW()
                 WHERE id = :id'
            );
            $updateUser->execute(['id' => $token['user_id']]);

            $this->pdo->commit();

            return true;
        } catch (Throwable $throwable) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }

            throw $throwable;
        }
    }

    public function authenticate(string $email, string $password): ?array
    {
        $email = $this->normalizeEmail($email);

        if ($email === null || $password === '') {
            return null;
        }

        $statement = $this->pdo->prepare(
            'SELECT
                id,
                name,
                username,
                email,
                bio,
                avatar_url,
                role,
                account_type,
                trust_level,
                login_disabled,
                created_by_admin_id,
                status,
                points,
                preferred_locale,
                email_verified_at,
                created_at,
                updated_at,
                password_hash
             FROM users
             WHERE email = :email
             LIMIT 1'
        );
        $statement->execute(['email' => $email]);
        $user = $statement->fetch(PDO::FETCH_ASSOC);

        if (!is_array($user)) {
            return null;
        }

        if (!TrustService::canLogin($user)) {
            return null;
        }

        if (($user['email_verified_at'] ?? null) === null) {
            return null;
        }

        $passwordHash = $user['password_hash'] ?? null;

        if (!is_string($passwordHash) || !password_verify($password, $passwordHash)) {
            return null;
        }

        unset($user['password_hash']);

        return $user;
    }

    public function requestPasswordReset(string $email): bool
    {
        $email = $this->normalizeEmail($email);

        if ($email === null) {
            return false;
        }

        $statement = $this->pdo->prepare(
            'SELECT id
             FROM users
             WHERE email = :email
             LIMIT 1'
        );
        $statement->execute(['email' => $email]);
        $userId = $statement->fetchColumn();

        if ($userId === false) {
            return true;
        }

        $rawToken = $this->generateToken();
        $this->storeToken((int) $userId, 'password_reset', $rawToken, self::PASSWORD_RESET_TTL);

        $resetUrl = $this->publicUrl('reset-password', ['token' => $rawToken]);
        $this->sendMail(new EmailMessage(
            $email,
            $this->message('email.reset_subject', ':site password reset', ['site' => $this->siteName]),
            $this->message('email.reset_body', 'Use this one-time password reset token: :token', [
                'token' => $rawToken,
                'url' => $resetUrl !== '' ? $resetUrl : $rawToken,
            ])
        ));

        return true;
    }

    private function sendMail(EmailMessage $message): void
    {
        try {
            $this->mailer->send($message);
        } catch (Throwable $throwable) {
            $this->mailDeliveryFailed = true;
            (new FileLogger())->error('Mail delivery failed.', $throwable, ['to' => $message->to]);
        }
    }

    public function resetPassword(string $rawToken, string $newPassword): bool
    {
        $rawToken = trim($rawToken);
        $newPassword = trim($newPassword);

        if ($rawToken === '' || strlen($newPassword) < 8) {
            return false;
        }

        $this->pdo->beginTransaction();

        try {
            $token = $this->lockActiveToken($rawToken, 'password_reset');

            if ($token === null) {
                $this->pdo->rollBack();

                return false;
            }

            if (!$this->markTokenUsed((int) $token['id'])) {
                $this->pdo->rollBack();

                return false;
            }

            $updateUser = $this->pdo->prepare(
                'UPDATE users
                 SET password_hash = :password_hash,
                     auth_version = auth_version + 1,
                     updated_at = NOW()
                 WHERE id = :id'
            );
            $updateUser->execute([
                'password_hash' => password_hash($newPassword, PASSWORD_DEFAULT),
                'id' => $token['user_id'],
            ]);

            $this->pdo->commit();

            return true;
        } catch (Throwable $throwable) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }

            throw $throwable;
        }
    }

    public function updatePreferredLocale(int $userId, string $locale): bool
    {
        if ($userId <= 0) {
            return false;
        }

        $locale = $this->normalizeLocale($locale);
        $statement = $this->pdo->prepare(
            'UPDATE users
             SET preferred_locale = :preferred_locale,
                 updated_at = NOW()
             WHERE id = :id'
        );
        $statement->execute([
            'preferred_locale' => $locale,
            'id' => $userId,
        ]);

        return $statement->rowCount() === 1;
    }

    private function findUserIdByEmail(string $email): ?int
    {
        $statement = $this->pdo->prepare(
            'SELECT id
             FROM users
             WHERE email = :email
             LIMIT 1'
        );
        $statement->execute(['email' => $email]);
        $userId = $statement->fetchColumn();

        if ($userId === false) {
            return null;
        }

        return (int) $userId;
    }

    private function storeToken(int $userId, string $type, string $rawToken, int $ttlSeconds): void
    {
        $statement = $this->pdo->prepare(
            "INSERT INTO email_tokens (user_id, type, token_hash, expires_at, used_at, created_at)
             VALUES (:user_id, :type, :token_hash, DATE_ADD(NOW(), INTERVAL {$ttlSeconds} SECOND), NULL, NOW())"
        );
        $statement->execute([
            'user_id' => $userId,
            'type' => $type,
            'token_hash' => hash('sha256', $rawToken),
        ]);
    }

    private function lockActiveToken(string $rawToken, string $type): ?array
    {
        $statement = $this->pdo->prepare(
            'SELECT id, user_id
             FROM email_tokens
             WHERE token_hash = :token_hash
               AND type = :type
               AND used_at IS NULL
               AND expires_at > NOW()
             LIMIT 1
             FOR UPDATE'
        );
        $statement->execute([
            'token_hash' => hash('sha256', $rawToken),
            'type' => $type,
        ]);
        $token = $statement->fetch(PDO::FETCH_ASSOC);

        return is_array($token) ? $token : null;
    }

    private function markTokenUsed(int $tokenId): bool
    {
        $statement = $this->pdo->prepare(
            'UPDATE email_tokens
             SET used_at = NOW()
             WHERE id = :id
               AND used_at IS NULL'
        );
        $statement->execute(['id' => $tokenId]);

        return $statement->rowCount() === 1;
    }

    private function message(string $key, string $fallback, array $replace = []): string
    {
        if ($this->translator instanceof Translator) {
            $value = $this->translator->get($key, $replace);
            if ($value !== $key) return $value;
        }
        foreach ($replace as $name => $value) {
            $fallback = str_replace(':' . $name, (string) $value, $fallback);
        }
        return $fallback;
    }

    private function publicUrl(string $path, array $query = []): string
    {
        $base = rtrim(trim($this->publicBaseUrl), '/');
        if ($base === '') return '';
        $url = $base . '/' . ltrim($path, '/');
        if ($query !== []) $url .= '?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
        return $url;
    }

    private function generatedUsername(int $userId, string $email): string
    {
        $preferred = 'member-' . $userId;
        $statement = $this->pdo->prepare('SELECT 1 FROM users WHERE username = :username AND id <> :id LIMIT 1');
        $statement->execute(['username' => $preferred, 'id' => $userId]);
        if ($statement->fetchColumn() === false) {
            return $preferred;
        }

        return 'u-' . $userId . '-' . substr(hash('sha256', $userId . ':' . $email), 0, 8);
    }

    private function normalizeEmail(string $email): ?string
    {
        $email = strtolower(trim($email));

        if ($email === '' || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
            return null;
        }

        return $email;
    }

    private function normalizeLocale(string $locale): string
    {
        $locale = strtolower(str_replace('_', '-', trim($locale)));
        if ($locale === '' || preg_match('/^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/', $locale) !== 1) {
            return self::DEFAULT_LOCALE;
        }

        try {
            $statement = $this->pdo->prepare('SELECT 1 FROM languages WHERE code = :code AND is_enabled = 1 LIMIT 1');
            $statement->execute(['code' => $locale]);
            if ($statement->fetchColumn()) return $locale;
        } catch (Throwable) {
            if (in_array($locale, ['ar', 'en'], true)) return $locale;
        }

        return self::DEFAULT_LOCALE;
    }

    private function generateToken(): string
    {
        return bin2hex(random_bytes(32));
    }
}
