import {
  Op,
  type Model,
  type ModelStatic,
  type WhereOptions,
} from 'sequelize';
import Utils from '@/db/utils';
import type { CurrentUser, DbApiOptions } from '@/db/api/types';

/**
 * Shared generic-repository helpers for the `db/api` layer. They cover the
 * methods that are byte-identical across every entity (`remove`, `deleteByIds`)
 * and the single-field autocomplete, parameterized by the model. Entity-specific
 * methods (`create`/`update`/`bulkImport`/`findBy`/`findAll`) stay in each
 * repository.
 *
 * Tenant scoping: mutations and id lookups must never cross organizations. The
 * helpers derive the tenant clause from `options.currentUser` and add
 * `organizationId` to the `where` for non-global users. Global-access users
 * (system roles) and internal calls without an authenticated user (seeders,
 * system jobs) are not org-restricted — matching `findAll`'s existing behavior.
 */

/**
 * The `{ organizationId }` clause to AND into a tenant-owned query, or `{}` when
 * the caller is global-access or has no resolvable organization. Mirrors the
 * scoping logic used by every entity `findAll`.
 */
export function tenantWhere(currentUser?: CurrentUser): {
  organizationId?: string;
} {
  const globalAccess = currentUser?.app_role?.globalAccess === true;
  const organizationId =
    currentUser?.organizations?.id ?? currentUser?.organizationId ?? null;
  if (globalAccess || !organizationId) {
    return {};
  }
  return { organizationId };
}

/**
 * Finds a record by id, scoped to the caller's tenant. Returns null when the row
 * is absent OR belongs to another organization (cross-tenant ids are
 * indistinguishable from missing). Use in place of `model.findByPk(id)` for the
 * lookup that precedes a tenant-owned update/remove/read-by-id.
 */
export async function findOwnedByPk<M extends Model>(
  model: ModelStatic<M>,
  id: string,
  options?: DbApiOptions,
): Promise<M | null> {
  const where: WhereOptions = { id, ...tenantWhere(options?.currentUser) };
  return model.findOne({ where, transaction: options?.transaction });
}

/** Finds a record by id and soft-deletes it, scoped to the caller's tenant. */
export async function removeRecord<M extends Model>(
  model: ModelStatic<M>,
  id: string,
  options?: DbApiOptions,
): Promise<M | null> {
  const transaction = options?.transaction;
  const record = await findOwnedByPk(model, id, options);

  if (!record) {
    return null;
  }

  await record.destroy({ transaction });
  return record;
}

/**
 * Deletes every record whose id is in `ids` and belongs to the caller's tenant
 * (within the caller's transaction). Cross-tenant ids are silently skipped.
 */
export async function deleteRecordsByIds<M extends Model>(
  model: ModelStatic<M>,
  ids: string[],
  options?: DbApiOptions,
): Promise<M[]> {
  const transaction = options?.transaction;
  const where: WhereOptions = {
    id: { [Op.in]: ids },
    ...tenantWhere(options?.currentUser),
  };

  const records = await model.findAll({ where, transaction });

  for (const record of records) {
    await record.destroy({ transaction });
  }

  return records;
}

/**
 * Autocomplete over a single label column: `{ id, label }` rows matching `query`
 * (by id or case-insensitive substring), scoped to the tenant unless global.
 */
export async function autocompleteByField<M extends Model>(
  model: ModelStatic<M>,
  field: string,
  query: string | undefined,
  limit: number | undefined,
  offset: number | undefined,
  globalAccess: boolean,
  organizationId: string | undefined,
): Promise<Array<{ id: string; label: string | null }>> {
  // Tenant scope (kept even when a query is present — see G3 in the
  // tenant-isolation audit, where the query branch used to overwrite it).
  const tenant: WhereOptions =
    !globalAccess && organizationId ? { organizationId } : {};

  const where: WhereOptions = query
    ? {
        ...tenant,
        [Op.or]: [
          { id: Utils.uuid(query) },
          Utils.ilike(model.name, field, query),
        ],
      }
    : tenant;

  const records = await model.findAll({
    attributes: ['id', field],
    where,
    limit: limit ? Number(limit) : undefined,
    offset: offset ? Number(offset) : undefined,
    order: [[field, 'ASC']],
  });

  return records.map((record) => {
    const id = record.get('id');
    const label = record.get(field);
    return {
      id: typeof id === 'string' ? id : String(id),
      label: typeof label === 'string' ? label : null,
    };
  });
}
