import {
  Injectable,
  ConflictException,
  InternalServerErrorException,
  Logger,
  UnauthorizedException,
} from '@nestjs/common';
import { ModuleRef, ContextIdFactory } from '@nestjs/core';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { AuthService as IamAuthService } from '@tria-plc/iamapi-common/module/auth/services/auth.service';
import { EUserType } from '@tria-plc/api-common/utils/enums/user.enum';
import { EOtpType } from '@tria-plc/iamapi-common/enums/otp.enum';
import { PrismaService } from '../../common/prisma.service';
import { RegisterDto, LoginDto } from './auth.dto';

type IamUserRow = {
  id: string;
  email: string;
  name: { en: string; am: string } | null;
  phone_number: string | null;
  metadata: Record<string, any> | null;
  verified_by: string | null;
};

function resolvePreferredCurrency(nationality: string | null | undefined, faydaVerified: boolean): string {
  if (faydaVerified) return 'ETB';
  const n = (nationality ?? '').toLowerCase();
  if (n.includes('ethiopi')) return 'ETB';
  if (n.includes('djibout')) return 'DJF';
  return 'USD';
}

@Injectable()
export class PassengerAuthService {
  private readonly logger = new Logger(PassengerAuthService.name);

  constructor(
    private readonly prisma: PrismaService,
    @InjectDataSource() private readonly dataSource: DataSource,
    private readonly moduleRef: ModuleRef,
    private readonly eventEmitter: EventEmitter2,
  ) {}

  private async resolveIamAuthService(req: any): Promise<IamAuthService> {
    const contextId = ContextIdFactory.getByRequest(req);
    this.moduleRef.registerRequestByContextId(req, contextId);
    return this.moduleRef.resolve(IamAuthService, contextId, { strict: false });
  }

  async register(dto: RegisterDto, req: any) {
    await this.clearPendingOrConflict(dto.email, dto.phoneNumber);

    const iamAuthService = await this.resolveIamAuthService(req);

    // IAM `signup` creates the user as PENDING/isActive=false with NO credential and
    // SMS-sends a 6-digit verification code. The account cannot log in until the code is
    // redeemed via PATCH /v1/auth/set-password. We intentionally discard the session
    // token `signup` returns — the account is not verified yet, so it must never reach
    // the client.
    await iamAuthService.signup({
      email: dto.email,
      username: dto.username,
      phoneNumber: dto.phoneNumber,
      userType: EUserType.INDIVIDUAL,
      name: dto.name,
    });

    const iamRows = await this.dataSource.query<IamUserRow[]>(
      `SELECT id, email, name, phone_number, metadata FROM iam.users WHERE email = $1 LIMIT 1`,
      [dto.email],
    );
    if (!iamRows.length) {
      await this.compensateIamSignup(dto.email);
      throw new InternalServerErrorException('Account creation failed. Please try again.');
    }
    const iamUserId = iamRows[0].id;

    // The Prisma "passenger satellite" (Passenger + wallet + loyalty) is NOT provisioned
    // here — `login()` lazy-provisions it on first successful login, so satellites exist
    // only for verified users who complete set-password and sign in.
    return {
      iamUserId,
      email: dto.email,
      phoneNumber: dto.phoneNumber,
      requiresPasswordSetup: true,
    };
  }

  /**
   * Immediate-activation account creation used by the payment-gated guest-checkout
   * "create account" path only. Unlike the public `register()` (OTP-gated), this creates a
   * ready-to-use account from the password entered at checkout and provisions the passenger
   * satellite synchronously so the booking can attach to it. Do NOT wire this to the public
   * registration form — that flow must stay behind SMS verification.
   */
  async registerWithPassword(
    dto: {
      email: string;
      username: string;
      phoneNumber: string;
      name: { en: string; am: string };
      password: string;
    },
    req: any,
  ): Promise<{ iamUserId: string; passengerId: string }> {
    await this.clearPendingOrConflict(dto.email, dto.phoneNumber);

    const iamAuthService = await this.resolveIamAuthService(req);
    await iamAuthService.signupWithPassword({
      email: dto.email,
      username: dto.username,
      phoneNumber: dto.phoneNumber,
      userType: EUserType.INDIVIDUAL,
      name: dto.name,
      password: dto.password,
      confirmPassword: dto.password,
    });

    const iamRows = await this.dataSource.query<IamUserRow[]>(
      `SELECT id, email, name, phone_number, metadata FROM iam.users WHERE email = $1 LIMIT 1`,
      [dto.email],
    );
    if (!iamRows.length) {
      await this.compensateIamSignup(dto.email);
      throw new InternalServerErrorException('Account creation failed. Please try again.');
    }
    const iamUserId = iamRows[0].id;

    try {
      const result = await this.provisionPassengerSatellite({ iamUserId, auditAction: 'USER_REGISTERED' });
      return { iamUserId, passengerId: result.passengerId };
    } catch {
      await this.compensateIamSignup(dto.email);
      throw new InternalServerErrorException('Account creation failed. Please try again.');
    }
  }

