<?php

declare(strict_types=1);

namespace CalendarPlugin\Domain;

use CalendarPlugin\Contracts\DatabaseAdapterInterface;
use DateTimeImmutable;
use DateTimeZone;

final class UserService
{
    private readonly string $usersTable;
    private readonly string $tokensTable;
    private readonly string $auditTable;

    public function __construct(private readonly DatabaseAdapterInterface $db, string $tableStem = 'cs_calendar')
    {
        $prefix = $db->getPrefix();
        $stem = trim($tableStem, '_');
        $this->usersTable = $prefix . $stem . '_users';
        $this->tokensTable = $prefix . $stem . '_user_tokens';
        $this->auditTable = $prefix . $stem . '_audit_log';
    }

    public function register(string $email, string $password): array
    {
        $email = $this->normalizeEmail($email);
        if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return $this->error('validation_error', 'email is required', 422);
        }
        if (strlen($password) < 8) {
            return $this->error('validation_error', 'password must be at least 8 characters', 422);
        }
        if ($this->isRateLimited('register:' . $email, 10, 3600)) {
            return $this->error('rate_limited', 'too many requests', 429);
        }
        if ($this->findUserByEmail($email)) {
            return $this->error('conflict_error', 'account already exists', 409);
        }

        $now = gmdate('c');
        $inserted = $this->db->insert(
            $this->usersTable,
            [
                'email' => $email,
                'password_hash' => password_hash($password, PASSWORD_DEFAULT),
                'email_verified_at' => null,
                'account_status' => 'pending_approval',
                'created_at' => $now,
                'updated_at' => $now,
            ]
        );
        if ($inserted === false) {
            return $this->error('internal_error', 'unable to create account', 500);
        }

        $userId = $this->db->insertId();
        $token = $this->issueToken($userId, 'email_verify', 24 * 3600);
        $this->audit('user.register', (string) $userId, 'success', ['email' => $email]);

