<?php
declare(strict_types=1);

namespace App;

final class Auth
{
    public function __construct(private App $app) {}

    private function pdo(): \PDO
    {
        $pdo = $this->app->pdo();
        if (!$pdo) {
            throw new \RuntimeException('Database connection not available.');
        }
        return $pdo;
    }

    public function register(string $displayName, string $email, string $password): int
    {
        $pdo = $this->pdo();
        $email = strtolower(trim($email));
        $displayName = trim($displayName);

        if ($displayName === '' || $email === '' || $password === '') {
            throw new \InvalidArgumentException('Display-Name, E-Mail und Passwort sind erforderlich.');
        }

        $pdo->beginTransaction();
        try {
            $stmt = $pdo->prepare('SELECT id FROM users WHERE email = :email LIMIT 1');
            $stmt->execute(['email' => $email]);
            if ($stmt->fetchColumn()) {
                throw new \RuntimeException('E-Mail ist bereits registriert.');
            }

            $hash = password_hash($password, PASSWORD_ARGON2ID);
            $stmt = $pdo->prepare('INSERT INTO users (email, password_hash, status, created_at, updated_at) VALUES (:email, :pw, :status, NOW(), NOW())');
            $stmt->execute([
                'email' => $email,
                'pw' => $hash,
                'status' => 'pending',
            ]);
            $userId = (int)$pdo->lastInsertId();

            $stmt = $pdo->prepare('INSERT INTO user_profiles (user_id, display_name, share_level, children_visibility, created_at, updated_at) VALUES (:uid, :name, :share, :childvis, NOW(), NOW())');
            $stmt->execute([
                'uid' => $userId,
                'name' => $displayName,
                'share' => 'basic',
                'childvis' => 'hidden',
            ]);

            $pdo->commit();
            return $userId;
        } catch (\Throwable $e) {
            $pdo->rollBack();
            throw $e;
        }
    }

    public function createVerifyCode(int $userId, string $email): string
    {
        $pdo = $this->pdo();
        $code = $this->generateCode(6);
        $hash = hash('sha256', $code);

        $pdo->prepare('DELETE FROM user_tokens WHERE user_id = :uid AND type = :t')->execute(['uid' => $userId, 't' => 'verify']);
        $stmt = $pdo->prepare('INSERT INTO user_tokens (user_id, type, code, token_hash, expires_at, created_at) VALUES (:uid, :type, :code, :hash, DATE_ADD(NOW(), INTERVAL 48 HOUR), NOW())');
        $stmt->execute([
            'uid' => $userId,
            'type' => 'verify',
            'code' => $code,
            'hash' => $hash,
        ]);

        return $code;
    }

    public function verifyCode(string $email, string $code): int
    {
        $pdo = $this->pdo();
        $email = strtolower(trim($email));
        $hash = hash('sha256', $code);

        $stmt = $pdo->prepare('SELECT u.id, u.status, t.id AS tid, t.token_hash FROM users u JOIN user_tokens t ON t.user_id = u.id AND t.type = :type WHERE u.email = :email AND (t.used_at IS NULL) AND t.expires_at > NOW() ORDER BY t.expires_at DESC LIMIT 1');
        $stmt->execute(['type' => 'verify', 'email' => $email]);
        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
        if (!$row || !hash_equals((string)$row['token_hash'], $hash)) {
            throw new \RuntimeException('Code ist ungültig oder abgelaufen.');
        }

        $userId = (int)$row['id'];
        $tid = (int)$row['tid'];

        $pdo->beginTransaction();
        try {
            $pdo->prepare('UPDATE user_tokens SET used_at = NOW() WHERE id = :id')->execute(['id' => $tid]);
            $pdo->prepare('UPDATE users SET status = :st, email_verified_at = NOW() WHERE id = :id')->execute(['st' => 'active', 'id' => $userId]);
            $pdo->commit();
        } catch (\Throwable $e) {
            $pdo->rollBack();
            throw $e;
        }

        return $userId;
    }

