import {
  Brackets,
  DeepPartial,
  DeleteResult,
  EntityManager,
  FindOptionsWhere,
  Repository,
  SelectQueryBuilder,
} from "typeorm"
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"
import { HasId } from "../interfaces/has-id.interface"
import { PaginatedResult } from "../interfaces/pagination-result.interface"
import { QueryParams } from "../interfaces/query-params.interface"
import { isEmpty } from "../../utils/helpers"

export abstract class BaseAbstractRepository<T extends HasId> {
  protected entity: Repository<T>

  protected constructor(entity: Repository<T>) {
    this.entity = entity
  }

  public async getByParams(
    params: QueryParams<T>,
  ): Promise<PaginatedResult<T> | T[] | T | number | undefined> {
    const {
      where,
      whereIn,
      whereNotIn,
      whereNull,
      orWhereNull,
      whereNotNull,
      whereNullOrNotNull,
      select,
      findOne,
      relations,
      search,
      take,
      skip,
      orderBy,
      groupBy,
      rawData,
      relationCount,
      sum,
      relationCountWithConditions,
      orWhere,
      orWhereIn,
      whereLower,
      getCountOnly,
      withDeleted,
      distinctValues,
    } = params
    let query: SelectQueryBuilder<T> = this.entity.createQueryBuilder("entity")

    // Apply where conditions

    if (where) {
      Object.entries(where).forEach(([key, value]) => {
        const whereKey = key.startsWith("entity_") ? key : `entity.${key}`

        if (typeof value === "object" && value !== null) {
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          const { not, lt, gt, lte, bt, gte, ilike, ...others } = value

          if (ilike !== undefined) {
            query = query.andWhere(`${whereKey} ILIKE :${key}_ilike`, {
              [`${key}_ilike`]: `%${ilike}%`,
            })
          }
          if (not !== undefined) {
            query = query.andWhere(`${whereKey} != :${key}_not`, {
              [`${key}_not`]: not,
            })
          }
          if (lt !== undefined) {
            query = query.andWhere(`${whereKey} < :${key}_lt`, {
              [`${key}_lt`]: lt,
            })
          }
          if (gt !== undefined) {
            query = query.andWhere(`${whereKey} > :${key}_gt`, {
              [`${key}_gt`]: gt,
            })
          }
          if (lte !== undefined) {
            query = query.andWhere(`${whereKey} <= :${key}_lte`, {
              [`${key}_lte`]: lte,
            })
          }
          if (gte !== undefined) {
            query = query.andWhere(`${whereKey} >= :${key}_gte`, {
              [`${key}_gte`]: gte,
            })
          }
          if (bt !== undefined) {
            query = query.andWhere(`entity.${key} BETWEEN :start AND :end`, {
              start: bt.start,
              end: bt.end,
            })
          }
        } else {
          query = query.andWhere(`${whereKey} = :${key}`, { [key]: value })
        }
      })
    }

    if (whereLower) {
      Object.entries(whereLower).forEach(([key, value]) => {
        const whereKey = key.startsWith("entity_") ? key : `entity.${key}`

        if (typeof value === "string") {
          query = query.andWhere(`LOWER(${whereKey}) = LOWER(:${key})`, {
            [`${key}`]: value,
          })
        } else {
          query = query.andWhere(`${whereKey} = :${key}`, { [key]: value })
        }
      })
    }

    // Apply orWhere conditions
    if (orWhere) {
      query = query.andWhere(
        new Brackets((qb) => {
          Object.entries(orWhere).forEach(([key, value]) => {
            qb.orWhere(`entity.${key} = :${key}`, { [key]: value })
          })
        }),
      )
    }

    if (orWhereIn) {
      Object.entries(orWhereIn).forEach(([key, value]) => {
        query = query.orWhere(`entity.${key} In (:${key})`, { [key]: value })
      })
    }

    // Apply wherein conditions
    if (whereIn) {
      Object.entries(whereIn).forEach(([key, values]) => {
        const whereKey = key.startsWith("entity_") ? key : `entity.${key}`
        if (Array.isArray(values)) {
          query = query.andWhere(`${whereKey} IN (:...${key})`, {
            [key]: [null, ...values],
          })
        }
      })
    }

    // Apply whereNotIn conditions
    if (whereNotIn) {
      Object.entries(whereNotIn).forEach(([key, values]) => {
        if (Array.isArray(values) && values.length > 0) {
          query = query.andWhere(`entity.${key} NOT IN (:...${key})`, {
            [key]: values,
          })
        }
      })
    }

    // Apply where null conditions
    if (whereNull && whereNull.length > 0) {
      whereNull.forEach((key) => {
        query = query.andWhere(`entity.${String(key)} IS NULL`)
      })
    }
    if (orWhereNull && orWhereNull.length > 0) {
      orWhereNull.forEach((key) => {
        query = query.orWhere(`entity.${String(key)} IS NULL`)
      })
    }
    // Apply where not null conditions
    if (whereNotNull && whereNotNull.length > 0) {
      whereNotNull.forEach((key: any) => {
        const whereKey = key.startsWith("entity_")
          ? key
          : `entity.${String(key)}`
        query = query.andWhere(`${String(whereKey)} IS NOT NULL`)
      })
    }

    if (whereNullOrNotNull && whereNullOrNotNull.length > 0) {
      whereNullOrNotNull.forEach((key: any) => {
        const whereKey = key.startsWith("entity_") ? key : `entity.${key}`
        query = query.andWhere(
          `${String(whereKey)} IS NOT NULL OR ${String(whereKey)} IS NULL`,
        )
      })
    }

    if (withDeleted) {
      query = query.withDeleted()
    }

    if (distinctValues && distinctValues.length > 0) {
      const result: any = {}

      for (const key of distinctValues) {
        const values: any = await query
          .select(`DISTINCT entity.${String(key)}`, String(key))
          .getRawMany()

        // Extract unique values and store in the result object
        result[String(key)] = values.map((row) => row[String(key)])
      }

      return result
    }

    // Apply select fields for the main entity
    if (select && select.length > 0) {
      query = query.select(select.map((field) => `entity.${String(field)}`))
    }

    // Load relations with select fields
    if (relations && relations.length > 0) {
      relations.forEach((relation) => {
        const parts = relation.split(".")
        let relationName = "entity"

        parts.forEach((part) => {
          const [relationPath, fields] = part.split(":")

          const alias = `${relationName}_${relationPath}`

          if (fields) {
            query = query.leftJoin(`${relationName}.${relationPath}`, alias)

            query = query.addSelect(
              fields.split(",").map((field) => `${alias}.${field.trim()}`),
            )
          } else
            query = query.leftJoinAndSelect(
              `${relationName}.${relationPath}`,
              alias,
            )

          relationName = alias
        })
      })
    }

    // Apply search conditions with OR logic
    if (search && Object.keys(search).length > 0) {
      query = query.andWhere(
        new Brackets((qb) => {
          Object.entries(search).forEach(([key, value]) => {
            const searchKey = key.startsWith("entity_") ? key : `entity.${key}`

            if (!isEmpty(value)) {
              const searchTerm = `%${value}%`
              qb.orWhere(`${searchKey} ILIKE :searchTerm`, { searchTerm })
            }
          })
        }),
      )
    }

    // Apply order by conditions
    if (orderBy && Object.keys(orderBy).length > 0) {
      Object.entries(orderBy).forEach(([key, value]) => {
        let entityName = `entity.${String(key)}`

        if (key.includes(".")) {
          entityName = `entity_${String(key)}`
        }

        query = query.addOrderBy(entityName, value as "ASC" | "DESC")
      })
    }

    // Apply group by conditions
    if (groupBy && groupBy.length > 0) {
      query = query.groupBy(
        groupBy.map((field) => `entity.${String(field)}`).join(", "),
      )

      // Add count selection for grouped fields
      query = query.select("COUNT(entity.id)", "count") // Adjust 'entity.id' to your unique field
      groupBy.forEach(
        (field) => (query = query.addSelect(`entity.${String(field)}`)),
      )
    }

    // Apply relation count dynamically
    if (relationCount && relationCount.length > 0) {
      relationCount.forEach((relation) => {
        query = query.loadRelationCountAndMap(
          `entity.${relation}_count`,
          `entity.${relation}`,
        )
      })
    }

    if (sum && sum.length > 0) {
      sum.forEach((key) => {
        const filed = key.startsWith("entity_") ? key : `entity.${key}`
        query = query.addSelect(`SUM(${filed})`, `${key}_total`)
      })
    }

    // not working as expected when provide the relation and relationCountWithConditions at the same time
    if (relationCountWithConditions) {
      relationCountWithConditions.forEach((relation) => {
        if (relation.condition) {
          query = query.loadRelationCountAndMap(
            `entity.${relation.alias}`, // Alias for the count field
            `entity.${relation.relation}`, // Relation to count
            relation.alias, // Alias for the relation
            (qb) => qb.andWhere(relation.condition, relation.params), // Apply condition
          )
        }
      })
    }

    if (findOne) return query.getOne()

    if (getCountOnly) return query.getCount()

    // Apply pagination
    if ((take && skip) || skip === 0) {
      query = query.take(take)
      query = query.skip(skip)

      const [data, count]: [T[], number] = await query
        .take(take)
        .skip(skip)
        .getManyAndCount()

      return { count, data }
    }

    if (rawData) return query.getRawMany()

    // Execute query based on findOne flag
    return query.getMany()
  }

