<?php

namespace MintyPHP\Service\Auth;

use MintyPHP\App\Module\ModuleEventDispatcher;
use MintyPHP\App\Module\ModuleEvents;
use MintyPHP\Auth;
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
use MintyPHP\Router;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Audit\AuditRecorderInterface;
use MintyPHP\Service\User\UserAccountService;
use MintyPHP\Service\User\UserTenantContextService;
use MintyPHP\Session;

/**
 * Orchestrates authentication flows: login, registration, logout, and session refresh.
 *
 * Login is a cascading validation — each step must pass before the next is attempted:
 *   1. Email verification status (unverified → redirect to verification)
 *   2. Credential check via Auth::login (email + password)
 *   3. Account active flag
 *   4. Local password login allowed (at least one tenant permits it)
 *   5. Permissions loaded + tenant context hydrated into session
 *   6. At least one active tenant assigned
 *
 * Every outcome (success or failure at any step) is recorded as an audit event.
 * On SSO-initiated login (loginUserById), steps 1-4 are skipped because the IdP
 * already authenticated the user; steps 5-6 still apply.
 */
class AuthService
{
    public function __construct(
        private readonly UserReadRepositoryInterface $userReadRepository,
        private readonly UserWriteRepositoryInterface $userWriteRepository,
        private readonly UserAccountService $userAccountService,
        private readonly UserTenantContextService $userTenantContextService,
        private readonly RememberMeService $rememberMeService,
        private readonly EmailVerificationService $emailVerificationService,
        private readonly PermissionService $permissionService,
        private readonly TenantSsoService $tenantSsoService,
        private readonly AuthSessionTenantContextService $authSessionTenantContextService,
        private readonly AuditRecorderInterface $systemAuditService,
        private readonly SessionStoreInterface $sessionStore,
        private readonly ?ModuleEventDispatcher $eventDispatcher = null
    ) {
    }

    public function login(string $email, string $password): array
    {
        $email = trim($email);

        // Check if email is verified before attempting login
        $existingUser = $this->userReadRepository->findByEmail($email);
        if ($existingUser && empty($existingUser['email_verified_at'])) {
            $this->recordAuthEvent('auth.login.failed', 'failed', [
                'error_code' => 'email_not_verified',
                'metadata' => ['email_hash' => \MintyPHP\Http\RequestContext::hashValue(strtolower($email))],
            ]);
            return [
                'ok' => false,
                'message' => 'Please verify your email first',
                'flash_key' => 'login_not_verified',
                'needs_verification' => true,
                'email' => $email,
            ];
        }

        $user = Auth::login($email, $password);

        if (!$user) {
            $this->recordAuthEvent('auth.login.failed', 'failed', [
                'error_code' => 'invalid_credentials',
                'metadata' => ['email_hash' => \MintyPHP\Http\RequestContext::hashValue(strtolower($email))],
            ]);
            return [
                'ok' => false,
                'message' => 'Email/password not valid',
                'flash_key' => 'login_invalid',
            ];
        }

        $sessionUser = $this->sessionUser();
        if (!($sessionUser['active'] ?? 1)) {
            $this->logoutWithModuleCleanup();
            $this->recordAuthEvent('auth.login.failed', 'failed', [
                'error_code' => 'account_inactive',
                'metadata' => ['email_hash' => \MintyPHP\Http\RequestContext::hashValue(strtolower($email))],
            ]);
            return [
                'ok' => false,
                'message' => 'Account is inactive',
                'flash_key' => 'login_inactive',
            ];
        }

        $userId = (int) ($sessionUser['id'] ?? 0);
        if ($userId > 0 && !$this->isLocalPasswordLoginAllowedForUser($userId)) {
            $this->logoutWithModuleCleanup();
            $this->recordAuthEvent('auth.login.failed', 'failed', [
                'error_code' => 'password_login_disabled',
                'metadata' => ['email_hash' => \MintyPHP\Http\RequestContext::hashValue(strtolower($email))],
            ]);
            return [
                'ok' => false,
                'message' => 'Password login is disabled for all assigned tenants',
                'flash_key' => 'login_password_disabled_all_tenants',
            ];
        }
        if ($userId > 0) {
            $this->permissionService->getUserPermissions($userId, true);
            $this->loadTenantDataIntoSession($userId);
            if ($this->noActiveTenant()) {
                $this->logoutWithModuleCleanup();
                $this->recordAuthEvent('auth.login.failed', 'failed', [
                    'error_code' => 'no_active_tenant',
                    'metadata' => ['email_hash' => \MintyPHP\Http\RequestContext::hashValue(strtolower($email))],
                ]);
                return [
                    'ok' => false,
                    'message' => 'No active tenant assigned',
                    'flash_key' => 'login_no_active_tenant',
                ];
            }
            $this->userWriteRepository->updateLastLogin($userId, 'local');
        }

        $this->recordAuthEvent('auth.login.success', 'success', [
            'actor_user_id' => $userId > 0 ? $userId : null,
            'actor_tenant_id' => $this->currentTenantIdFromSession(),
        ]);

        $this->eventDispatcher?->dispatch(ModuleEvents::USER_LOGIN, [
            'user_id' => $userId,
            'provider' => 'local',
        ]);

        return ['ok' => true];
    }