        return [
            'ok' => true,
            'user' => $this->publicUser((array) $this->getUserById($userId)),
            'verify_token' => $token,
            'message' => 'registration submitted',
        ];
    }

    public function verifyEmail(string $token): array
    {
        if ($token === '') {
            return $this->error('validation_error', 'token is required', 422);
        }
        $tok = $this->consumeToken($token, 'email_verify');
        if (!$tok) {
            return $this->error('validation_error', 'invalid or expired token', 422);
        }
        $user = $this->getUserById((int) $tok['user_id']);
        if (!$user) {
            return $this->error('not_found', 'user not found', 404);
        }

        $now = gmdate('c');
        $this->db->update(
            $this->usersTable,
            ['email_verified_at' => $now, 'updated_at' => $now],
            ['id' => (int) $user['id']]
        );
        $updated = $this->getUserById((int) $user['id']);
        $this->audit('user.verify_email', (string) $user['id'], 'success', []);

        return [
            'ok' => true,
            'user' => $updated ? $this->publicUser($updated) : $this->publicUser($user),
            'message' => 'An admin will review your request and notify you if approved.',
        ];
    }

    public function login(string $email, string $password): array
    {
        $email = $this->normalizeEmail($email);
        if ($this->isRateLimited('login:' . $email, 20, 3600)) {
            return $this->error('rate_limited', 'too many requests', 429);
        }

        $user = $this->findUserByEmail($email);
        if (!$user || !password_verify($password, (string) ($user['password_hash'] ?? ''))) {
            return $this->error('auth_required', 'Login failure', 401);
        }
        if (empty($user['email_verified_at'])) {
            return $this->error('auth_required', 'Login failure', 401);
        }
        if ((string) ($user['account_status'] ?? '') !== 'active') {
            return $this->error('auth_required', 'Login failure', 401);
        }

        $this->audit('user.login', (string) $user['id'], 'success', []);

        return [
            'ok' => true,
            'user' => $this->publicUser($user),
        ];
    }

    public function authenticateActiveUserCredentials(string $email, string $password): ?array
    {
        $email = $this->normalizeEmail($email);
        if ($email === '' || $password === '') {
            return null;
        }
        $user = $this->findUserByEmail($email);
        if (!$user) {
            return null;
        }
        if (!password_verify($password, (string) ($user['password_hash'] ?? ''))) {
            return null;
        }
        if (empty($user['email_verified_at'])) {
            return null;
        }
        if ((string) ($user['account_status'] ?? '') !== 'active') {
            return null;
        }
        return $this->publicUser($user);
    }

    public function issueSessionToken(int $userId, int $ttlSeconds = 2592000): string
    {
        if ($userId <= 0) {
            return '';
        }
        return $this->issueToken($userId, 'session', max(300, $ttlSeconds));
    }

    public function authenticateSessionToken(string $token): ?array
    {
        $row = $this->findValidToken($token, 'session');
        if ($row === null) {
            return null;
        }

        $user = $this->getUserById((int) ($row['user_id'] ?? 0));
        if (!$user) {
            return null;
        }
        if (empty($user['email_verified_at'])) {
            return null;
        }
        if ((string) ($user['account_status'] ?? '') !== 'active') {
            return null;
        }

        return $this->publicUser($user);
    }

    public function revokeSessionToken(string $token): void
    {
        $row = $this->findValidToken($token, 'session');
        if ($row === null) {
            return;
        }
        $this->db->update(
            $this->tokensTable,
            ['used_at' => gmdate('c')],
            ['id' => (int) $row['id']]
        );
    }

    public function requestPasswordReset(string $email): array
    {
        $email = $this->normalizeEmail($email);
        if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return $this->error('validation_error', 'email is required', 422);
        }
        if ($this->isRateLimited('reset:' . $email, 10, 3600)) {
            return $this->error('rate_limited', 'too many requests', 429);
        }

        $user = $this->findUserByEmail($email);
        if (!$user) {
            return ['ok' => true, 'message' => 'if account exists, reset email will be sent'];
        }

        $token = $this->issueToken((int) $user['id'], 'password_reset', 30 * 60);
        $this->audit('user.password_reset.request', (string) $user['id'], 'success', []);

        return [
            'ok' => true,
            'reset_token' => $token,
            'message' => 'password reset requested',
        ];
    }

    public function resetPassword(string $token, string $newPassword): array
    {
        if (strlen($newPassword) < 8) {
            return $this->error('validation_error', 'password must be at least 8 characters', 422);
        }

        $tok = $this->consumeToken($token, 'password_reset');
        if (!$tok) {
            return $this->error('validation_error', 'invalid or expired token', 422);
        }

        $user = $this->getUserById((int) $tok['user_id']);
        if (!$user) {
            return $this->error('not_found', 'user not found', 404);
        }

        $this->db->update(
            $this->usersTable,
            ['password_hash' => password_hash($newPassword, PASSWORD_DEFAULT), 'updated_at' => gmdate('c')],
            ['id' => (int) $user['id']]
        );
        // Invalidate persistent web sessions after password reset.
        $this->db->delete($this->tokensTable, ['user_id' => (int) $user['id'], 'token_type' => 'session']);
        $this->audit('user.password_reset.complete', (string) $user['id'], 'success', []);

        return ['ok' => true, 'message' => 'password updated'];
    }

    public function listUsers(): array
    {
        $rows = $this->db->getResults("SELECT * FROM {$this->usersTable} ORDER BY id ASC");
        return array_map(fn(object $r): array => $this->publicUser((array) $r), $rows);
    }

    public function approveUser(int $id): ?array
    {
        $user = $this->getUserById($id);
        if (!$user) {
            return null;
        }
        if (empty($user['email_verified_at'])) {
            return null;
        }
        $this->db->update(
            $this->usersTable,
            ['account_status' => 'active', 'updated_at' => gmdate('c')],
            ['id' => $id]
        );
        $updated = $this->getUserById($id);
        $this->audit('user.approve', (string) $id, 'success', []);
        return $updated ? $this->publicUser($updated) : null;
    }

    public function removeUser(int $id): bool
    {
        $deleted = $this->db->delete($this->usersTable, ['id' => $id]);
        $this->db->delete($this->tokensTable, ['user_id' => $id]);
        if ($deleted !== false) {
            $this->audit('user.remove', (string) $id, 'success', []);
            return true;
        }
        return false;
    }

    private function findUserByEmail(string $email): ?array
    {
        foreach ($this->listRawUsers() as $user) {
            if (strtolower((string) ($user['email'] ?? '')) === strtolower($email)) {
                return $user;
            }
        }
        return null;
    }

    private function getUserById(int $id): ?array
    {
        $sql = $this->db->prepare("SELECT * FROM {$this->usersTable} WHERE id = %d", $id);
        $row = $this->db->getRow($sql);
        return $row ? (array) $row : null;
    }

    private function listRawUsers(): array
    {
        $rows = $this->db->getResults("SELECT * FROM {$this->usersTable} ORDER BY id ASC");
        return array_map(static fn(object $r): array => (array) $r, $rows);
    }

    private function issueToken(int $userId, string $type, int $ttlSeconds): string
    {
        $token = bin2hex(random_bytes(16));
        $hash = hash('sha256', $token);
        $expiresAt = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->modify('+' . $ttlSeconds . ' seconds')->format('c');

        $this->db->insert(
            $this->tokensTable,
            [
                'user_id' => $userId,
                'token_type' => $type,
                'token_hash' => $hash,
                'expires_at' => $expiresAt,
                'used_at' => null,
                'created_at' => gmdate('c'),
            ]
        );

        return $token;
    }

    private function consumeToken(string $token, string $type): ?array
    {
        $row = $this->findValidToken($token, $type);
        if ($row === null) {
            return null;
        }
        $this->db->update(
            $this->tokensTable,
            ['used_at' => gmdate('c')],
            ['id' => (int) $row['id']]
        );
        return $row;
    }

    private function findValidToken(string $token, string $type): ?array
    {
        $raw = trim($token);
        if ($raw === '') {
            return null;
        }
        $hash = hash('sha256', $raw);
        $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));

        $rows = $this->db->getResults("SELECT * FROM {$this->tokensTable} ORDER BY id ASC");
        foreach ($rows as $rowObj) {
            $row = (array) $rowObj;
            if ((string) ($row['token_type'] ?? '') !== $type) {
                continue;
            }
            if ((string) ($row['token_hash'] ?? '') !== $hash) {
                continue;
            }
            if (!empty($row['used_at'])) {
                return null;
            }
            try {
                $expires = new DateTimeImmutable((string) $row['expires_at'], new DateTimeZone('UTC'));
            } catch (\Throwable) {
                return null;
            }
            if ($expires < $now) {
                return null;
            }
            return $row;
        }
        return null;
    }

    private function publicUser(array $user): array
    {
        return [
            'id' => (int) ($user['id'] ?? 0),
            'email' => (string) ($user['email'] ?? ''),
            'email_verified_at' => $user['email_verified_at'] ?? null,
            'account_status' => (string) ($user['account_status'] ?? 'pending_approval'),
            'created_at' => (string) ($user['created_at'] ?? ''),
            'updated_at' => (string) ($user['updated_at'] ?? ''),
        ];
    }

    private function error(string $code, string $message, int $status): array
    {
        return ['error' => ['code' => $code, 'message' => $message, 'status' => $status]];
    }

    private function normalizeEmail(string $email): string
    {
        return strtolower(trim($email));
    }

    private function audit(string $action, string $target, string $result, array $context): void
    {
        if (!$this->isDiagnosticsEnabled()) {
            return;
        }
        $this->db->insert(
            $this->auditTable,
            [
                'actor' => 'plugin',
                'action' => $action,
                'target' => $target,
                'result' => $result,
                'created_at' => gmdate('c'),
                'context_json' => json_encode($context),
            ]
        );
    }

    private function isDiagnosticsEnabled(): bool
    {
        $optionsTable = $this->db->getPrefix() . 'options';
        $sql = $this->db->prepare(
            "SELECT option_value FROM {$optionsTable} WHERE option_name = %s LIMIT 1",
            'calendar_plugin_diagnostics_enabled'
        );
        $row = $this->db->getRow($sql);
        if (!$row || !property_exists($row, 'option_value')) {
            return true;
        }
        return in_array(strtolower(trim((string) $row->option_value)), ['1', 'true', 'yes', 'on'], true);
    }

    private function isRateLimited(string $bucket, int $limit, int $windowSeconds): bool
    {
        $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
        $type = 'rate:' . substr(hash('sha256', $bucket), 0, 32);
        $sql = $this->db->prepare("SELECT * FROM {$this->tokensTable} WHERE token_type = %s ORDER BY id ASC", $type);
        $rows = $this->db->getResults($sql);

        $activeCount = 0;
        foreach ($rows as $rowObj) {
            $row = (array) $rowObj;
            $id = (int) ($row['id'] ?? 0);
            $usedAt = (string) ($row['used_at'] ?? '');
            $expiresAtRaw = (string) ($row['expires_at'] ?? '');
            $expired = true;
            try {
                $expiresAt = new DateTimeImmutable($expiresAtRaw, new DateTimeZone('UTC'));
                $expired = $expiresAt < $now;
            } catch (\Throwable) {
                $expired = true;
            }

            if ($id > 0 && ($usedAt !== '' || $expired)) {
                $this->db->delete($this->tokensTable, ['id' => $id]);
                continue;
            }

            if (!$expired && $usedAt === '') {
                $activeCount++;
            }
        }

        if ($activeCount >= $limit) {
            return true;
        }

        $this->db->insert(
            $this->tokensTable,
            [
                'user_id' => 0,
                'token_type' => $type,
                'token_hash' => hash('sha256', bin2hex(random_bytes(16))),
                'expires_at' => $now->modify('+' . max(1, $windowSeconds) . ' seconds')->format('c'),
                'used_at' => null,
                'created_at' => gmdate('c'),
            ]
        );
        return false;
    }
}
