// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0

import { type BootstrapConfig, CrudRepository } from '@citrineos/base';
import type {
  AggregateOptions,
  Attributes,
  FindAndCountOptions,
  FindOptions,
  ModelStatic,
  WhereOptions,
} from 'sequelize';
import { QueryTypes } from 'sequelize';
import { type Model, type Sequelize } from 'sequelize-typescript';
import { type ILogObj, Logger } from 'tslog';
import { DefaultSequelizeInstance } from '../util.js';

export class SequelizeRepository<T extends Model<any, any>> extends CrudRepository<T> {
  protected s: Sequelize;
  protected namespace: string;
  protected logger: Logger<ILogObj>;

  constructor(
    config: BootstrapConfig,
    namespace: string,
    logger?: Logger<ILogObj>,
    sequelizeInstance?: Sequelize,
  ) {
    super();
    this.s = sequelizeInstance ?? DefaultSequelizeInstance.getInstance(config, logger);
    this.namespace = namespace;
    this.logger = logger
      ? logger.getSubLogger({ name: this.constructor.name })
      : new Logger<ILogObj>({ name: this.constructor.name });
  }

  async readByKey(
    tenantId: number,
    key: string | number,
    namespace: string = this.namespace,
  ): Promise<T | undefined> {
    return await this.s.models[namespace].findByPk(key).then((row) => row as T);
  }

  async readAllByQuery(
    tenantId: number,
    query: object,
    namespace: string = this.namespace,
  ): Promise<T[]> {
    const { where, ...rest } = query as FindOptions<any>;
    return await this.s.models[namespace]
      .findAll({ where: { ...where, tenantId }, ...rest })
      .then((row) => row as T[]);
  }

  async readAllBySqlString(
    tenantId: number,
    sqlString: string,
    _namespace: string = this.namespace,
  ): Promise<object[]> {
    return await this.s.query(`${sqlString}`, { type: QueryTypes.SELECT });
  }

  async readNextValue(
    tenantId: number,
    columnName: string,
    query?: object,
    startValue?: number,
    namespace: string = this.namespace,
  ): Promise<number> {
    const options = query ? (query as AggregateOptions<any>) : undefined;
    const maxValue = await this.s.models[namespace].max(columnName, options);
    if (maxValue === null || maxValue === undefined) {
      return startValue ?? 1;
    }
    if (typeof maxValue !== 'number' || isNaN(maxValue)) {
      throw new Error(`Max value ${maxValue} on ${columnName} is invalid.`);
    }
    return maxValue + 1;
  }

  async existsByKey(
    tenantId: number,
    key: string,
    namespace: string = this.namespace,
  ): Promise<boolean> {
    return await this.s.models[namespace].findByPk(key).then((row) => row !== null);
  }

  async existByQuery(
    tenantId: number,
    query: object,
    namespace: string = this.namespace,
  ): Promise<number> {
    const { where, ...rest } = query as FindOptions<any>;
    return await this.s.models[namespace]
      .findAll({ where: { ...where, tenantId }, ...rest })
      .then((row) => row.length);
  }

  async findAndCount(
    tenantId: number,
    options: Omit<FindAndCountOptions<Attributes<T>>, 'group'>,
    namespace: string = this.namespace,
  ): Promise<{ rows: T[]; count: number }> {
    return (this.s.models[namespace] as ModelStatic<T>).findAndCountAll(options);
  }

  protected async _create(
    tenantId: number,
    value: T,
    _namespace: string = this.namespace,
  ): Promise<T> {
    return await value.save();
  }

  protected async _readOrCreateByQuery(
    tenantId: number,
    query: object,
    namespace: string = this.namespace,
  ): Promise<[T, boolean]> {
    const { where, ...rest } = query as FindOptions<any>;
    return await this.s.models[namespace]
      .findOrCreate({ where: { ...where, tenantId }, ...rest })
      .then((result) => [result[0] as T, result[1]]);
  }

  protected async _updateByKey(
    tenantId: number,
    value: Partial<T>,
    key: string,
    namespace: string = this.namespace,
  ): Promise<T | undefined> {
    const model = this.s.models[namespace] as ModelStatic<T>;
    const pk = model.primaryKeyAttribute;

    const { tenantId: _ignored, ...safeValue } = value as any;

    const where: WhereOptions<any> = {
      [pk]: key,
      tenantId,
    };

    const row = await model.findOne({ where });

    if (!row) return undefined;

    await row.update(safeValue);

    return row.reload();
  }

  protected async _updateAllByQuery(
    tenantId: number,
    value: Partial<T>,
    query: object,
    namespace: string = this.namespace,
  ): Promise<T[]> {
    const model = this.s.models[namespace] as ModelStatic<T>;

    const where: WhereOptions<any> = {
      ...(query as any).where,
      tenantId,
    };

    const rows = await model.findAll({
      ...(query as any),
      where,
    });

    if (rows.length === 0) return [];

    for (const row of rows) {
      await row.update(value);
    }

    return Promise.all(rows.map((r) => r.reload()));
  }

  protected async _deleteByKey(
    tenantId: number,
    key: string,
    namespace: string = this.namespace,
  ): Promise<T | undefined> {
    return this.s.transaction(async (transaction) => {
      const entryToDelete = await this.s.models[namespace]
        .findByPk(key, { transaction })
        .then((row) => row as T);

      if (entryToDelete) {
        await this.s.models[namespace].destroy({
          where: { [this.s.models[namespace].primaryKeyAttribute]: key },
          transaction,
        });
        return entryToDelete;
      } else {
        return undefined;
      }
    });
  }

  protected async _deleteAllByQuery(
    tenantId: number,
    query: object,
    namespace: string = this.namespace,
  ): Promise<T[]> {
    return this.s.transaction(async (transaction) => {
      const entriesToDelete = await this.s.models[namespace]
        .findAll({ ...query, transaction })
        .then((rows) => rows as T[]);

      const deletedCount = await this.s.models[namespace].destroy({ ...query, transaction });

      if (entriesToDelete.length === deletedCount) {
        return entriesToDelete;
      } else {
        throw new Error(`Deleted ${deletedCount} entries, expected ${entriesToDelete.length}`);
      }
    });
  }
}