  async resendRegistrationCode(
    dto: { email: string; phoneNumber: string },
    req: any,
  ): Promise<{ sent: boolean }> {
    // Only regenerate for accounts still pending password setup. A fully-registered user
    // should use forgot-password instead. Always return { sent: true } to avoid leaking
    // whether the email/phone maps to a pending account (enumeration guard).
    const users = await this.dataSource.query<{ email: string; phone_number: string }[]>(
      `SELECT email, phone_number FROM iam.users
        WHERE email = $1 AND phone_number = $2 AND has_set_password = false LIMIT 1`,
      [dto.email, dto.phoneNumber],
    );
    if (!users.length) return { sent: true };

    const iamAuthService = await this.resolveIamAuthService(req);
    try {
      await iamAuthService.generateVerificationCode({
        email: users[0].email,
        phoneNumber: users[0].phone_number,
        type: EOtpType.VERIFY_PHONE_NUMBER,
      });
    } catch (err) {
      this.logger.error(
        `[PassengerAuthService] resend registration code failed for ${dto.email}`,
        (err as Error).message,
      );
    }
    return { sent: true };
  }

  async login(dto: LoginDto, req: any) {
    const iamAuthService = await this.resolveIamAuthService(req);

    let iamResult: { token: string; refreshToken: string } | { mfaRequired: boolean };
    try {
      iamResult = await iamAuthService.login({ email: dto.email, password: dto.password });
    } catch {
      this.eventEmitter.emit('auth.login.failed', { email: dto.email });
      throw new UnauthorizedException('Invalid credentials');
    }

    if ('mfaRequired' in iamResult && iamResult.mfaRequired) {
      return iamResult;
    }

    const { token, refreshToken } = iamResult as { token: string; refreshToken: string };

    // `dto.email` may hold an email OR a phone number (passengers without an email log in
    // with their phone). Match on either so the post-auth lookup works regardless of which
    // identifier was used.
    const iamRows = await this.dataSource.query<IamUserRow[]>(
      `SELECT id, email, name, phone_number, metadata FROM iam.users WHERE email = $1 OR phone_number = $1 LIMIT 1`,
      [dto.email],
    );
    const iamUser = iamRows[0];
    if (!iamUser) {
      throw new InternalServerErrorException('IAM user not found after successful authentication');
    }

    // Find existing Passenger record or lazy-provision one on first login
    let passenger = await this.prisma.passenger.findUnique({
      where: { iamUserId: iamUser.id },
      select: { id: true },
    });

    if (!passenger) {
      const result = await this.provisionPassengerSatellite({
        iamUserId: iamUser.id,
        auditAction: 'USER_AUTO_PROVISIONED',
      });
      passenger = { id: result.passengerId };
    }

    return {
      token,
      refreshToken,
      user: { id: iamUser.id, iamUserId: iamUser.id, email: iamUser.email, passengerId: passenger.id },
    };
  }

  private async provisionPassengerSatellite(data: {
    iamUserId: string;
    auditAction: string;
  }): Promise<{ passengerId: string }> {
    return this.prisma.$transaction(async (tx) => {
      const passenger = await tx.passenger.create({
        data: { iamUserId: data.iamUserId },
      });
      await tx.loyaltyAccount.create({ data: { passengerId: passenger.id } });
      await tx.walletAccount.create({ data: { passengerId: passenger.id } });
      await tx.userPreferences.create({ data: { iamUserId: data.iamUserId } });
      await tx.auditLog.create({
        data: {
          iamUserId: data.iamUserId,
          action: data.auditAction,
          entityType: 'User',
          entityId: data.iamUserId,
          newData: { iamUserId: data.iamUserId },
        },
      });
      return { passengerId: passenger.id };
    });
  }

  async logout(user: any, req: any) {
    const iamAuthService = await this.resolveIamAuthService(req);
    await iamAuthService.logout(user);
    return { success: true, message: 'Logged out successfully' };
  }

