<?php
namespace App\Core;

use PDO;
use App\Core\Security;

final class Auth
{
    public static function start(): void
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            $security = $GLOBALS['config']['security'] ?? [];
            $isNativeApp = self::isNativeAppRequest();
            $nativeLifetime = self::nativePersistentLifetime();
            $secureCookie = (bool)($security['secure_cookies'] ?? false);

            if (!headers_sent()) {
                session_name($security['session_name'] ?? 'PYRKSESSID');

                if ($isNativeApp) {
                    setcookie('pyrk_native_app', '1', [
                        'expires' => time() + $nativeLifetime,
                        'path' => '/',
                        'secure' => $secureCookie,
                        'httponly' => true,
                        'samesite' => 'Lax',
                    ]);
                }

                if ($isNativeApp) {
                    @ini_set('session.gc_maxlifetime', (string)$nativeLifetime);
                }

                session_set_cookie_params([
                    'lifetime' => $isNativeApp ? $nativeLifetime : 0,
                    'path' => '/',
                    'secure' => $secureCookie,
                    'httponly' => true,
                    'samesite' => 'Lax',
                ]);
            }
            session_start();
        }

        if (self::isNativeAppRequest()) {
            $_SESSION['pyrk_native_app'] = 1;
            if (empty($_SESSION['user_id'])) {
                self::restorePersistentNativeLogin();
            }
        }

        self::enforceSessionTimeout();
    }

    public static function user(): ?array
    {
        self::start();
        if (empty($_SESSION['user_id'])) {
            return null;
        }
        $pdo = Database::connection();
        $stmt = $pdo->prepare('SELECT u.*, r.name AS role_name FROM users u JOIN roles r ON r.id = u.role_id WHERE u.id = ? LIMIT 1');
        $stmt->execute([$_SESSION['user_id']]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$user) {
            self::revokePersistentNativeLogin();
            self::clearSession(false);
            return null;
        }
        if (($user['status'] ?? '') !== 'active') {
            self::writeLogoutLog((int)$user['id'], 'account_' . ($user['status'] ?? 'not_active'));
            self::revokePersistentNativeLogin();
            self::clearSession(false);
            return null;
        }
        return $user;
    }

    public static function loginById(int $userId): bool
    {
        self::start();
        $pdo = Database::connection();
        $stmt = $pdo->prepare('SELECT id, email, status FROM users WHERE id = ? AND status = "active" LIMIT 1');
        $stmt->execute([$userId]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$user) {
            self::writeLoginLog(null, null, false, 'demo_login_blocked_or_missing');
            return false;
        }
        self::createSession((int)$user['id']);
        self::writeLoginLog((int)$user['id'], (string)$user['email'], true, null);
        $pdo->prepare('UPDATE users SET failed_login_count = 0, locked_until = NULL, last_login_at = NOW(), last_login_ip = ? WHERE id = ?')->execute([Security::clientIp(), $userId]);
        return true;
    }

    public static function attempt(string $email, string $password): bool
    {
        self::start();
        $email = strtolower(trim($email));
        $maxAttempts = (int)Security::setting('max_failed_attempts', $GLOBALS['config']['security']['login_max_attempts'] ?? 3);
        $lockMinutes = (int)Security::setting('lockout_minutes', $GLOBALS['config']['security']['login_lock_minutes'] ?? 60);
        $maxAttempts = max(1, $maxAttempts);
        $lockMinutes = max(1, $lockMinutes);

        $pdo = Database::connection();
        $stmt = $pdo->prepare('SELECT id, email, password_hash, status, failed_login_count, locked_until, email_verified_at, activation_expires_at FROM users WHERE email = ? LIMIT 1');
        $stmt->execute([$email]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$user) {
            Security::recordAttempt('login_unknown_email', $email . '|' . Security::clientIp(), false, $maxAttempts, $lockMinutes);
            self::writeLoginLog(null, $email, false, 'unknown_email');
            return false;
        }

        $userId = (int)$user['id'];
        if (self::blockExpiredActivation($pdo, $userId)) {
            self::writeLoginLog($userId, $email, false, 'activation_expired_blocked');
            return false;
        }

        if (($user['status'] ?? '') !== 'active') {
            $reason = ($user['status'] ?? '') === 'inactive' && empty($user['email_verified_at']) ? 'email_not_activated' : 'account_' . ($user['status'] ?? 'not_active');
            self::writeLoginLog($userId, $email, false, $reason);
            return false;
        }

        if (!empty($user['locked_until']) && strtotime((string)$user['locked_until']) > time()) {
            self::writeLoginLog($userId, $email, false, 'account_locked');
            return false;
        }

        if (!password_verify($password, (string)$user['password_hash'])) {
            $failed = (int)($user['failed_login_count'] ?? 0) + 1;
            $lockedUntil = $failed >= $maxAttempts ? date('Y-m-d H:i:s', time() + ($lockMinutes * 60)) : null;
            $pdo->prepare('UPDATE users SET failed_login_count = ?, locked_until = ?, updated_at = NOW() WHERE id = ?')->execute([$failed, $lockedUntil, $userId]);
            self::writeLoginLog($userId, $email, false, $lockedUntil ? 'locked_after_failed_attempts' : 'wrong_password');
            return false;
        }

        self::createSession($userId);
        $pdo->prepare('UPDATE users SET failed_login_count = 0, locked_until = NULL, last_login_at = NOW(), last_login_ip = ?, updated_at = NOW() WHERE id = ?')->execute([Security::clientIp(), $userId]);
        self::writeLoginLog($userId, $email, true, null);
        Security::recordAttempt('login_unknown_email', $email . '|' . Security::clientIp(), true, $maxAttempts, $lockMinutes);
        return true;
    }

    public static function logout(): void
    {
        self::start();
        self::writeLogoutLog(!empty($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null, 'manual_logout');
        self::revokePersistentNativeLogin();
        self::clearSession(true);
    }

    public static function requireUser(): array
    {
        $user = self::user();
        if (!$user) {
            Response::error('Login required.', 401);
        }
        return $user;
    }

    public static function requireRole(array $roles): array
    {
        $user = self::requireUser();
        if (!in_array($user['role_name'], $roles, true)) {
            Response::error('You do not have permission for this action.', 403);
        }
        return $user;
    }

    public static function hasRole(array $roles): bool
    {
        $user = self::user();
        return $user && in_array($user['role_name'], $roles, true);
    }


    public static function permissionCatalog(): array
    {
        return [
            'events' => [
                'label' => 'Events',
                'items' => [
                    'routes.manage' => 'Create and edit routes',
                    'routes.delete' => 'Delete routes',
                    'walks.manage' => 'Full access to all walks/events',
                    'walks.view_all' => 'View all walks/events',
                    'walks.view_assigned' => 'View only walks created by me or assigned to me',
                    'walks.create' => 'Create new walks/events',
                    'walks.edit' => 'Edit walks created by me or where I am assigned Manager',
                    'walks.assign_staff' => 'Assign Manager, Head Walker and Walkers to walks',
                    'walks.unregister' => 'Unregister members from walks',
                    'walks.delete' => 'Delete walks/events',
                    'attendance.manage' => 'Manage attendance and QR check-in',
                ],
            ],
            'people' => [
                'label' => 'People',
                'items' => [
                    'members.view' => 'View members',
                    'members.manage' => 'Create and edit members',
                    'members.delete' => 'Delete members',
                    'groups.manage' => 'Create and manage member groups',
                    'users.manage' => 'Create and manage system users',
                    'users.delete' => 'Delete system users',
                ],
            ],
            'rewards' => [
                'label' => 'Rewards',
                'items' => [
                    'loyalty.manage' => 'Manage loyalty points',
                    'rewards.manage' => 'Create prizes and manage claims',
                    'badges.manage' => 'Create badges and award badges',
                ],
            ],
            'operations' => [
                'label' => 'Operations',
                'items' => [
                    'photo_contest.manage' => 'Manage photo contests and voting',
                    'polls.manage' => 'Create, send and manage member polls',
                    'payments.manage' => 'Manage paid walk payments',
                    'reports.view' => 'View dashboards and reports',
                    'branding.manage' => 'Manage branding, logo and footer',
                    'security.manage' => 'Manage security center and locks',
                    'notifications.manage' => 'Manage email notifications and backup center',
                    'backups.manage' => 'Create and manage database backups',
                ],
            ],
        ];
    }

    public static function allPermissionKeys(): array
    {
        $keys = [];
        foreach (self::permissionCatalog() as $group) {
            foreach (($group['items'] ?? []) as $key => $label) {
                $keys[] = $key;
            }
        }
        return $keys;
    }

    public static function defaultPermissionKeysForRole(string $role): array
    {
        if ($role === 'super_admin') {
            return self::allPermissionKeys();
        }

        if ($role === 'event_manager') {
            return [
                'routes.manage',
                'walks.view_assigned',
                'walks.create',
                'walks.assign_staff',
                'attendance.manage',
                'members.view',
                'members.manage',
                'groups.manage',
                'loyalty.manage',
                'rewards.manage',
                'badges.manage',
                'photo_contest.manage',
                'polls.manage',
                'payments.manage',
                'reports.view',
                'branding.manage',
                'notifications.manage',
                'backups.manage',
            ];
        }

        if ($role === 'walk_leader') {
            return [
                'walks.view_assigned',
                'attendance.manage',
                'members.view',
                'reports.view',
                'photo_contest.manage',
            ];
        }

        return [];
    }

    public static function hasPermission(string $permission, ?array $user = null): bool
    {
        $user = $user ?: self::user();
        if (!$user) {
            return false;
        }

        $role = (string)($user['role_name'] ?? '');
        if ($role === 'super_admin') {
            return true;
        }

        $userId = (int)($user['id'] ?? 0);
        if ($userId <= 0) {
            return false;
        }

        try {
            $pdo = Database::connection();

            // If the user has explicit permissions, use them. Otherwise use role defaults.
            $countStmt = $pdo->prepare('SELECT COUNT(*) FROM user_permissions WHERE user_id = ?');
            $countStmt->execute([$userId]);
            $explicitCount = (int)$countStmt->fetchColumn();

            if ($explicitCount > 0) {
                $stmt = $pdo->prepare('SELECT granted FROM user_permissions WHERE user_id = ? AND permission_key = ? LIMIT 1');
                $stmt->execute([$userId, $permission]);
                return (int)($stmt->fetchColumn() ?: 0) === 1;
            }
        } catch (\Throwable) {
            // If the permissions migration is not installed yet, keep the app working with role defaults.
        }

        return in_array($permission, self::defaultPermissionKeysForRole($role), true);
    }

    public static function requirePermission(string $permission): array
    {
        $user = self::requireUser();
        if (!self::hasPermission($permission, $user)) {
            Response::error('You do not have permission for this action.', 403);
        }
        return $user;
    }


    private static function blockExpiredActivation(PDO $pdo, int $userId): bool
    {
        try {
            $stmt = $pdo->prepare('SELECT status, activation_expires_at, email_verified_at FROM users WHERE id = ? LIMIT 1');
            $stmt->execute([$userId]);
            $row = $stmt->fetch(PDO::FETCH_ASSOC);
            if (!$row) {
                return false;
            }

            if (($row['status'] ?? '') === 'inactive'
                && empty($row['email_verified_at'])
                && !empty($row['activation_expires_at'])
                && strtotime((string)$row['activation_expires_at']) < time()) {
                $pdo->prepare('UPDATE users SET status = "blocked", blocked_at = NOW(), blocked_reason = "Activation link expired after 2 hours", updated_at = NOW() WHERE id = ?')->execute([$userId]);
                return true;
            }
        } catch (\Throwable) {
            // Older installs may not have activation columns yet.
        }

        return false;
    }

    private static function createSession(int $userId): void
    {
        $_SESSION['user_id'] = $userId;
        $_SESSION['last_activity_at'] = time();
        if (self::isNativeAppRequest()) {
            $_SESSION['pyrk_native_app'] = 1;
        }
        session_regenerate_id(true);
        if (self::isNativeAppRequest()) {
            self::issuePersistentNativeLogin($userId);
        }
    }

    public static function isNativeAppRequest(): bool
    {
        $source = strtolower((string)($_GET['app'] ?? $_GET['source'] ?? ''));
        if (in_array($source, ['1', 'app', 'native', 'mobile_app', 'android', 'ios', 'pwa', 'standalone'], true)) {
            return true;
        }

        if (!empty($_COOKIE['pyrk_native_app'])) {
            return true;
        }

        if (!empty($_SESSION['pyrk_native_app'])) {
            return true;
        }

        $ua = strtolower((string)($_SERVER['HTTP_USER_AGENT'] ?? ''));
        return str_contains($ua, 'pyrkapp') || str_contains($ua, 'capacitor');
    }


    private static function nativePersistentLifetime(): int
    {
        $security = $GLOBALS['config']['security'] ?? [];
        $seconds = (int)($security['native_app_persistent_login_seconds'] ?? (10 * 365 * 24 * 60 * 60));
        return max(30 * 24 * 60 * 60, $seconds);
    }

    private static function nativeLoginCookieName(): string
    {
        return 'PYRK_NATIVE_LOGIN';
    }

    private static function nativeCookieOptions(int $expires): array
    {
        $security = $GLOBALS['config']['security'] ?? [];
        $https = (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off')
            || ((int)($_SERVER['SERVER_PORT'] ?? 0) === 443)
            || strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https';

        return [
            'expires' => $expires,
            'path' => '/',
            'secure' => $https || (bool)($security['secure_cookies'] ?? false),
            'httponly' => true,
            'samesite' => 'Lax',
        ];
    }

    private static function setNativeLoginCookie(string $value, int $expires): void
    {
        if (headers_sent()) {
            return;
        }
        setcookie(self::nativeLoginCookieName(), $value, self::nativeCookieOptions($expires));
    }

    private static function clearNativeLoginCookie(): void
    {
        unset($_COOKIE[self::nativeLoginCookieName()]);
        self::setNativeLoginCookie('', time() - 42000);
    }

    private static function issuePersistentNativeLogin(int $userId): void
    {
        if ($userId <= 0 || !self::isNativeAppRequest()) {
            return;
        }

        try {
            $pdo = Database::connection();
            $selector = bin2hex(random_bytes(12));
            $validator = bin2hex(random_bytes(32));
            $hash = hash('sha256', $validator);
            $expiresAt = date('Y-m-d H:i:s', time() + self::nativePersistentLifetime());
            $userAgentHash = hash('sha256', (string)($_SERVER['HTTP_USER_AGENT'] ?? ''));

            $existing = self::parseNativeLoginCookie();
            if ($existing) {
                $stmt = $pdo->prepare('UPDATE mobile_login_tokens SET revoked_at = COALESCE(revoked_at, NOW()) WHERE selector = ? AND user_id = ?');
                $stmt->execute([$existing['selector'], $userId]);
            }

            $stmt = $pdo->prepare('INSERT INTO mobile_login_tokens (user_id, selector, token_hash, user_agent_hash, created_at, last_used_at, expires_at, revoked_at) VALUES (?, ?, ?, ?, NOW(), NOW(), ?, NULL)');
            $stmt->execute([$userId, $selector, $hash, $userAgentHash, $expiresAt]);

            $expires = time() + self::nativePersistentLifetime();
            self::setNativeLoginCookie($selector . '.' . $validator, $expires);
            $_COOKIE[self::nativeLoginCookieName()] = $selector . '.' . $validator;
        } catch (\Throwable) {
            // The app remains usable with the normal PHP session if the additive migration is not installed yet.
        }
    }

    private static function parseNativeLoginCookie(): ?array
    {
        $raw = trim((string)($_COOKIE[self::nativeLoginCookieName()] ?? ''));
        if ($raw === '' || !str_contains($raw, '.')) {
            return null;
        }
        [$selector, $validator] = explode('.', $raw, 2);
        if (!preg_match('/^[a-f0-9]{24}$/', $selector) || !preg_match('/^[a-f0-9]{64}$/', $validator)) {
            return null;
        }
        return ['selector' => $selector, 'validator' => $validator];
    }

    private static function restorePersistentNativeLogin(): void
    {
        $token = self::parseNativeLoginCookie();
        if (!$token) {
            return;
        }

        try {
            $pdo = Database::connection();
            $stmt = $pdo->prepare('SELECT mlt.id, mlt.user_id, mlt.token_hash, mlt.expires_at, u.status FROM mobile_login_tokens mlt JOIN users u ON u.id = mlt.user_id WHERE mlt.selector = ? AND mlt.revoked_at IS NULL LIMIT 1');
            $stmt->execute([$token['selector']]);
            $row = $stmt->fetch(PDO::FETCH_ASSOC);

            $valid = $row
                && ($row['status'] ?? '') === 'active'
                && !empty($row['expires_at'])
                && strtotime((string)$row['expires_at']) > time()
                && hash_equals((string)$row['token_hash'], hash('sha256', $token['validator']));

            if (!$valid) {
                if ($row && !empty($row['id'])) {
                    $pdo->prepare('UPDATE mobile_login_tokens SET revoked_at = COALESCE(revoked_at, NOW()) WHERE id = ?')->execute([(int)$row['id']]);
                }
                self::clearNativeLoginCookie();
                return;
            }

            $_SESSION['user_id'] = (int)$row['user_id'];
            $_SESSION['last_activity_at'] = time();
            $_SESSION['pyrk_native_app'] = 1;
            session_regenerate_id(true);

            // Rotate the secret whenever a device session is restored, while keeping the stable selector.
            $newValidator = bin2hex(random_bytes(32));
            $expires = time() + self::nativePersistentLifetime();
            $pdo->prepare('UPDATE mobile_login_tokens SET token_hash = ?, last_used_at = NOW(), expires_at = ? WHERE id = ?')
                ->execute([hash('sha256', $newValidator), date('Y-m-d H:i:s', $expires), (int)$row['id']]);
            self::setNativeLoginCookie($token['selector'] . '.' . $newValidator, $expires);
            $_COOKIE[self::nativeLoginCookieName()] = $token['selector'] . '.' . $newValidator;
        } catch (\Throwable) {
            // Fail closed for the remember token while preserving any valid normal session.
        }
    }

    private static function revokePersistentNativeLogin(): void
    {
        $token = self::parseNativeLoginCookie();
        if ($token) {
            try {
                $pdo = Database::connection();
                $pdo->prepare('UPDATE mobile_login_tokens SET revoked_at = COALESCE(revoked_at, NOW()) WHERE selector = ?')
                    ->execute([$token['selector']]);
            } catch (\Throwable) {
                // Migration may not be installed yet.
            }
        }
        self::clearNativeLoginCookie();
    }

    private static function enforceSessionTimeout(): void
    {
        if (empty($_SESSION['user_id'])) {
            return;
        }

        if (self::isNativeAppRequest()) {
            $_SESSION['last_activity_at'] = time();
            $_SESSION['pyrk_native_app'] = 1;
            return;
        }

        $minutes = (int)Security::setting('session_timeout_minutes', $GLOBALS['config']['security']['session_timeout_minutes'] ?? 180);
        $minutes = max(5, $minutes);
        $last = (int)($_SESSION['last_activity_at'] ?? time());
        if ((time() - $last) > ($minutes * 60)) {
            self::writeLogoutLog((int)$_SESSION['user_id'], 'session_timeout');
            self::clearSession(false);
            return;
        }
        $_SESSION['last_activity_at'] = time();
    }

    private static function clearSession(bool $destroyCookie): void
    {
        $_SESSION = [];
        if ($destroyCookie && ini_get('session.use_cookies')) {
            $params = session_get_cookie_params();
            setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'] ?? '', (bool)$params['secure'], (bool)$params['httponly']);
        }
        if ($destroyCookie && session_status() === PHP_SESSION_ACTIVE) {
            session_destroy();
        }
    }

    private static function writeLoginLog(?int $userId, ?string $email, bool $success, ?string $failureReason): void
    {
        try {
            $pdo = Database::connection();
            $stmt = $pdo->prepare('INSERT INTO login_logs (user_id, email, success, failure_reason, ip_address, user_agent, login_at, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW())');
            $stmt->execute([$userId, $email, $success ? 1 : 0, $failureReason, Security::clientIp(), substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 1000)]);
            if ($success) {
                $_SESSION['login_log_id'] = (int)$pdo->lastInsertId();
            }
        } catch (\Throwable) {
            // Logging must never break login.
        }
    }

    private static function writeLogoutLog(?int $userId, string $reason): void
    {
        try {
            $pdo = Database::connection();
            $loginLogId = (int)($_SESSION['login_log_id'] ?? 0);
            if ($loginLogId > 0) {
                $stmt = $pdo->prepare('UPDATE login_logs SET logout_at = NOW(), logout_reason = ? WHERE id = ? AND logout_at IS NULL');
                $stmt->execute([$reason, $loginLogId]);
            } elseif ($userId) {
                $stmt = $pdo->prepare('UPDATE login_logs SET logout_at = NOW(), logout_reason = ? WHERE user_id = ? AND success = 1 AND logout_at IS NULL ORDER BY id DESC LIMIT 1');
                $stmt->execute([$reason, $userId]);
            }
        } catch (\Throwable) {
            // Logging must never break logout.
        }
    }
}
