import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { IsNull, Repository } from 'typeorm';
import { IdempotencyKey } from 'src/common/entities';
import { IdempotencyStatusEnum } from 'src/common/enum/idempotency-status.enum';
import { AppLoggerService } from 'src/common/services/logger.service';
import { handleKnownErrors } from 'src/common/utils/handle-error.util';
import { ERROR_CODES } from 'src/common/constants/error-string-constants';

@Injectable()
export class IdempotencyRepository {
  constructor(
    @InjectRepository(IdempotencyKey)
    private readonly repo: Repository<IdempotencyKey>,
    private readonly logger: AppLoggerService,
  ) {}

  // Atomic claim for a brand-new key: ON CONFLICT DO NOTHING means two concurrent requests
  // with the same key race safely — exactly one gets a row back. TypeORM has no first-class
  // "insert or tell me if it already existed" primitive, so raw SQL is used here deliberately.
  // The conflict target includes user_id so two different callers never collide on the same
  // (idempotency_key, endpoint) pair.
  async tryInsertPending(
    idempotencyKey: string,
    endpoint: string,
    requestHash: string,
    userId: number | null,
  ): Promise<{ id: number } | null> {
    try {
      const rows = await this.repo.query(
        `INSERT INTO idempotency_key (idempotency_key, endpoint, request_hash, status, user_id)
         VALUES ($1, $2, $3, $4, $5)
         ON CONFLICT (idempotency_key, endpoint, user_id) DO NOTHING
         RETURNING id`,
        [idempotencyKey, endpoint, requestHash, IdempotencyStatusEnum.PENDING, userId],
      );
      return rows.length ? { id: rows[0].id } : null;
    } catch (error) {
      handleKnownErrors(ERROR_CODES.IDEMPOTENCY_KEY_CONFLICT, error);
    }
  }

  async findByKeyAndEndpoint(idempotencyKey: string, endpoint: string, userId: number | null): Promise<IdempotencyKey | null> {
    try {
      return await this.repo.findOne({
        where: { idempotencyKey, endpoint, userId: userId === null ? IsNull() : userId },
      });
    } catch (error) {
      handleKnownErrors(ERROR_CODES.IDEMPOTENCY_KEY_CONFLICT, error);
    }
  }

  // Best-effort counter, mirrors markCompleted/markFailed: swallows failures so a logging/DB
  // blip on this side channel never blocks the actual claim/replay/conflict decision.
  async incrementHitCount(id: number): Promise<void> {
    try {
      await this.repo.increment({ id }, 'hitCount', 1);
    } catch (error) {
      this.logger.error('Failed to increment idempotency hit count', '', { id, error: error.message });
    }
  }

  // A previously FAILED attempt is allowed to be retried under the same key: this is a
  // compare-and-swap back to PENDING, so only one of several concurrent retries wins.
  // user_id is compared with IS NOT DISTINCT FROM (not =) so this also works when userId is null.
  async tryReclaimFailed(
    idempotencyKey: string,
    endpoint: string,
    requestHash: string,
    userId: number | null,
  ): Promise<{ id: number } | null> {
    try {
      const rows = await this.repo.query(
        `UPDATE idempotency_key
         SET status = $4, request_hash = $3, response_data = NULL, updated_at = CURRENT_TIMESTAMP
         WHERE idempotency_key = $1 AND endpoint = $2 AND status = $5 AND user_id IS NOT DISTINCT FROM $6
         RETURNING id`,
        [idempotencyKey, endpoint, requestHash, IdempotencyStatusEnum.PENDING, IdempotencyStatusEnum.FAILED, userId],
      );
      return rows.length ? { id: rows[0].id } : null;
    } catch (error) {
      handleKnownErrors(ERROR_CODES.IDEMPOTENCY_KEY_CONFLICT, error);
    }
  }

  async markCompleted(id: number, responseData: object | null): Promise<void> {
    try {
      await this.repo.update({ id }, { status: IdempotencyStatusEnum.COMPLETED, responseData });
    } catch (error) {
      this.logger.error('Failed to mark idempotency key as completed', '', { id, error: error.message });
    }
  }

  async markFailed(id: number): Promise<void> {
    try {
      await this.repo.update({ id }, { status: IdempotencyStatusEnum.FAILED });
    } catch (error) {
      this.logger.error('Failed to mark idempotency key as failed', '', { id, error: error.message });
    }
  }
}