    // Returns true if at least ONE assigned tenant permits local login.
    // A user is only blocked if ALL tenants enforce Microsoft-only login.
    private function isLocalPasswordLoginAllowedForUser(int $userId): bool
    {
        if ($userId <= 0) {
            return false;
        }

        $availableTenants = $this->userTenantContextService->getAvailableTenants($userId);
        if (!$availableTenants) {
            return true;
        }

        foreach ($availableTenants as $tenant) {
            $tenantId = (int) ($tenant['id'] ?? 0);
            if ($tenantId <= 0) {
                continue;
            }
            if ($this->tenantSsoService->isLocalPasswordLoginAllowed($tenantId)) {
                return true;
            }
        }

        return false;
    }

    public function canLoginToTenant(int $userId, int $tenantId): bool
    {
        if ($userId <= 0 || $tenantId <= 0) {
            return false;
        }

        foreach ($this->userTenantContextService->getAvailableTenants($userId) as $tenant) {
            if ((int) ($tenant['id'] ?? 0) === $tenantId) {
                return true;
            }
        }

        return false;
    }

    public function loginUserById(int $userId, ?int $preferredTenantId = null, string $loginProvider = 'local'): array
    {
        if ($userId <= 0) {
            return ['ok' => false, 'error' => 'user_not_found'];
        }

        $user = $this->userReadRepository->find($userId);
        if (!$user) {
            return ['ok' => false, 'error' => 'user_not_found'];
        }
        if (!((int) ($user['active'] ?? 1) === 1)) {
            return ['ok' => false, 'error' => 'user_inactive'];
        }

        Session::regenerate();
        $this->sessionStore->set('user', $user);

        if ($preferredTenantId !== null && $preferredTenantId > 0) {
            $setTenant = $this->userTenantContextService->setCurrentTenant($userId, $preferredTenantId);
            if ($setTenant['ok'] ?? false) {
                $sessionUser = $this->sessionUser();
                $sessionUser['current_tenant_id'] = $preferredTenantId;
                $this->sessionStore->set('user', $sessionUser);
            }
        }

        $this->permissionService->getUserPermissions($userId, true);
        $this->loadTenantDataIntoSession($userId);
        if ($this->noActiveTenant()) {
            $this->logoutWithModuleCleanup();
            $this->recordAuthEvent('auth.login.failed', 'failed', [
                'error_code' => 'login_no_active_tenant',
                'actor_user_id' => $userId,
            ]);
            return ['ok' => false, 'error' => 'login_no_active_tenant'];
        }

        $this->userWriteRepository->updateLastLogin($userId, $loginProvider);

        $this->recordAuthEvent('auth.login.success', 'success', [
            'actor_user_id' => $userId,
            'actor_tenant_id' => $this->currentTenantIdFromSession(),
            'metadata' => ['provider' => $loginProvider],
        ]);

        $this->eventDispatcher?->dispatch(ModuleEvents::USER_LOGIN, [
            'user_id' => $userId,
            'provider' => $loginProvider,
        ]);

        return ['ok' => true, 'user' => $user];
    }

    public function register(array $data): array
    {
        $email = trim((string) ($data['email'] ?? ''));
        $result = $this->userAccountService->register($data);
        if (!($result['ok'] ?? false)) {
            return $result;
        }

        // Get the created user to send verification email
        $createdUser = $this->userReadRepository->findByEmail($email);
        if ($createdUser && isset($createdUser['id'])) {
            $userId = (int) $createdUser['id'];
            $this->emailVerificationService->sendVerification($userId);
        }

        // Don't login - user needs to verify email first
        return ['ok' => true, 'email' => $email, 'needs_verification' => true];
    }

