import mongoose, {
  Model,
  Types,
  FilterQuery,
  SchemaType,
  Document,
  HydratedDocument,
} from 'mongoose';

import { ApiError } from '@/shared/utils/errors';
import { defaultStatus } from '@/shared/utils/responseCode/httpStatusAlias';

interface ProtectedDoc extends Document {
  protected?: boolean;
}

/** Generic model type (no `any`). */
type AnyModel<S = unknown, H = HydratedDocument<S>> = Model<
  S,
  unknown,
  unknown,
  unknown,
  H
>;

type RefEntry = { model: AnyModel; path: string; refPathField?: string };
const REF_MAP = new Map<string, RefEntry[]>();

/** Narrow options (no `any`). */
function extractRefOptions(
  st: SchemaType,
): { ref?: string; refPath?: string } | undefined {
  const self = st as unknown as {
    options?: { ref?: string; refPath?: string };
    caster?: { options?: { ref?: string; refPath?: string } };
  };
  return self.options ?? self.caster?.options;
}

/** Build once after all models are registered. */
export function buildGlobalRefMap(): void {
  REF_MAP.clear();

  const models = Object.values(
    mongoose.connection.models,
  ) as ReadonlyArray<AnyModel>;
  for (const m of models)
    m.schema.eachPath((pathname, st) => {
      const opts = extractRefOptions(st);
      if (!opts) return;

      if (opts.ref) {
        const list = REF_MAP.get(opts.ref) ?? [];
        list.push({ model: m, path: pathname });
        REF_MAP.set(opts.ref, list);
      }

      if (opts.refPath && typeof opts.refPath === 'string') {
        const list = REF_MAP.get('__DYNAMIC__') ?? [];
        list.push({ model: m, path: pathname, refPathField: opts.refPath });
        REF_MAP.set('__DYNAMIC__', list);
      }
    });
}

/** Guard 2: ensure no other model references this id (uses cached map). */
export async function assertNotReferencedFromCache<K>(
  targetModel: Model<K>,
  id: string | Types.ObjectId,
  responseCode: number,
): Promise<void> {
  // Convert string id to ObjectId if needed
  const idVal = typeof id === 'string' ? new Types.ObjectId(id) : id;
  const targetName = targetModel.modelName;

  // Get direct and dynamic reference entries from REF_MAP
  const direct = REF_MAP.get(targetName) ?? [];
  const dynamic = REF_MAP.get('__DYNAMIC__') ?? [];

  // Accumulate promises of existence checks with model name info
  const queries: Array<Promise<{ exists: boolean; modelName: string }>> = [];

  function isDiscriminator<T>(model: Model<T>): boolean {
    // baseModelName is set to base name for discriminator models
    return !!model.baseModelName && model.baseModelName !== model.modelName;
  }

  for (const { model, path } of direct) {
    // Exclude Activity model and discriminator models
    if (model.modelName === 'Activity' || isDiscriminator(model)) continue;

    const filter: Record<string, unknown> = { [path]: idVal };
    queries.push(
      model
        .exists(filter as FilterQuery<unknown>)
        .exec()
        .then((exists) => ({
          exists: !!exists,
          modelName: model.modelName,
        })),
    );
  }

  for (const { model, path, refPathField } of dynamic) {
    if (!refPathField) continue;

    if (model.modelName === 'Activity' || isDiscriminator(model)) continue;

    const filter: Record<string, unknown> = {
      [refPathField]: targetName,
      [path]: idVal,
    };

    queries.push(
      model
        .exists(filter as FilterQuery<unknown>)
        .exec()
        .then((exists) => ({
          exists: !!exists,
          modelName: model.modelName,
        })),
    );
  }

  if (queries.length === 0) return;

  // Wait for all checks to complete
  const results = await Promise.all(queries);

  const referencingModels = [
    ...new Set(results.filter((r) => r.exists).map((r) => r.modelName)),
  ];

  if (referencingModels.length > 0)
    throw new ApiError(
      defaultStatus.BAD_REQUEST,
      `Can't delete ${targetName} because it is referenced to: ${referencingModels.join(
        ', ',
      )}`,
      true,
      '',
      responseCode,
    );
}