    public function createResetCode(string $email): array
    {
        $pdo = $this->pdo();
        $email = strtolower(trim($email));

        $stmt = $pdo->prepare('SELECT u.id, p.display_name FROM users u LEFT JOIN user_profiles p ON p.user_id = u.id WHERE u.email = :email LIMIT 1');
        $stmt->execute(['email' => $email]);
        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
        if (!$row) {
            throw new \RuntimeException('E-Mail ist nicht registriert.');
        }

        $userId = (int)$row['id'];
        $displayName = (string)($row['display_name'] ?? $email);
        $code = $this->generateCode(6);
        $hash = hash('sha256', $code);

        $pdo->prepare('DELETE FROM user_tokens WHERE user_id = :uid AND type = :t')->execute(['uid' => $userId, 't' => 'reset']);
        $stmt = $pdo->prepare('INSERT INTO user_tokens (user_id, type, code, token_hash, expires_at, created_at) VALUES (:uid, :type, :code, :hash, DATE_ADD(NOW(), INTERVAL 2 HOUR), NOW())');
        $stmt->execute([
            'uid' => $userId,
            'type' => 'reset',
            'code' => $code,
            'hash' => $hash,
        ]);

        return ['user_id' => $userId, 'code' => $code, 'display_name' => $displayName];
    }

    public function verifyResetCode(string $email, string $code): int
    {
        $pdo = $this->pdo();
        $email = strtolower(trim($email));
        $hash = hash('sha256', $code);

        $stmt = $pdo->prepare('SELECT u.id, t.id AS tid, t.token_hash FROM users u JOIN user_tokens t ON t.user_id = u.id AND t.type = :type WHERE u.email = :email AND (t.used_at IS NULL) AND t.expires_at > NOW() ORDER BY t.expires_at DESC LIMIT 1');
        $stmt->execute(['type' => 'reset', 'email' => $email]);
        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
        if (!$row || !hash_equals((string)$row['token_hash'], $hash)) {
            throw new \RuntimeException('Code ist ungültig oder abgelaufen.');
        }

        $userId = (int)$row['id'];
        $tid = (int)$row['tid'];

        $pdo->beginTransaction();
        try {
            $pdo->prepare('UPDATE user_tokens SET used_at = NOW() WHERE id = :id')->execute(['id' => $tid]);
            $pdo->commit();
        } catch (\Throwable $e) {
            $pdo->rollBack();
            throw $e;
        }

        return $userId;
    }

    public function resetPassword(int $userId, string $password): void
    {
        $pdo = $this->pdo();
        if ($password === '' || strlen($password) < 8) {
            throw new \InvalidArgumentException('Passwort muss mindestens 8 Zeichen haben.');
        }
        $hash = password_hash($password, PASSWORD_ARGON2ID);
        $stmt = $pdo->prepare('UPDATE users SET password_hash = :pw, status = :status, updated_at = NOW() WHERE id = :id');
        $stmt->execute([
            'pw' => $hash,
            'status' => 'active',
            'id' => $userId,
        ]);
    }

    private function generateCode(int $len = 6): string
    {
        $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
        $out = '';
        for ($i = 0; $i < $len; $i++) {
            $out .= $chars[random_int(0, strlen($chars) - 1)];
        }
        return $out;
    }

    public function login(string $email, string $password): array
    {
        $pdo = $this->pdo();
        $email = strtolower(trim($email));

        $stmt = $pdo->prepare('SELECT id, password_hash, status FROM users WHERE email = :email LIMIT 1');
        $stmt->execute(['email' => $email]);
        $row = $stmt->fetch(\PDO::FETCH_ASSOC);

        if (!$row) {
            throw new \RuntimeException('E-Mail oder Passwort ist falsch.');
        }
        if (!password_verify($password, (string)$row['password_hash'])) {
            throw new \RuntimeException('E-Mail oder Passwort ist falsch.');
        }

        $userId = (int)$row['id'];
        $status = (string)$row['status'];

        if ($status === 'active') {
            $upd = $pdo->prepare('UPDATE users SET last_login_at = NOW() WHERE id = :id');
            $upd->execute(['id' => $userId]);
        }

        return ['id' => $userId, 'status' => $status];
    }
}