  async getProfile(iamUserId: string) {
    const [passenger, iamRows] = await Promise.all([
      this.prisma.passenger.findUnique({
        where: { iamUserId },
        include: { loyalty: true, wallet: true },
      }),
      this.dataSource.query<IamUserRow[]>(
        `SELECT id, email, name, phone_number, metadata, verified_by FROM iam.users WHERE id = $1 LIMIT 1`,
        [iamUserId],
      ),
    ]);

    if (!passenger) throw new Error('Passenger not found');
    const iam = iamRows[0];
    const meta = iam?.metadata ?? {};
    const faydaVerified = iam?.verified_by === 'fayda';
    // A Fayda-verified holder is an Ethiopian national ID holder, so default nationality to
    // Ethiopian when the metadata doesn't carry it explicitly.
    const nationality = meta.nationality ?? (faydaVerified ? 'ETHIOPIAN' : null);
    // Fayda stores gender as { am, en }; tolerate a legacy plain string too.
    const gender =
      meta.gender && typeof meta.gender === 'object'
        ? (meta.gender.en ?? meta.gender.am ?? null)
        : (meta.gender ?? null);
    // birthdate is persisted as ISO by the Fayda upsert; tolerate a "/"-separated legacy value.
    const rawDob = meta.dateOfBirth ?? meta.birthdate ?? null;
    const dateOfBirth = rawDob ? String(rawDob).replace(/\//g, '-') : null;

    return {
      // The web User object keys on `id` (the IAM user id) — the login response returns it, so
      // this profile refresh MUST too, otherwise fetchProfile() overwrites the logged-in user
      // with an id-less object and everything guarded on `user.id` (passenger-form prefill,
      // save-details userId) silently breaks.
      id: iamUserId,
      iamUserId,
      // Top-level passengerId keeps the profile shape consistent with the login
      // response so the web User object always carries it (the JWT does not).
      passengerId: passenger.id,
      email: iam?.email ?? null,
      phone: iam?.phone_number ?? null,
      fullName: iam?.name?.en ?? iam?.name?.am ?? null,
      gender,
      dateOfBirth,
      nationality,
      faydaVerified,
      faydaSub: meta.sub ?? null,
      preferredCurrency: resolvePreferredCurrency(nationality, faydaVerified),
      createdAt: passenger.createdAt,
      passenger: {
        id: passenger.id,
        preferredLanguage: passenger.preferredLanguage,
        loyalty: passenger.loyalty
          ? { tier: passenger.loyalty.tier, pointsBalance: passenger.loyalty.pointsBalance, lifetimePoints: passenger.loyalty.lifetimePoints }
          : null,
        wallet: passenger.wallet
          ? { balanceMinor: passenger.wallet.balanceMinor, currency: passenger.wallet.currency }
          : null,
      },
    };
  }

  async listUsers(filters: { search?: string; role?: string; status?: string; page?: number; pageSize?: number }) {
    const page = filters.page ?? 1;
    const pageSize = filters.pageSize ?? 20;
    const offset = (page - 1) * pageSize;

    const params: any[] = [];
    const conditions: string[] = [];

    if (filters.search) {
      params.push(`%${filters.search}%`);
      conditions.push(`(u.email ILIKE $${params.length} OR (u.name->>'en') ILIKE $${params.length})`);
    }
    if (filters.role) {
      params.push(`%${filters.role}%`);
      conditions.push(`r.key ILIKE $${params.length}`);
    }
    if (filters.status) {
      const active = filters.status === 'ACTIVE';
      params.push(active);
      conditions.push(`u.is_active = $${params.length}`);
    }

    const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';

    const baseQuery = `
      FROM iam.users u
      LEFT JOIN iam.user_roles ur ON ur.user_id = u.id
      LEFT JOIN iam.roles r ON r.id = ur.role_id
      ${where}
    `;

    const countParams = [...params];
    const [rows, countRows] = await Promise.all([
      this.dataSource.query(
        `SELECT DISTINCT u.id, u.email, u.name, u.phone_number, u.is_active, u.status, u.created_at,
                r.key as role_key, r.name as role_name
         ${baseQuery}
         ORDER BY u.created_at DESC
         LIMIT $${params.length + 1} OFFSET $${params.length + 2}`,
        [...params, pageSize, offset],
      ),
      this.dataSource.query(
        `SELECT COUNT(DISTINCT u.id) as count ${baseQuery}`,
        countParams,
      ),
    ]);

    const items = rows.map((u: any) => ({
      id: u.id,
      email: u.email,
      fullName: u.name?.en ?? u.name?.am ?? '',
      role: u.role_key ?? '',
      status: u.is_active ? 'ACTIVE' : 'INACTIVE',
      lastLogin: u.metadata?.lastLogin ?? null,
      createdAt: u.created_at,
    }));

    return { items, total: parseInt(countRows[0]?.count ?? '0'), page, pageSize };
  }

  async createUser(data: { email: string; fullName: string; role: string; password: string; status?: string }) {
    const existing = await this.dataSource.query<{ id: string }[]>(
      `SELECT id FROM iam.users WHERE email = $1 LIMIT 1`,
      [data.email],
    );
    if (existing.length) throw new ConflictException('Email already registered');

    // Derive username from email local-part; ensure uniqueness by appending a short suffix if taken
    const baseUsername = data.email.split('@')[0].toLowerCase().replace(/[^a-z0-9._-]/g, '');
    const taken = await this.dataSource.query<{ username: string }[]>(
      `SELECT username FROM iam.users WHERE username LIKE $1 LIMIT 10`,
      [`${baseUsername}%`],
    );
    const takenSet = new Set(taken.map((r) => r.username));
    let username = baseUsername;
    let suffix = 1;
    while (takenSet.has(username)) {
      username = `${baseUsername}${suffix++}`;
    }

    // Hash with argon2 — same algorithm the IAM login uses (verifyPassword in auth.service.js)
    const { hashPassword } = await import('@tria-plc/api-common/utils/argon');
    const passwordHash = await hashPassword(data.password);

    await this.dataSource.query(
      `INSERT INTO iam.users (email, username, name, user_type, status, is_active)
       VALUES ($1, $2, $3::jsonb, 'employee', $4, $5)`,
      [
        data.email,
        username,
        JSON.stringify({ en: data.fullName, am: data.fullName }),
        data.status === 'INACTIVE' ? 'pending' : 'accepted',
        data.status !== 'INACTIVE',
      ],
    );

    // Insert credential with correct column `password` and is_active = true
    // so the IAM login SQL (find-user-for-login.sql) can find and verify it
    const newUser = await this.dataSource.query<{ id: string }[]>(
      `SELECT id FROM iam.users WHERE email = $1 LIMIT 1`, [data.email],
    );
    if (newUser.length) {
      await this.dataSource.query(
        `UPDATE iam.user_credentials SET is_active = false WHERE user_id = $1 AND is_active = true`,
        [newUser[0].id],
      );
      await this.dataSource.query(
        `INSERT INTO iam.user_credentials (user_id, password, is_active) VALUES ($1, $2, true)`,
        [newUser[0].id, passwordHash],
      );
    }

    // Assign the selected role in iam.user_roles
    const rows = await this.dataSource.query(
      `SELECT id, email, name, is_active, created_at FROM iam.users WHERE email = $1 LIMIT 1`,
      [data.email],
    );
    const u = rows[0];

    if (data.role && u) {
      try {
        const roleRows = await this.dataSource.query<{ id: string }[]>(
          `SELECT id FROM iam.roles WHERE key = $1 LIMIT 1`,
          [data.role],
        );
        if (roleRows.length) {
          await this.dataSource.query(
            `INSERT INTO iam.user_roles (user_id, role_id)
             VALUES ($1, $2)
             ON CONFLICT DO NOTHING`,
            [u.id, roleRows[0].id],
          );
        }
      } catch {
        // non-fatal — role assignment failure should not block user creation
      }
    }

    return {
      id: u.id, email: u.email,
      fullName: data.fullName, role: data.role,
      status: u.is_active ? 'ACTIVE' : 'INACTIVE',
      createdAt: u.created_at,
    };
  }

  async updateUser(id: string, data: { fullName?: string; role?: string; status?: string }) {
    const rows = await this.dataSource.query(
      `SELECT id, name, is_active FROM iam.users WHERE id = $1 LIMIT 1`,
      [id],
    );
    if (!rows.length) throw new ConflictException('User not found');
    const existing = rows[0];
    const name = data.fullName ? { en: data.fullName, am: data.fullName } : existing.name;
    const isActive = data.status ? data.status === 'ACTIVE' : existing.is_active;
    await this.dataSource.query(
      `UPDATE iam.users SET name = $1::jsonb, is_active = $2, updated_at = NOW() WHERE id = $3`,
      [JSON.stringify(name), isActive, id],
    );

    // Update role: remove existing user_roles then assign the new one
    if (data.role) {
      try {
        const roleRows = await this.dataSource.query<{ id: string }[]>(
          `SELECT id FROM iam.roles WHERE key = $1 LIMIT 1`,
          [data.role],
        );
        if (roleRows.length) {
          await this.dataSource.query(`DELETE FROM iam.user_roles WHERE user_id = $1`, [id]);
          await this.dataSource.query(
            `INSERT INTO iam.user_roles (user_id, role_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
            [id, roleRows[0].id],
          );
        }
      } catch {
        // non-fatal
      }
    }

    return { id, fullName: (name as any)?.en, role: data.role, status: isActive ? 'ACTIVE' : 'INACTIVE' };
  }

  async deleteUser(id: string) {
    await this.dataSource.query(`DELETE FROM iam.users WHERE id = $1`, [id]);
    return { success: true };
  }

  async resetUserPassword(id: string, tempPassword: string) {
    const { hashPassword } = await import('@tria-plc/api-common/utils/argon');
    const passwordHash = await hashPassword(tempPassword);
    // Deactivate existing credentials first (IAM keeps history, only one active at a time)
    await this.dataSource.query(
      `UPDATE iam.user_credentials SET is_active = false WHERE user_id = $1 AND is_active = true`,
      [id],
    );
    // Insert new active credential
    await this.dataSource.query(
      `INSERT INTO iam.user_credentials (user_id, password, is_active) VALUES ($1, $2, true)`,
      [id, passwordHash],
    );
    return { success: true, message: 'Password reset successfully' };
  }

  async requestFaydaPasswordSetup(phoneNumber: string, req: any): Promise<{ sent: boolean }> {
    const phone = this.standardizePhone(phoneNumber);

    const users = await this.dataSource.query<{ id: string; email: string }[]>(
      `SELECT id, email FROM iam.users WHERE phone_number = $1 AND verified_by = 'fayda' LIMIT 1`,
      [phone],
    );
    this.logger.log(`requestFaydaPasswordSetup: phone=${phone} found=${users.length > 0}`);
    // Return success regardless to avoid phone enumeration
    if (!users.length) return { sent: true };
    const u = users[0];

    const iamAuthService = await this.resolveIamAuthService(req);
    await iamAuthService.generateVerificationCode({
      email: u.email,
      phoneNumber: phone,
      type: EOtpType.SET_PASSWORD,
    });

    return { sent: true };
  }

  async verifyFaydaAndLogin(
    phoneNumber: string,
    otp: string,
  ): Promise<{ token: string; refreshToken: string; requiresPassword: boolean; iamUserId: string }> {
    const phone = this.standardizePhone(phoneNumber);

    const users = await this.dataSource.query<{
      id: string;
      email: string;
      name: { en: string; am: string } | null;
      username: string;
      phone_number: string | null;
      has_set_password: boolean;
    }[]>(
      `SELECT id, email, name, username, phone_number, has_set_password
         FROM iam.users WHERE phone_number = $1 AND verified_by = 'fayda' LIMIT 1`,
      [phone],
    );
    if (!users.length) throw new UnauthorizedException('Invalid phone number or OTP');
    const u = users[0];

    const verifications = await this.dataSource.query<{
      id: string; verification_code: string; attempt_count: number;
    }[]>(
      `SELECT id, verification_code, attempt_count FROM iam.user_verifications
        WHERE user_id = $1 AND otp_type = 'set-password' AND "isUsed" = false AND expires_at > NOW()
        ORDER BY created_at DESC LIMIT 1`,
      [u.id],
    );
    if (!verifications.length) throw new UnauthorizedException('Invalid phone number or OTP');
    const v = verifications[0];

    if (v.attempt_count >= 5) {
      await this.dataSource.query(
        `UPDATE iam.user_verifications SET "isUsed" = true WHERE id = $1`, [v.id],
      );
      throw new UnauthorizedException('Too many attempts. Request a new code.');
    }

    await this.dataSource.query(
      `UPDATE iam.user_verifications SET attempt_count = attempt_count + 1 WHERE id = $1`, [v.id],
    );

    const { verifyPassword } = await import('@tria-plc/api-common/utils/argon');
    const valid = await verifyPassword(otp, v.verification_code);
    if (!valid) throw new UnauthorizedException('Invalid phone number or OTP');

    await this.dataSource.query(
      `UPDATE iam.user_verifications SET "isUsed" = true WHERE id = $1`, [v.id],
    );

    const userInfo = {
      id: u.id,
      email: u.email ?? '',
      name: u.name ?? { en: '', am: '' },
      userType: 'individual',
      status: 'accepted',
      hasSetPassword: u.has_set_password,
      isPhoneNumberVerified: false,
      hasFinishedRegistration: false,
      hasFinishedDMSOnboarding: false,
      username: u.username,
      phoneNumber: u.phone_number ?? '',
      roles: [],
      permissions: [],
      employee: [],
    };

    const sessions = await this.dataSource.query<{ id: string }[]>(
      `INSERT INTO iam.sessions
          (id, email, device, "userInfo", expiry_time, refresh_count, status, user_id)
        VALUES (gen_random_uuid(), $1, 'fayda-otp-setup', $2::jsonb, NOW() + INTERVAL '1 day', 0, 'ACTIVE', $3)
        ON CONFLICT (user_id, device) DO UPDATE
          SET status = 'ACTIVE', "userInfo" = EXCLUDED."userInfo",
              expiry_time = NOW() + INTERVAL '1 day', updated_at = NOW()
        RETURNING id`,
      [u.email ?? '', JSON.stringify(userInfo), u.id],
    );

    const { generateToken, generateRefreshToken } = await import('@tria-plc/api-common/utils/token');
    const token = generateToken({ id: sessions[0].id });
    const refreshToken = generateRefreshToken({ id: sessions[0].id });

    return { token, refreshToken, requiresPassword: !u.has_set_password, iamUserId: u.id };
  }

  private standardizePhone(phone: string): string {
    const digits = phone.replace(/\D/g, '');
    if (digits.startsWith('251')) return `+${digits}`;
    if (digits.startsWith('0')) return `+251${digits.slice(1)}`;
    return `+${digits}`;
  }

  /**
   * Pre-signup uniqueness guard. Throws `ConflictException` only when a
   * *fully-registered* account (`has_set_password = true`) already owns the
   * email or phone. Abandoned PENDING signups — where the user received the OTP
   * but never completed `set-password` — are deleted so this fresh attempt can
   * re-create the account and re-send the code, instead of being blocked with a
   * 409 forever. Matches `resendRegistrationCode`'s `has_set_password = false`
   * notion of "still pending".
   */
  private async clearPendingOrConflict(email: string, phoneNumber: string): Promise<void> {
    const matches = await this.dataSource.query<
      { id: string; email: string; has_set_password: boolean }[]
    >(
      `SELECT id, email, has_set_password FROM iam.users WHERE email = $1 OR phone_number = $2`,
      [email, phoneNumber],
    );
    if (!matches.length) return;
    if (matches.some((u) => u.has_set_password)) {
      throw new ConflictException('Email or phone already registered');
    }
    // Every match is an abandoned pending signup — clean it up so the caller can proceed.
    for (const u of matches) {
      await this.compensateIamSignup(u.email);
    }
  }

  private async compensateIamSignup(email: string): Promise<void> {
    try {
      const rows = await this.dataSource.query<{ id: string }[]>(
        `SELECT id FROM iam.users WHERE email = $1 LIMIT 1`,
        [email],
      );
      if (!rows.length) return;
      const iamUserId = rows[0].id;

      // Discover every table in the iam schema that has a FK pointing at iam.users.id
      const fkDeps = await this.dataSource.query<{ table_name: string; column_name: string }[]>(`
        SELECT kcu.table_name, kcu.column_name
        FROM information_schema.table_constraints tc
        JOIN information_schema.key_column_usage kcu
          ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
        JOIN information_schema.referential_constraints rc
          ON tc.constraint_name = rc.constraint_name
        JOIN information_schema.key_column_usage ccu
          ON rc.unique_constraint_name = ccu.constraint_name
        WHERE ccu.table_schema = 'iam' AND ccu.table_name = 'users' AND ccu.column_name = 'id'
          AND tc.table_schema = 'iam' AND tc.constraint_type = 'FOREIGN KEY'
      `);

      for (const { table_name, column_name } of fkDeps) {
        await this.dataSource.query(
          `DELETE FROM iam.${table_name} WHERE ${column_name} = $1`,
          [iamUserId],
        );
      }

      await this.dataSource.query(`DELETE FROM iam.users WHERE id = $1`, [iamUserId]);
    } catch (err) {
      this.logger.error(`[PassengerAuthService] IAM compensating cleanup failed for ${email}`, (err as Error).message);
    }
  }
}
