/**
 * Login use case. Rate limit first, then credential check with timing
 * equalization, then session creation. Server is the only authority.
 */
import type { PasswordHasher, SessionRepository, UserRepository } from '../domain/ports.js';
import type { User } from '../domain/user.js';
import { normalizeEmail } from '../domain/user.js';
import { sessionExpiry } from '../domain/session.js';
import { InvalidCredentialsError, RateLimitedError } from '../domain/errors.js';
import type { LoginRateLimiter } from './login-rate-limiter.js';

export interface LoginInput {
  email: string;
  password: string;
}

export interface LoginResult {
  user: User;
  /** Opaque raw token; only its hash is persisted. */
  token: string;
  tokenHash: string;
  expiresAt: Date;
}

export interface LoginDeps {
  users: UserRepository;
  sessions: SessionRepository;
  hasher: PasswordHasher;
  rateLimiter: LoginRateLimiter;
  generateToken: () => string;
  hashToken: (token: string) => string;
}

export class Login {
  private dummyHashPromise: Promise<string> | undefined;

  constructor(private readonly deps: LoginDeps) {}

  async execute(input: LoginInput): Promise<LoginResult> {
    const email = normalizeEmail(input.email);

    const decision = this.deps.rateLimiter.consume(email);
    if (!decision.allowed) {
      throw new RateLimitedError(decision.retryAfterMs);
    }

    const record = await this.deps.users.findByEmail(email);

    // Timing equalization: unknown email still pays one argon2 verify so
    // response timing does not reveal whether the account exists.
    const hashToCheck = record?.passwordHash ?? (await this.dummyHash());
    const valid = await this.deps.hasher.verify(hashToCheck, input.password);

    if (!record || !valid) {
      this.deps.rateLimiter.recordFailure(email);
      throw new InvalidCredentialsError();
    }

    this.deps.rateLimiter.reset(email);

    const token = this.deps.generateToken();
    const tokenHash = this.deps.hashToken(token);
    const expiresAt = sessionExpiry();
    await this.deps.sessions.create(record.id, tokenHash, expiresAt);

    return { user: record, token, tokenHash, expiresAt };
  }

  private async dummyHash(): Promise<string> {
    this.dummyHashPromise ??= this.deps.hasher.hash('identity-dummy-password');
    return this.dummyHashPromise;
  }
}