  public async save(
    data: QueryDeepPartialEntity<T>,
    conditions?: FindOptionsWhere<T>,
    manager?: EntityManager,
    useForceSave?: boolean,
  ): Promise<T> {
    const repository = manager
      ? manager.getRepository(this.entity.target)
      : this.entity

    if (!isEmpty(conditions)) {
      const record = await repository.findOne({ where: conditions })

      if (record) {
        if (!isEmpty(useForceSave) && useForceSave)
          await repository.save(data as DeepPartial<T>)
        else await repository.update(conditions, data)

        return repository.findOneOrFail({ where: conditions })
      }
    }

    return repository.save(data as DeepPartial<T>)
  }

  public async saveMany(
    data: DeepPartial<T>[],
    conditions?: FindOptionsWhere<T>,
    manager?: EntityManager,
  ): Promise<T[]> {
    const repository = manager
      ? manager.getRepository(this.entity.target)
      : this.entity

    return repository.save(data)
  }

  public async remove(
    conditions: FindOptionsWhere<T>,
    manager?: EntityManager,
    hardDelete?: boolean,
  ): Promise<T | DeleteResult> {
    const repository = manager
      ? manager.getRepository(this.entity.target)
      : this.entity
    if (hardDelete) {
      return repository.delete({ ...conditions })
    }

    return repository.softDelete({ ...conditions })
  }
}
