import { OAuthClientGrantTypeEnum } from '@api/graphql';
import { OAuthCreateAccessTokenService } from '@app/o-auth/access-token/application/create/o-auth-create-access-token.service';
import { OAuthFindApplicationService } from '@app/o-auth/application';
import { OAuthCreateCredentialService } from '@app/o-auth/credential/application/create/o-auth-create-credential.service';
import { OAuthCreateRefreshTokenService } from '@app/o-auth/refresh-token/application/create/o-auth-create-refresh-token.service';
import { oAuthResolveApplicationClient } from '@app/o-auth/shared';
import {
  ACCOUNT_LOADER,
  CREDENTIAL_VERIFIER,
  type IAccountLoader,
  type ICredentialVerifier,
  type IJwtSigner,
  JWT_SIGNER,
} from '@aurora/modules/authentication/domain/ports';
import { Inject, Injectable } from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import { InvalidGrantError } from '../../../domain/errors';
import type {
  OAuthCreateTokenPasswordInput,
  OAuthCreateTokenResponse,
} from '../../../domain/flows.types';

@Injectable()
export class OAuthCreateTokenPasswordService {
  constructor(
    @Inject(CREDENTIAL_VERIFIER)
    private readonly credentialVerifier: ICredentialVerifier,
    @Inject(JWT_SIGNER)
    private readonly jwtSigner: IJwtSigner,
    @Inject(ACCOUNT_LOADER)
    private readonly accountLoader: IAccountLoader,
    private readonly findApplication: OAuthFindApplicationService,
    private readonly createAccessToken: OAuthCreateAccessTokenService,
    private readonly createRefreshToken: OAuthCreateRefreshTokenService,
    private readonly createCredential: OAuthCreateCredentialService,
  ) {}

  async main(payload: OAuthCreateTokenPasswordInput): Promise<OAuthCreateTokenResponse> {
    // 1. Authenticate the application (Basic auth client_id = OAuthApplication.code) and
    // resolve the technical OAuthClient for the PASSWORD grant. Raises invalid_client /
    // unauthorized_client per RFC 6749 §5.2.
    const client = await oAuthResolveApplicationClient(this.findApplication, {
      clientId: payload.clientId,
      clientSecret: payload.clientSecret,
      grantType: OAuthClientGrantTypeEnum.PASSWORD,
    });

    // 2. Verify resource-owner credentials. All failure modes (user-not-found,
    // password-mismatch, inactive-account) collapse to a single `null` to prevent
    // enumeration attacks (RFC 6819 §5.3.4).
    const verified = await this.credentialVerifier.verify(payload.username, payload.password);
    if (!verified) throw new InvalidGrantError();

    // 3. Confirm the account is still loadable. Catches edge cases where the user exists
    // but the account was deleted between verify() and load().
    const account = await this.accountLoader.loadById(verified.accountId);
    if (!account) throw new InvalidGrantError();

    // 4. Sign the access JWT + opaque refresh token.
    const access = await this.jwtSigner.signAccess({ aci: account.id });
    const refresh = await this.jwtSigner.signRefresh();

    // 5. Persist all three rows. Best-effort sequential — no transaction. If a later
    // step fails, the retention job (Group 11) will purge the orphans by TTL.
    const accessTokenId = randomUUID();
    const refreshTokenId = randomUUID();

    await this.createAccessToken.main({
      id: accessTokenId,
      clientId: client.id,
      accountId: account.id,
      token: access.token,
      name: null,
      isRevoked: false,
      expiresAt: access.expiresAt,
    });

    await this.createRefreshToken.main({
      id: refreshTokenId,
      accessTokenId,
      token: refresh.token,
      isRevoked: false,
      expiresAt: refresh.expiresAt,
    });

    await this.createCredential.main({
      id: randomUUID(),
      grantType: 'PASSWORD',
      accountId: account.id,
      accessTokenId,
      refreshTokenId,
      ipAddress: payload.ipAddress,
      userAgent: truncate(payload.userAgent, 255),
    });

    return {
      access_token: access.token,
      token_type: 'Bearer',
      expires_in: secondsUntil(access.expiresAt),
      refresh_token: refresh.token,
    };
  }
}

function truncate(value: string | null, max: number): string | null {
  if (value === null) return null;
  return value.length > max ? value.slice(0, max) : value;
}

function secondsUntil(date: Date): number {
  return Math.max(0, Math.floor((date.getTime() - Date.now()) / 1000));
}
