import { randomInt } from "node:crypto";
import { $inject, Alepha } from "alepha";
import type { FileController } from "alepha/api/files";
import { CacheProvider } from "alepha/cache";
import { DateTimeProvider } from "alepha/datetime";
import { $logger } from "alepha/logger";
import {
  CryptoProvider,
  InvalidCredentialsError,
  type UserAccount,
} from "alepha/security";
import { BadRequestError, UnauthorizedError } from "alepha/server";
import type { OAuth2Profile } from "alepha/server/auth";
import { $client } from "alepha/server/links";
import { FileSystemProvider } from "alepha/system";
import { SessionAudits } from "../audits/SessionAudits.ts";
import { UserAudits } from "../audits/UserAudits.ts";
import type { UserEntity } from "../entities/users.ts";
import { UserNotifications } from "../notifications/UserNotifications.ts";
import { RealmProvider } from "../providers/RealmProvider.ts";
import { UsernameSlugger } from "./UsernameSlugger.ts";

export class SessionService {
  protected readonly alepha = $inject(Alepha);
  protected readonly fsp = $inject(FileSystemProvider);
  protected readonly dateTimeProvider = $inject(DateTimeProvider);
  protected readonly cryptoProvider = $inject(CryptoProvider);
  protected readonly log = $logger();
  protected readonly realmProvider = $inject(RealmProvider);
  protected readonly fileController = $client<FileController>();
  protected readonly cacheProvider = $inject(CacheProvider);
  protected readonly usernameSlugger = $inject(UsernameSlugger);

  protected userAudits(realmName?: string) {
    const realm = this.realmProvider.getRealm(realmName);
    if (realm.features.audits) {
      return this.alepha.inject(UserAudits);
    }
    return undefined;
  }

  protected sessionAudits(realmName?: string) {
    const realm = this.realmProvider.getRealm(realmName);
    if (realm.features.audits) {
      return this.alepha.inject(SessionAudits);
    }
    return undefined;
  }

  protected userNotifications(realmName?: string) {
    const realm = this.realmProvider.getRealm(realmName);
    if (realm.features.notifications) {
      return this.alepha.inject(UserNotifications);
    }
    return undefined;
  }

  public users(userRealmName?: string) {
    return this.realmProvider.userRepository(userRealmName);
  }

  public sessions(userRealmName?: string) {
    return this.realmProvider.sessionRepository(userRealmName);
  }

  public identities(userRealmName?: string) {
    return this.realmProvider.identityRepository(userRealmName);
  }

  /**
   * Check if user should be auto-promoted to admin based on adminEmails/adminUsernames settings.
   * If user matches and doesn't have admin role, promote them.
   */
  protected async ensureAdminRole(
    user: {
      id: string;
      email?: string | null;
      username?: string | null;
      roles: string[];
    },
    userRealmName?: string,
  ): Promise<boolean> {
    if (user.roles.includes("admin")) return false;

    const realm = this.realmProvider.getRealm(userRealmName);
    const settings = await realm.getSettings();
    const { name } = realm;
    const adminEmails = settings.adminEmails ?? [];
    const adminUsernames = settings.adminUsernames ?? [];

    const isAdminByEmail = user.email && adminEmails.includes(user.email);
    const isAdminByUsername =
      user.username && adminUsernames.includes(user.username);

    if (!isAdminByEmail && !isAdminByUsername) return false;

    // Promote to admin
    user.roles = [...user.roles.filter((r) => r !== "admin"), "admin"];
    await this.users(userRealmName).updateById(user.id, { roles: user.roles });

    const reason = isAdminByEmail ? "adminEmails" : "adminUsernames";
    this.log.info(`User auto-promoted to admin via ${reason} setting`, {
      userId: user.id,
      email: user.email,
      username: user.username,
      realm: name,
    });

    await this.userAudits(userRealmName)?.user.log("role_change", {
      resourceType: "user",
      userId: user.id,
      userEmail: user.email ?? undefined,
      userRealm: name,
      resourceId: user.id,
      description: `User auto-promoted to admin via ${reason} setting`,
      metadata: { addedRole: "admin", reason },
    });

    return true;
  }

  /**
   * Generate a unique username from an OAuth profile.
   *
   * Routes through {@link UsernameSlugger}, which is the same code path as
   * `username: "email"` registration. The OAuth profile's email is the
   * primary signal; if absent (rare — most IDPs return one), we fall back
   * to `profile.name`, then to a random handle. The slugger applies the
   * realm's `usernameBlocklist` and retries on collision.
   */
  protected async generateUniqueUsername(
    profile: OAuth2Profile,
    _realmSettings: any,
    _users: any,
    realmName?: string,
  ): Promise<string> {
    const seed =
      profile.email ??
      profile.name ??
      `user-${Math.random().toString(36).slice(2, 8)}`;
    const base = this.usernameSlugger.slug(seed);
    return this.usernameSlugger.pickAvailable(realmName, base);
  }

