import { SessionStore } from '@backend/auth'
import { BaseEntity } from '@backend/domain'
import { NotImplementedException } from '@nestjs/common'
import { isArray } from 'es-toolkit/compat'
import {
	DeepPartial,
	DeleteResult,
	FindManyOptions,
	FindOneOptions,
	FindOptionsWhere,
	ObjectId,
	QueryRunner,
	RemoveOptions,
	Repository,
	SaveOptions,
	SelectQueryBuilder,
	UpdateResult,
} from 'typeorm'
import { PickKeysByType } from 'typeorm/common/PickKeysByType'
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'

export abstract class BaseEntityRepository<T extends BaseEntity<T>> extends Repository<T> {
	constructor(readonly _repository: Repository<T>) {
		super(_repository.target, _repository.manager, _repository.queryRunner)
	}

	override find(options?: FindManyOptions<T> | undefined): Promise<T[]> {
		options = this.manyOptionsFilter(options)
		return this._repository.find(options)
	}

	override findOne(options: FindOneOptions<T>): Promise<T | null> {
		options = this.oneOptionsFilter(options)
		return this._repository.findOne(options)
	}

	override findOneBy(where: FindOptionsWhere<T> | FindOptionsWhere<T>[]): Promise<T | null> {
		where = this.optionsWhereFilter(where)
		return this._repository.findOneBy(where)
	}

	override save<K extends DeepPartial<T>>(entities: K[], options: SaveOptions & { reload: false }): Promise<T[]>
	override save<K extends DeepPartial<T>>(entities: K[], options?: SaveOptions | undefined): Promise<(T & T)[]>
	override save<K extends DeepPartial<T>>(entity: K, options: SaveOptions & { reload: false }): Promise<T>
	override save<K extends DeepPartial<T>>(entity: K, options?: SaveOptions | undefined): Promise<T & T>
	override save(
		entityOrEntities: DeepPartial<T> | DeepPartial<T>[],
		options?: SaveOptions,
	): Promise<T[]> | Promise<(T & T)[]> | Promise<T> | Promise<T & T> {
		const user = SessionStore.get()
		if (user && user.tenantId) {
			if (isArray(entityOrEntities)) {
				for (const _entity of entityOrEntities) _entity.tenantId = user.tenantId
			} else entityOrEntities.tenantId = user.tenantId
		}
		return this._repository.save(entityOrEntities as never, options)
	}

	override update(criteria: FindOptionsWhere<T>, partialEntity: QueryDeepPartialEntity<T>): Promise<UpdateResult> {
		criteria = this.optionsWhereFilter(criteria)
		return this._repository.update(criteria, partialEntity)
	}

	override delete(criteria: FindOptionsWhere<T>): Promise<DeleteResult> {
		criteria = this.optionsWhereFilter(criteria)
		return this._repository.delete(criteria)
	}

	override softDelete(criteria: FindOptionsWhere<T>): Promise<UpdateResult> {
		criteria = this.optionsWhereFilter(criteria)
		return this._repository.softDelete(criteria)
	}

	override exist(options?: FindManyOptions<T> | undefined): Promise<boolean> {
		options = this.manyOptionsFilter(options)
		return this._repository.exist(options)
	}

	override count(options?: FindManyOptions<T> | undefined): Promise<number> {
		options = this.manyOptionsFilter(options)
		return this._repository.count(options)
	}

	override query<K = unknown>(query: string, parameters?: unknown[] | undefined): Promise<K> {
		return this._repository.query(query, parameters)
	}

	override maximum(
		columnName: PickKeysByType<T, number>,
		where?: FindOptionsWhere<T> | FindOptionsWhere<T>[] | undefined,
	): Promise<number | null> {
		where = this.optionsWhereFilter(where ?? {})
		return this._repository.maximum(columnName, where)
	}

	// ! Throw expection on all other methods so that we don't accidently miss multi tenancy constraints
	// ! Implement them as needed
	// ! Please also implement any method that is used, otherwise it will throw error
	//#region Unimplemented
	/* eslint-disable @typescript-eslint/no-unused-vars */
	override average(
		columnName: PickKeysByType<T, number>,
		where?: FindOptionsWhere<T> | FindOptionsWhere<T>[] | undefined,
	): Promise<number | null> {
		throw new NotImplementedException()
	}

	override clear(): Promise<void> {
		throw new NotImplementedException()
	}

	override countBy(where: FindOptionsWhere<T> | FindOptionsWhere<T>[]): Promise<number> {
		throw new NotImplementedException()
	}

	override create(): T
	override create(entityLikeArray: DeepPartial<T>[]): T[]
	override create(entityLike: DeepPartial<T>): T
	override create(entityLike?: unknown): T | T[] {
		throw new NotImplementedException()
	}

	override createQueryBuilder(
		alias?: string | undefined,
		queryRunner?: QueryRunner | undefined,
	): SelectQueryBuilder<T> {
		throw new NotImplementedException()
	}

	override decrement(
		conditions: FindOptionsWhere<T>,
		propertyPath: string,
		value: string | number,
	): Promise<UpdateResult> {
		throw new NotImplementedException()
	}

	override extend<CustomRepository>(
		custom: CustomRepository & ThisType<this & CustomRepository>,
	): this & CustomRepository {
		throw new NotImplementedException()
	}

	override findAndCount(options?: FindManyOptions<T> | undefined): Promise<[T[], number]> {
		throw new NotImplementedException()
	}