export const checkIfProtected = async <S extends ProtectedDoc>(
  model: Model<S>,
  id: string | Types.ObjectId,
  responseCode: number,
): Promise<boolean> => {
  const _id = typeof id === 'string' ? new Types.ObjectId(id) : id;
  const doc: HydratedDocument<S> | null = await model.findById(_id).exec();

  if (!doc)
    throw new ApiError(
      defaultStatus.CONFLICT,
      'Listing not found',
      true,
      '',
      responseCode,
    );

  if (doc.protected)
    throw new ApiError(
      defaultStatus.BAD_REQUEST,
      'Cannot delete protected listing',
      true,
      '',
      responseCode,
    );

  return true;
};

export async function safeDeleteById<S extends ProtectedDoc>(
  model: Model<S>,
  id: string,
  responseCode: number,
): Promise<void> {
  await checkIfProtected(model, id, responseCode);
  await assertNotReferencedFromCache(model, id, responseCode);
  await model.findByIdAndDelete(id).exec();
}

function getBaseModel(model: mongoose.Model<any>) {
  return (model as any).baseModelName
    ? mongoose.model((model as any).baseModelName)
    : model;
}

type IdOnly = { _id: Types.ObjectId };

async function cascadeDelete(
  model: mongoose.Model<any>,
  ids: Types.ObjectId[],
  visited = new Set<string>(),
): Promise<void> {
  if (!ids.length) return;

  const baseModel = getBaseModel(model);
  const modelName = baseModel.modelName;

  // prevent circular loops
  const visitKey = `${modelName}:${ids.join(',')}`;
  if (visited.has(visitKey)) return;
  visited.add(visitKey);

  const directRefs = REF_MAP.get(modelName) ?? [];
  const dynamicRefs = REF_MAP.get('__DYNAMIC__') ?? [];

  /* ---------------------------------- */
  /* 1️⃣ FIND DEPENDENTS (PARALLEL SAFE) */
  /* ---------------------------------- */

  const dependentPromises: Promise<void>[] = [];

  // DIRECT refs
  for (const { model: refModel, path } of directRefs) {
    const baseRefModel = getBaseModel(refModel);

    dependentPromises.push(
      (async () => {
        const docs = await baseRefModel
          .find({ [path]: { $in: ids } }, { _id: 1 })
          .lean<IdOnly[]>();

        const depIds = docs.map((d) => d._id);

        if (depIds.length) await cascadeDelete(baseRefModel, depIds, visited);
      })(),
    );
  }

  // DYNAMIC (refPath) refs
  for (const { model: refModel, path, refPathField } of dynamicRefs) {
    if (!refPathField) continue;

    const baseRefModel = getBaseModel(refModel);

    dependentPromises.push(
      (async () => {
        const docs = await baseRefModel
          .find(
            {
              [refPathField]: modelName,
              [path]: { $in: ids },
            },
            { _id: 1 },
          )
          .lean<IdOnly[]>();

        const depIds = docs.map((d) => d._id);

        if (depIds.length) await cascadeDelete(baseRefModel, depIds, visited);
      })(),
    );
  }

  // 🔥 ESLint-safe parallel execution
  await Promise.all(dependentPromises);

  /* ---------------------------------- */
  /* 2️⃣ DELETE REFERENCES               */
  /* ---------------------------------- */

  const deleteOps: Promise<any>[] = [];

  for (const { model: refModel, path } of directRefs) {
    const baseRefModel = getBaseModel(refModel);
    deleteOps.push(
      baseRefModel
        .deleteMany({
          [path]: { $in: ids },
        })
        .exec(),
    );
  }

  for (const { model: refModel, path, refPathField } of dynamicRefs) {
    if (!refPathField) continue;

    const baseRefModel = getBaseModel(refModel);
    deleteOps.push(
      baseRefModel
        .deleteMany({
          [refPathField]: modelName,
          [path]: { $in: ids },
        })
        .exec(),
    );
  }

  await Promise.all(deleteOps);

  /* ---------------------------------- */
  /* 3️⃣ DELETE DOCUMENTS THEMSELVES     */
  /* ---------------------------------- */

  await baseModel.deleteMany({ _id: { $in: ids } }).exec();
}

export async function deleteWithReferences<K>(
  targetModel: mongoose.Model<K>,
  id: string | Types.ObjectId,
): Promise<void> {
  const idVal = typeof id === 'string' ? new Types.ObjectId(id) : id;

  const baseTargetModel = getBaseModel(targetModel);

  // 🚀 One call = infinite depth cascade
  await cascadeDelete(baseTargetModel, [idVal]);
}