    public function logout(): void
    {
        $actorUserId = (int) ($this->sessionUser()['id'] ?? 0);
        $actorTenantId = $this->currentTenantIdFromSession();

        $this->eventDispatcher?->dispatch(ModuleEvents::USER_LOGOUT, [
            'user_id' => $actorUserId,
        ]);

        $this->rememberMeService->forgetCurrentUser();
        $this->logoutWithModuleCleanup();
        $this->recordAuthEvent('auth.logout', 'success', [
            'actor_user_id' => $actorUserId > 0 ? $actorUserId : null,
            'actor_tenant_id' => $actorTenantId > 0 ? $actorTenantId : null,
        ]);
    }

    public function logoutDueToSessionTimeout(): void
    {
        $actorUserId = (int) ($this->sessionUser()['id'] ?? 0);
        $actorTenantId = $this->currentTenantIdFromSession();
        $this->sessionStore->remove('session_started_at');
        $this->sessionStore->remove('session_last_activity');
        $this->logoutWithModuleCleanup();
        $this->recordAuthEvent('auth.session.timeout', 'success', [
            'actor_user_id' => $actorUserId > 0 ? $actorUserId : null,
            'actor_tenant_id' => $actorTenantId > 0 ? $actorTenantId : null,
        ]);
    }

    public function logoutAndRedirect(string $target = 'login'): void
    {
        $this->logout();
        Router::redirect($target);
    }

    private function recordAuthEvent(string $eventType, string $outcome, array $context = []): void
    {
        $this->systemAuditService->record($eventType, $outcome, $context);
    }

    /**
     * Re-validates the session against the current DB state.
     *
     * Uses an authz_version counter: the DB version is bumped whenever permissions or
     * tenant assignments change. A mismatch with the session version triggers a full
     * reload of user data, permissions, and tenant context. This is the mechanism that
     * makes permission changes take effect without requiring re-login.
     */
    public function refreshSessionAuthState(int $userId): array
    {
        if ($userId <= 0) {
            return [
                'ok' => false,
                'logout_required' => true,
                'reason' => 'user_missing',
            ];
        }

        $snapshot = $this->userReadRepository->findAuthzSnapshot($userId);
        if (!$snapshot) {
            return [
                'ok' => false,
                'logout_required' => true,
                'reason' => 'user_not_found',
            ];
        }

        if ((int) ($snapshot['active'] ?? 0) !== 1) {
            return [
                'ok' => false,
                'logout_required' => true,
                'reason' => 'inactive',
            ];
        }

        // authz_version is bumped in DB whenever permissions/tenants change.
        // A mismatch means the session is stale and needs to be refreshed.
        $sessionVersion = (int) ($this->sessionUser()['authz_version'] ?? 0);
        $dbVersion = max(1, (int) ($snapshot['authz_version'] ?? 1));
        $refreshed = false;

        if ($sessionVersion !== $dbVersion) {
            $user = $this->userReadRepository->find($userId);
            if (!$user || (int) ($user['active'] ?? 0) !== 1) {
                return [
                    'ok' => false,
                    'logout_required' => true,
                    'reason' => 'inactive',
                ];
            }

            $this->sessionStore->set('user', $user);
            $this->permissionService->getUserPermissions($userId, true);
            $this->loadTenantDataIntoSession($userId);
            $refreshed = true;
        }

        if ($this->noActiveTenant()) {
            return [
                'ok' => false,
                'logout_required' => true,
                'reason' => 'no_active_tenant',
                'refreshed' => $refreshed,
            ];
        }

        return [
            'ok' => true,
            'logout_required' => false,
            'refreshed' => $refreshed,
        ];
    }

    public function loadTenantDataIntoSession(int $userId): void
    {
        $this->authSessionTenantContextService->hydrateForUser($userId);
    }

    /**
     * @return array<string, mixed>
     */
    private function sessionUser(): array
    {
        $user = $this->sessionStore->get('user', []);
        return is_array($user) ? $user : [];
    }

    /**
     * @return array<string, mixed>
     */
    private function sessionCurrentTenant(): array
    {
        $tenant = $this->sessionStore->get('current_tenant', []);
        return is_array($tenant) ? $tenant : [];
    }

    private function currentTenantIdFromSession(): int
    {
        return (int) ($this->sessionCurrentTenant()['id'] ?? 0);
    }

    private function noActiveTenant(): bool
    {
        return (bool) $this->sessionStore->get('no_active_tenant', false);
    }

    private function logoutWithModuleCleanup(): void
    {
        $this->authSessionTenantContextService->clearModuleSessionData();
        Auth::logout();
    }
}