  /**
   * Random delay to prevent timing attacks (50-200ms)
   * Uses cryptographically secure random number generation
   */
  protected randomDelay(): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, randomInt(50, 201)));
  }

  protected static readonly LOGIN_CACHE_NAME = "login-rate-limit";

  /**
   * Check if a login key is currently locked out.
   * Read-only — does not increment the counter.
   */
  protected async isLoginLocked(key: string, max: number): Promise<boolean> {
    try {
      const count = await this.cacheProvider.getTyped<number>(
        SessionService.LOGIN_CACHE_NAME,
        key,
      );
      return count != null && count >= max;
    } catch (error) {
      this.log.warn(
        "Failed to check login rate limit, allowing attempt",
        error,
      );
      return false;
    }
  }

  /**
   * Record a failed login attempt. Uses getTyped + setTyped (not incr) so that
   * each write refreshes the TTL — implementing sliding-window behavior.
   *
   * Returns `true` if this failure just crossed the lockout threshold.
   */
  protected async recordFailedLogin(
    key: string,
    max: number,
    windowMs: number,
  ): Promise<boolean> {
    try {
      const count =
        (await this.cacheProvider.getTyped<number>(
          SessionService.LOGIN_CACHE_NAME,
          key,
        )) ?? 0;
      const newCount = count + 1;
      await this.cacheProvider.setTyped(
        SessionService.LOGIN_CACHE_NAME,
        key,
        newCount,
        { ttl: windowMs },
      );
      return newCount === max;
    } catch (error) {
      this.log.warn("Failed to record failed login attempt", error);
      return false;
    }
  }

  /**
   * Validate user credentials and return the user if valid.
   */
  public async login(
    provider: string,
    username: string,
    password: string,
    userRealmName?: string,
  ): Promise<UserEntity> {
    const realm = this.realmProvider.getRealm(userRealmName);
    const settings = await realm.getSettings();
    const { name } = realm;
    const { loginRateLimit } = settings;
    const isEmail = username.includes("@");
    const isPhone = /^[+\d][\d\s()-]+$/.test(username);
    const isUsername = !isEmail && !isPhone;
    const identities = this.identities(userRealmName);
    const users = this.users(userRealmName);

    // IP rate limit check (global, cross-realm) — before any DB work
    const request = this.alepha.store.get("alepha.http.request");
    const ipKey = request?.ip ? `login:ip:${request.ip}` : undefined;

    if (ipKey) {
      const ipLocked = await this.isLoginLocked(
        ipKey,
        loginRateLimit.ipMaxAttempts,
      );
      if (ipLocked) {
        this.log.warn("Login blocked — IP rate limit exceeded", {
          ip: request?.ip,
        });
        throw new InvalidCredentialsError();
      }
    }

    await this.randomDelay();

    try {
      const where = users.createQueryWhere();

      where.realm = name;

      if (settings.username !== "none" && isUsername) {
        // validate username format if regex is provided
        if (settings.usernameRegExp) {
          const regex = new RegExp(settings.usernameRegExp);
          if (!regex.test(username)) {
            this.log.warn("Username does not match required format", {
              provider,
              username,
              realm: name,
            });

            await this.sessionAudits(userRealmName)?.auth.log("login", {
              userRealm: name,
              success: false,
              description: "Username does not match required format",
              metadata: { provider, username },
            });

            throw new InvalidCredentialsError();
          }
        }
        where.username = { ilike: username };
      } else if (settings.email !== "none" && isEmail) {
        where.email = username;
      } else if (settings.phoneNumber !== "none" && isPhone) {
        where.phoneNumber = username;
      } else {
        this.log.warn("Invalid login identifier format", {
          provider,
          username,
          realm: name,
        });

        await this.sessionAudits(userRealmName)?.auth.log("login", {
          userRealm: name,
          success: false,
          description: "Invalid login identifier format",
          metadata: { provider, username },
        });

        throw new InvalidCredentialsError();
      }

      const user = await users.findOne({ where });
      if (!user) {
        this.log.warn("User not found during login attempt", {
          provider,
          username,
          realm: name,
        });

        await this.sessionAudits(userRealmName)?.auth.log("login", {
          userRealm: name,
          success: false,
          description: "User not found",
          metadata: { provider, username },
        });

        // Only increment IP counter (no user ID to track)
        if (ipKey) {
          const justLocked = await this.recordFailedLogin(
            ipKey,
            loginRateLimit.ipMaxAttempts,
            loginRateLimit.windowMs,
          );
          if (justLocked) {
            await this.sessionAudits(userRealmName)?.security.log(
              "rate_limited",
              {
                userRealm: name,
                success: false,
                description:
                  "IP temporarily locked due to too many failed login attempts",
                metadata: { ip: request?.ip },
              },
            );
          }
        }

        throw new InvalidCredentialsError();
      }

      // Check if user account is enabled
      if (!user.enabled) {
        this.log.warn("Login attempt for disabled account", {
          userId: user.id,
          realm: name,
        });

        await this.sessionAudits(userRealmName)?.auth.log("login", {
          userRealm: name,
          success: false,
          resourceId: user.id,
          description: "Login attempt for disabled account",
          metadata: { provider, username },
        });

        throw new InvalidCredentialsError();
      }

      // Account rate limit check (per-realm)
      const accountKey = `login:account:${name}:${user.id}`;
      const accountLocked = await this.isLoginLocked(
        accountKey,
        loginRateLimit.accountMaxAttempts,
      );
      if (accountLocked) {
        this.log.warn("Login blocked — account rate limit exceeded", {
          userId: user.id,
          realm: name,
        });
        throw new InvalidCredentialsError();
      }

      const identity = await identities.getOne({
        where: {
          provider: { eq: provider },
          userId: { eq: user.id },
        },
      });

      const storedPassword = identity.password;
      if (!storedPassword) {
        this.log.error("Identity has no password configured", {
          provider,
          username,
          identityId: identity.id,
          realm: name,
        });
        throw new InvalidCredentialsError();
      }

      const valid = await this.cryptoProvider.verifyPassword(
        password,
        storedPassword,
      );

      if (!valid) {
        this.log.warn("Invalid password during login attempt", {
          provider,
          username,
          realm: name,
        });

        await this.sessionAudits(userRealmName)?.auth.log("login", {
          userRealm: name,
          success: false,
          resourceId: user.id,
          description: "Invalid password",
          metadata: { provider, username },
        });

        // Record failed attempt on both IP and account counters
        if (ipKey) {
          const ipJustLocked = await this.recordFailedLogin(
            ipKey,
            loginRateLimit.ipMaxAttempts,
            loginRateLimit.windowMs,
          );
          if (ipJustLocked) {
            await this.sessionAudits(userRealmName)?.security.log(
              "rate_limited",
              {
                userRealm: name,
                success: false,
                description:
                  "IP temporarily locked due to too many failed login attempts",
                metadata: { ip: request?.ip },
              },
            );
          }
        }

        const accountJustLocked = await this.recordFailedLogin(
          accountKey,
          loginRateLimit.accountMaxAttempts,
          loginRateLimit.windowMs,
        );
        if (accountJustLocked) {
          await this.sessionAudits(userRealmName)?.security.log(
            "rate_limited",
            {
              userRealm: name,
              resourceId: user.id,
              success: false,
              description:
                "Account temporarily locked due to too many failed login attempts",
              metadata: { userId: user.id },
            },
          );

          // Notify user about account lockout
          if (user.email) {
            const lockoutMinutes = Math.round(loginRateLimit.windowMs / 60_000);
            await this.userNotifications(userRealmName)?.accountLockout.push({
              contact: user.email,
              variables: { email: user.email, lockoutMinutes },
            });
          }
        }

        throw new InvalidCredentialsError();
      }

      await this.sessionAudits(userRealmName)?.auth.log("login", {
        userId: user.id,
        userEmail: user.email ?? undefined,
        userRealm: name,
        resourceId: user.id,
        description: `User logged in via ${provider}`,
        metadata: { provider, username },
      });

      // Auto-promote to admin if configured
      await this.ensureAdminRole(user, userRealmName);

      return user;
    } catch (error) {
      if (error instanceof InvalidCredentialsError) {
        throw error;
      }

      this.log.warn("Error during login attempt", error);

      throw new InvalidCredentialsError();
    }
  }

  public async createSession(
    user: UserAccount,
    expiresIn: number,
    userRealmName?: string,
    clientId?: string,
  ) {
    this.log.trace("Creating session", { userId: user.id, expiresIn });

    const request = this.alepha.store.get("alepha.http.request");
    const refreshToken = this.cryptoProvider.randomUUID();

    const expiresAt = this.dateTimeProvider
      .now()
      .add(expiresIn, "seconds")
      .toISOString();

    const nowIso = this.dateTimeProvider.nowISOString();

    const session = await this.sessions(userRealmName).create({
      userId: user.id,
      expiresAt,
      lastUsedAt: nowIso,
      ip: request?.ip,
      country: request?.geo?.country,
      userAgent: request?.userAgent,
      refreshToken,
      clientId,
    });

    await this.users(userRealmName).updateById(user.id, {
      lastLoginAt: nowIso,
    });

    this.log.info("Session created", {
      sessionId: session.id,
      userId: user.id,
      ip: request?.ip,
    });

    return {
      refreshToken,
      sessionId: session.id,
    };
  }

  public async refreshSession(refreshToken: string, userRealmName?: string) {
    this.log.trace("Refreshing session");

    // getOne() throws DbEntityNotFoundError if not found — never returns null.
    // No null check needed here.
    const session = await this.sessions(userRealmName).getOne({
      where: {
        refreshToken: { eq: refreshToken },
      },
    });

    const now = this.dateTimeProvider.now();
    const expiresAt = this.dateTimeProvider.of(session.expiresAt);

    if (this.dateTimeProvider.of(session.expiresAt) < now) {
      this.log.debug("Session expired during refresh", {
        sessionId: session.id,
        userId: session.userId,
      });
      await this.sessions(userRealmName).deleteById(session.id);
      throw new UnauthorizedError("Session expired");
    }

    // Idle timeout check — opt-in via realm settings.
    // Falls back to createdAt when lastUsedAt is null (pre-migration rows or
    // sessions that never refreshed since the column was introduced).
    const realm = this.realmProvider.getRealm(userRealmName);
    const settings = await realm.getSettings();
    const idleMs = settings.refreshToken?.expirationIdle;
    if (idleMs && idleMs > 0) {
      const lastUsedRef = session.lastUsedAt ?? session.createdAt;
      const idleSince = now.diff(this.dateTimeProvider.of(lastUsedRef));
      if (idleSince > idleMs) {
        this.log.info("Session expired (idle timeout)", {
          sessionId: session.id,
          userId: session.userId,
          idleMs: idleSince,
          thresholdMs: idleMs,
        });
        await this.sessions(userRealmName).deleteById(session.id);
        throw new UnauthorizedError("Session expired");
      }
    }

    const user = await this.users(userRealmName).getOne({
      where: {
        id: { eq: session.userId },
      },
    });

    // Check if user account is still enabled
    if (!user.enabled) {
      this.log.warn("Session refresh for disabled account", {
        userId: user.id,
        sessionId: session.id,
      });
      await this.sessions(userRealmName).deleteById(session.id);
      throw new UnauthorizedError("Account disabled");
    }

    // Auto-promote to admin if configured (handles "I promote you admin" case)
    await this.ensureAdminRole(user, userRealmName);

    // Update lastUsedAt — sliding-window for idle timeout enforcement.
    await this.sessions(userRealmName).updateById(session.id, {
      lastUsedAt: now.toISOString(),
    });

    this.log.debug("Session refreshed", {
      sessionId: session.id,
      userId: session.userId,
    });

    return {
      user,
      expiresIn: expiresAt.unix() - now.unix(),
      sessionId: session.id,
    };
  }

  public async deleteSession(refreshToken: string, userRealmName?: string) {
    this.log.trace("Deleting session");

    // Get session info before deletion for audit
    const session = await this.sessions(userRealmName).findOne({
      where: { refreshToken: { eq: refreshToken } },
    });

    await this.sessions(userRealmName).deleteOne({
      refreshToken,
    });
    this.log.debug("Session deleted");

    if (session) {
      const { name } = this.realmProvider.getRealm(userRealmName);

      await this.sessionAudits(userRealmName)?.auth.log("logout", {
        userId: session.userId,
        userRealm: name,
        sessionId: session.id,
        description: "User logged out",
      });
    }
  }

  public async link(
    provider: string,
    profile: OAuth2Profile,
    userRealmName?: string,
  ) {
    this.log.trace("Linking OAuth2 profile", {
      provider,
      profileSub: profile.sub,
      email: profile.email,
    });

    const realm = this.realmProvider.getRealm(userRealmName);
    const identities = this.identities(userRealmName);
    const users = this.users(userRealmName);

    const identity = await identities.findOne({
      where: {
        provider,
        providerUserId: profile.sub,
      },
    });

    // existing identity found, return associated user
    if (identity) {
      this.log.debug("Existing identity found", {
        provider,
        identityId: identity.id,
        userId: identity.userId,
      });

      const user = await users.getById(identity.userId);

      await this.sessionAudits(userRealmName)?.auth.log("login", {
        userId: user.id,
        userEmail: user.email ?? undefined,
        userRealm: realm.name,
        resourceId: user.id,
        description: `User logged in via OAuth2 (${provider})`,
        metadata: { provider, providerUserId: profile.sub },
      });

      // Auto-promote to admin if configured
      await this.ensureAdminRole(user, userRealmName);

      return user;
    }

    if (!profile.email) {
      this.log.debug("OAuth2 profile has no email, returning profile as-is", {
        provider,
        profileSub: profile.sub,
      });
      return {
        id: profile.sub,
        ...profile,
      };
    }

    const existing = await users.findOne({
      where: {
        realm: realm.name,
        email: profile.email,
      },
    });

    if (existing) {
      // Refuse auto-link if the OAuth provider explicitly says email is not verified
      if (profile.email_verified === false) {
        this.log.warn(
          "OAuth2 profile email not verified by provider, refusing auto-link",
          { provider, email: profile.email, userId: existing.id },
        );
        throw new BadRequestError(
          "Cannot link account: email not verified by provider",
        );
      }

      this.log.debug("Linking OAuth2 profile to existing user by email", {
        provider,
        profileSub: profile.sub,
        userId: existing.id,
        email: profile.email,
      });
      await identities.create({
        provider,
        providerUserId: profile.sub,
        userId: existing.id,
      });

      await this.sessionAudits(userRealmName)?.auth.log("login", {
        userId: existing.id,
        userEmail: existing.email ?? undefined,
        userRealm: realm.name,
        resourceId: existing.id,
        description: `OAuth2 identity linked to existing user (${provider})`,
        metadata: { provider, providerUserId: profile.sub, linked: true },
      });

      // Auto-promote to admin if configured
      await this.ensureAdminRole(existing, userRealmName);

      return existing;
    }

    const realmSettings = await realm.getSettings();
    const adminEmails = realmSettings?.adminEmails ?? [];
    const isAdmin = profile.email && adminEmails.includes(profile.email);

    if (realmSettings?.registrationAllowed === false && !isAdmin) {
      this.log.warn("Registration not allowed for realm via OAuth2", {
        provider,
        userRealmName,
      });
      throw new BadRequestError("Account doesn't exist");
    }

    const username = await this.generateUniqueUsername(
      profile,
      realmSettings,
      users,
      userRealmName,
    );

    const user = await users.create({
      realm: realm.name,
      username,
      email: profile.email,
      firstName: profile.given_name,
      lastName: profile.family_name,
      // we trust the OAuth2 provider
      emailVerified: true,
      roles: realmSettings.defaultRoles,
    });

    if (profile.picture) {
      this.log.debug("Fetching user profile picture from OAuth2 provider", {
        provider,
        url: profile.picture,
      });
      try {
        const response = await fetch(profile.picture);
        const file = this.fsp.createFile({
          response,
        });
        if (response.ok && response.body) {
          const fileEntity = await this.fileController.uploadFile(
            {
              body: { file },
            },
            {
              user,
            },
          );
          await users.updateById(user.id, { picture: fileEntity.id });
        }
      } catch (error) {
        this.log.warn("Failed to fetch user profile picture", error);
      }
    }

    await this.identities(userRealmName).create({
      provider,
      providerUserId: profile.sub,
      userId: user.id,
    });

    this.log.info("New user created via OAuth2 link", {
      provider,
      userId: user.id,
      email: user.email,
      username: user.username,
    });

    // Audit: user created via OAuth
    await this.userAudits(userRealmName)?.user.log("create", {
      resourceType: "user",
      userId: user.id,
      userEmail: user.email ?? undefined,
      userRealm: realm.name,
      resourceId: user.id,
      description: `User created via OAuth2 (${provider})`,
      metadata: {
        provider,
        providerUserId: profile.sub,
        username: user.username,
        email: user.email,
      },
    });

    // Audit: login event
    await this.sessionAudits(userRealmName)?.auth.log("login", {
      userId: user.id,
      userEmail: user.email ?? undefined,
      userRealm: realm.name,
      resourceId: user.id,
      description: `First login via OAuth2 (${provider})`,
      metadata: { provider, providerUserId: profile.sub, firstLogin: true },
    });

    // Auto-promote to admin if configured
    await this.ensureAdminRole(user, userRealmName);

    return user;
  }
}
