// otp.repository.ts

import { Injectable } from "@nestjs/common";

import { InjectRepository } from "@nestjs/typeorm";

import { FindOptionsWhere, Repository } from "typeorm";

import { OtpVerification } from "./otp.entity";

type Target = { phone?: string; email?: string };

@Injectable()
export class OtpRepository {
  constructor(
    @InjectRepository(
      OtpVerification
    )
    private readonly repository: Repository<OtpVerification>
  ) {}

  // ---------------------------------------------------------------------------
  // Find By Phone
  // ---------------------------------------------------------------------------

  async findByPhone(
    phone: string
  ) {
    return this.repository.findOne({
      where: {
        phone,
      },
    });
  }

  // ---------------------------------------------------------------------------
  // Find By Email
  // ---------------------------------------------------------------------------

  async findByEmail(
    email: string
  ) {
    return this.repository.findOne({
      where: {
        email,
      },
    });
  }

  // ---------------------------------------------------------------------------
  // Find By Target (any named channel)
  // ---------------------------------------------------------------------------

  /**
   * OR across every channel the target names. A code sent to both phone and
   * email lives in ONE row carrying both values, so a verify that quotes either
   * one resolves the same row — that is what makes "sent to both, verify with
   * either" work.
   */
  private whereForTarget(
    target: Target
  ): FindOptionsWhere<OtpVerification>[] {
    const where: FindOptionsWhere<OtpVerification>[] =
      [];

    if (target.email)
      where.push({
        email: target.email,
      });

    if (target.phone)
      where.push({
        phone: target.phone,
      });

    return where;
  }

  async findAllByTarget(
    target: Target
  ) {
    const where =
      this.whereForTarget(target);

    if (!where.length) return [];

    // Newest first: a target that somehow overlaps two legacy single-channel
    // rows should resolve to the most recently issued code, not an arbitrary one.
    return this.repository.find({
      where,
      order: { updatedAt: "DESC" },
    });
  }

  async findByTarget(
    target: Target
  ) {
    const [
      newest,
    ] = await this.findAllByTarget(
      target
    );

    return newest ?? null;
  }

  // ---------------------------------------------------------------------------
  // Replace OTP (upsert across every channel the target names)
  // ---------------------------------------------------------------------------

  /**
   * Drop every row this target overlaps and write a single fresh one holding
   * all its channels.
   *
   * `phone` and `email` are each UNIQUE, so a dual-channel send can collide with
   * up to two pre-existing single-channel rows (say an old signup code on the
   * phone and a reset code on the email). Merging into one row instead of
   * updating in place is what keeps that from raising a unique violation, and it
   * preserves the single-use guarantee: consuming the code deletes one row and
   * kills every channel it was sent to at once.
   *
   * "Last code sent wins" was already the behaviour between any two flows
   * sharing this table — this only widens it from one channel to all of them.
   */
  async replaceOtp(
    target: Target,
    otp: string
  ): Promise<{
    record: OtpVerification;
    rotated: boolean;
  }> {
    const existing =
      await this.findAllByTarget(
        target
      );

    if (existing.length) {
      await this.repository.remove(
        existing
      );
    }

    const record =
      await this.repository.save(
        this.repository.create({
          phone: target.phone,
          email: target.email,
          otp,
          verified: false,
        })
      );

    return {
      record,
      rotated: existing.length > 0,
    };
  }

  // ---------------------------------------------------------------------------
  // Mark Verified
  // ---------------------------------------------------------------------------

  async markVerified(
    otpVerification: OtpVerification
  ) {
    otpVerification.verified =
      true;

    return this.repository.save(
      otpVerification
    );
  }

  // ---------------------------------------------------------------------------
  // Delete OTP (single-use consume)
  // ---------------------------------------------------------------------------

  // Hard delete so the unique `phone`/`email` rows are freed and a fresh code can
  // be requested for the same target on the next action.
  async deleteOtp(
    otpVerification: OtpVerification
  ) {
    return this.repository.remove(
      otpVerification
    );
  }
}
