import { type Kysely } from 'kysely';
import { type JsonObject } from 'type-fest';

import {
  CoopError,
  ErrorType,
  type ErrorInstanceData,
} from '../../../utils/errors.js';
import { isUniqueViolationError } from '../../../utils/kysely.js';
import {
  makeKyselyTransactionWithRetry,
  type KyselyTransactionWithRetry,
} from '../../../utils/kyselyTransactionWithRetry.js';
import { removeUndefinedKeys } from '../../../utils/misc.js';
import { type ModerationConfigServicePg } from '../dbTypes.js';

const userStrikeThresholdSelection = [
  'id',
  'org_id as orgId',
  'threshold',
  'actions',
  'action_parameters as actionParameters',
] as const;

export default class UserStrikeOperations {
  private readonly transactionWithRetry: KyselyTransactionWithRetry<ModerationConfigServicePg>;

  constructor(
    private readonly pgQuery: Kysely<ModerationConfigServicePg>,
    private readonly pgQueryReplica: Kysely<ModerationConfigServicePg>,
  ) {
    this.transactionWithRetry = makeKyselyTransactionWithRetry(this.pgQuery);
  }

  async getUserStrikeThresholds(opts: {
    orgId: string;
    readFromReplica?: boolean;
  }) {
    const { orgId, readFromReplica } = opts;
    const pgQuery = this.#getPgQuery(readFromReplica);
    const query = pgQuery
      .selectFrom('public.user_strike_thresholds')
      .select(userStrikeThresholdSelection)
      .where('org_id', '=', orgId);
    return query.execute();
  }

  async createUserStrikeThreshold(opts: {
    orgId: string;
    thresholdSettings: {
      threshold: number;
      actions: string[];
      actionParameters?: JsonObject;
    };
  }) {
    const { orgId: org_id, thresholdSettings } = opts;

    try {
      return await this.pgQuery
        .insertInto('public.user_strike_thresholds')
        .values({
          org_id,
          threshold: thresholdSettings.threshold,
          actions: thresholdSettings.actions,
          action_parameters: thresholdSettings.actionParameters ?? {},
        })
        .returning(userStrikeThresholdSelection)
        .executeTakeFirstOrThrow();
    } catch (e: unknown) {
      throw isUniqueViolationError(e)
        ? makeThresholdAlreadyExistsError({ shouldErrorSpan: true })
        : e;
    }
  }

  async updateUserStrikeThreshold(opts: {
    orgId: string;
    thresholdSettings: {
      id: string;
      threshold?: number;
      actions?: string[];
      actionParameters?: JsonObject;
    };
  }) {
    const { orgId, thresholdSettings } = opts;

    try {
      return await this.pgQuery
        .updateTable('public.user_strike_thresholds')
        .set(
          removeUndefinedKeys({
            threshold: thresholdSettings.threshold,
            actions: thresholdSettings.actions,
            action_parameters: thresholdSettings.actionParameters,
          }),
        )
        .where('id', '=', thresholdSettings.id)
        .where('org_id', '=', orgId)
        .returning(userStrikeThresholdSelection)
        .executeTakeFirstOrThrow();
    } catch (e: unknown) {
      throw isUniqueViolationError(e)
        ? makeThresholdAlreadyExistsError({ shouldErrorSpan: true })
        : e;
    }
  }
  /**
   * Set all user strike thresholds for an organization.
   * this replaces all existing thresholds with the new set
   */
  async setAllUserStrikeThresholds(opts: {
    orgId: string;
    thresholds: readonly {
      threshold: number;
      actions: readonly string[];
      actionParameters?: JsonObject;
    }[];
  }) {
    await this.transactionWithRetry(async (trx) => {
      await trx
        .deleteFrom('public.user_strike_thresholds')
        .where('org_id', '=', opts.orgId)
        .execute();
      for (const threshold of opts.thresholds) {
        await trx
          .insertInto('public.user_strike_thresholds')
          .values({
            org_id: opts.orgId,
            threshold: threshold.threshold,
            actions: [...threshold.actions],
            action_parameters: threshold.actionParameters ?? {},
          })
          .onConflict((oc) =>
            oc.columns(['org_id', 'threshold']).doUpdateSet({
              actions: [...threshold.actions],
              action_parameters: threshold.actionParameters ?? {},
            }),
          )
          .execute();
      }
    });
  }

  async deleteUserStrikeThreshold(opts: {
    orgId: string;
    threshold: number;
    id: string;
  }) {
    const { orgId, id, threshold } = opts;

    const rowsDeleted = await this.pgQuery
      .deleteFrom('public.user_strike_thresholds')
      .where('org_id', '=', orgId)
      .where('id', '=', id)
      .where('threshold', '=', threshold)
      .execute();

    return rowsDeleted.length === 1;
  }

  #getPgQuery(readFromReplica: boolean = false) {
    return readFromReplica ? this.pgQueryReplica : this.pgQuery;
  }
}

export type UserStrikeThresholdErrorType =
  'UserStrikeThresholdAlreadyExistsError';

// TODO: throw this error on failed policy creation/update when appropriate.
export const makeThresholdAlreadyExistsError = (data: ErrorInstanceData) =>
  new CoopError({
    status: 409,
    type: [ErrorType.UniqueViolation],
    title:
      'A rule with that threshold value already exists in this organization.',
    name: 'UserStrikeThresholdAlreadyExistsError',
    ...data,
  });