	override findAndCountBy(where: FindOptionsWhere<T> | FindOptionsWhere<T>[]): Promise<[T[], number]> {
		throw new NotImplementedException()
	}

	override findBy(where: FindOptionsWhere<T> | FindOptionsWhere<T>[]): Promise<T[]> {
		throw new NotImplementedException()
	}

	override findOneByOrFail(where: FindOptionsWhere<T> | FindOptionsWhere<T>[]): Promise<T> {
		throw new NotImplementedException()
	}

	override findOneOrFail(options: FindOneOptions<T>): Promise<T> {
		throw new NotImplementedException()
	}

	override increment(
		conditions: FindOptionsWhere<T>,
		propertyPath: string,
		value: string | number,
	): Promise<UpdateResult> {
		throw new NotImplementedException()
	}

	override merge(mergeIntoEntity: T, ...entityLikes: DeepPartial<T>[]): T {
		throw new NotImplementedException()
	}

	override minimum(
		columnName: PickKeysByType<T, number>,
		where?: FindOptionsWhere<T> | FindOptionsWhere<T>[] | undefined,
	): Promise<number | null> {
		throw new NotImplementedException()
	}

	override preload(entityLike: DeepPartial<T>): Promise<T | undefined> {
		throw new NotImplementedException()
	}

	override recover<K extends DeepPartial<T>>(entities: K[], options: SaveOptions & { reload: false }): Promise<T[]>
	override recover<K extends DeepPartial<T>>(entities: K[], options?: SaveOptions | undefined): Promise<(T & T)[]>
	override recover<K extends DeepPartial<T>>(entity: K, options: SaveOptions & { reload: false }): Promise<T>
	override recover<K extends DeepPartial<T>>(entity: K, options?: SaveOptions | undefined): Promise<T & T>
	override recover(
		entity: unknown,
		options?: unknown,
	): Promise<T[]> | Promise<(T & T)[]> | Promise<T> | Promise<T & T> {
		throw new NotImplementedException()
	}

	override remove(entities: T[], options?: RemoveOptions | undefined): Promise<T[]>
	override remove(entity: T, options?: RemoveOptions | undefined): Promise<T>
	override remove(entity: unknown, options?: unknown): Promise<T[]> | Promise<T> {
		throw new NotImplementedException()
	}

	override restore(
		criteria: string | number | FindOptionsWhere<T> | Date | ObjectId | string[] | number[] | Date[] | ObjectId[],
	): Promise<UpdateResult> {
		throw new NotImplementedException()
	}

	override softRemove<K extends DeepPartial<T>>(entities: K[], options: SaveOptions & { reload: false }): Promise<T[]>
	override softRemove<K extends DeepPartial<T>>(entities: K[], options?: SaveOptions | undefined): Promise<(T & T)[]>
	override softRemove<K extends DeepPartial<T>>(entity: K, options: SaveOptions & { reload: false }): Promise<T>
	override softRemove<K extends DeepPartial<T>>(entity: K, options?: SaveOptions | undefined): Promise<T & T>
	override softRemove(
		entity: unknown,
		options?: unknown,
	): Promise<T[]> | Promise<(T & T)[]> | Promise<T> | Promise<T & T> {
		throw new NotImplementedException()
	}

	override sum(
		columnName: PickKeysByType<T, number>,
		where?: FindOptionsWhere<T> | FindOptionsWhere<T>[] | undefined,
	): Promise<number | null> {
		throw new NotImplementedException()
	}
	//#endregion

	private oneOptionsFilter(options: FindOneOptions<T>): FindOneOptions<T> {
		const tenantId = SessionStore.tenantId
		if (tenantId) {
			const filter = { tenantId } as FindOptionsWhere<T>
			if (options.where) {
				// eslint-disable-next-line unicorn-x/prefer-ternary
				if (isArray(options.where)) {
					options.where = options.where.map((_filter) => ({ ..._filter, ...filter }))
				} else options.where = { ...options.where, ...filter }
			} else options.where = filter
		}
		return options
	}

	private manyOptionsFilter(options?: FindManyOptions<T>): FindManyOptions<T> | undefined {
		const tenantId = SessionStore.tenantId
		if (tenantId) {
			const filter = { tenantId } as FindOptionsWhere<T>
			if (options && options.where) {
				// eslint-disable-next-line unicorn-x/prefer-ternary
				if (isArray(options.where)) {
					options.where = options.where.map((_filter) => ({ ..._filter, ...filter }))
				} else options.where = { ...options.where, ...filter }
			} else if (options) options.where = filter
			else options = { where: filter }
		}
		return options
	}

	private optionsWhereFilter(where: FindOptionsWhere<T>): FindOptionsWhere<T>
	private optionsWhereFilter(
		where: FindOptionsWhere<T> | FindOptionsWhere<T>[],
	): FindOptionsWhere<T> | FindOptionsWhere<T>[]
	private optionsWhereFilter(
		where: FindOptionsWhere<T> | FindOptionsWhere<T>[],
	): FindOptionsWhere<T> | FindOptionsWhere<T>[] {
		const tenantId = SessionStore.tenantId
		if (tenantId) {
			const filter = { tenantId } as FindOptionsWhere<T>
			where = isArray(where) ? where.map((_filter) => ({ ..._filter, ...filter })) : { ...filter, ...where }
		}
		return where
	}
}
