import { Injectable } from "@nestjs/common"
import { ConfigService } from "@nestjs/config"
import { failureResponse, successResponse } from "src/common/response/response"
import { code } from "src/common/response/response.code"
import { messageKey } from "src/constants/message-keys"
import {
  errorMessage,
  getPlaceIdFromAddress,
  isEmpty,
  successMessage,
  validationMessage,
} from "src/utils/helpers"
import { CityRepository } from "../../city/repositories/city.repository"
import { StateRepository } from "../../state/repositories/state.repository"
import { CreateHospitalDto } from "../dto/create-hospital.dto"
import { HospitalFilterDto } from "../dto/filter-hospital.dto"
import { UpdateHospitalDto } from "../dto/update-hospital.dto"
import { Hospital } from "../entities/hospital.entity"
import { HospitalRepository } from "../repositories/hospital.repository"

const buildAddress = (parts: (string | null | undefined)[]): string => {
  return parts
    .filter((p): p is string => p != null && String(p).trim() !== "")
    .map((p) => p.trim())
    .join(", ")
}

@Injectable()
export class HospitalsService {
  constructor(
    private readonly hospitalRepository: HospitalRepository,
    private readonly stateRepository: StateRepository,
    private readonly cityRepository: CityRepository,
    private readonly configService: ConfigService,
  ) {}

  async create(createHospitalDto: CreateHospitalDto) {
    const checkUniqueHospital = await this.hospitalRepository.getByParams({
      where: {
        name: createHospitalDto?.name,
        address: createHospitalDto?.address,
      },
      findOne: true,
    })

    if (!isEmpty(checkUniqueHospital)) {
      return failureResponse(
        code.VALIDATION,
        validationMessage(messageKey.already_exist, {
          ":data": "Hospital Name",
        }),
      )
    }

    if (await this.checkDataExist(createHospitalDto?.email, "email")) {
      return failureResponse(
        code.VALIDATION,
        validationMessage(messageKey.already_exist, {
          ":data": "Hospital Email",
        }),
      )
    }

    if (
      await this.checkDataExist(createHospitalDto?.phone_number, "phone_number")
    ) {
      return failureResponse(
        code.VALIDATION,
        validationMessage(messageKey.already_exist, {
          ":data": "Hospital number",
        }),
      )
    }

    const state = await this.stateRepository.getByParams({
      where: {
        id: createHospitalDto?.state_id,
      },
      findOne: true,
    })

    if (isEmpty(state)) {
      return failureResponse(
        code.DATA_NOT_FOUND,
        errorMessage(messageKey.data_not_found, { ":data": "State" }),
      )
    }

    const city = await this.cityRepository.getByParams({
      where: {
        id: createHospitalDto?.city_id,
      },
      findOne: true,
    })

    if (isEmpty(city)) {
      return failureResponse(
        code.DATA_NOT_FOUND,
        errorMessage(messageKey.data_not_found, { ":data": "City" }),
      )
    }

    const hospital = new Hospital()
    Object.assign(hospital, createHospitalDto)

    const savedHospital = await this.hospitalRepository.save(hospital)

    return successResponse(
      code.SUCCESS,
      successMessage(messageKey.data_add, { ":data": "Hospital" }),
      savedHospital,
    )
  }

  async findAll(hospitalFilterDto: HospitalFilterDto) {
    const defaultPagination = {
      take: this.configService.get<number>("APP.pagination.take"),
      skip: this.configService.get<number>("APP.pagination.skip"),
    }

    const { search, limit, skip, state_id, is_partner } = hospitalFilterDto

    const queryParams: any = {
      take: Number(limit) || defaultPagination.take,
      skip: Number(skip) || defaultPagination.skip,
      orderBy: { created_at: "DESC" },
      select: [
        "id",
        "name",
        "address",
        "city_id",
        "state_id",
        "country_id",
        "zip_code",
        "country_code",
        "phone_number",
        "email",
        "is_partner",
        "created_at",
        "latitude",
        "longitude",
        "place_id",
      ],
      relations: [
        "state:id,name",
        "city:id,name",
        "country:id,name",
        "contacts:id,is_primary,email,country_code,phone_number,name",
        "customer:id,customer_name",
      ],
      where: {},
    }

    if (search) {
      queryParams.search = {
        name: search,
        phone_number: search,
        email: search,
      }
    }

    // Build where conditions
    const whereConditions: Record<string, any> = {}

    if (state_id) {
      whereConditions.state_id = state_id
    }

    if (is_partner !== undefined) {
      whereConditions.is_partner =
        typeof is_partner === "string"
          ? is_partner === "true"
          : Boolean(is_partner)
    }

    // Apply where conditions if they exist
    if (Object.keys(whereConditions).length > 0) {
      queryParams.where = whereConditions
    }

    const hospitals: any =
      await this.hospitalRepository.getByParams(queryParams)

    // We need to filter contacts here (instead of in the query) to ensure:
    // 1. All hospitals are included in the result, even if they have no contacts.
    // 2. For hospitals with contacts, only those with is_primary = true are shown.

    const hospitalsWithPrimaryContacts = {
      ...hospitals,
      data: hospitals.data.map((hospital) => ({
        ...hospital,
        contacts: hospital?.contacts
          ? hospital?.contacts.filter((contact) => contact?.is_primary)
          : [],
        customer_count: hospital?.customer ? hospital.customer.length : 0,
      })),
    }

    return successResponse(
      code.SUCCESS,
      successMessage(messageKey.data_retrieve, { ":data": "Hospital" }),
      hospitalsWithPrimaryContacts,
    )
  }

  async findOne(id: number) {
    const hospital: any = await this.hospitalRepository.getByParams({
      where: {
        id,
      },
      select: [
        "id",
        "name",
        "address",
        "city_id",
        "state_id",
        "country_id",
        "zip_code",
        "country_code",
        "phone_number",
        "email",
        "is_partner",
        "created_at",
        "latitude",
        "longitude",
        "place_id",
      ],
      relations: [
        "state:id,name",
        "city:id,name",
        "country:id,name",
        "contacts:id,is_primary,email,country_code,phone_number,name",
        "customer:id,customer_name",
      ],
      findOne: true,
    })

    if (isEmpty(hospital)) {
      return failureResponse(
        code.DATA_NOT_FOUND,
        errorMessage(messageKey.data_not_found, { ":data": "Hospital" }),
      )
    }

    // We need to filter contacts here (instead of in the query) to ensure:
    // 1. The hospital is included in the result even if it has no contacts.
    // 2. For hospitals with contacts, only those with is_primary = true are shown.

    const hospitalWithPrimaryContacts = {
      ...hospital,
      contacts_count: hospital?.contacts?.length || 0,
      contacts: hospital?.contacts
        ? hospital?.contacts.filter((contact) => contact?.is_primary)
        : [],
      customer_count: hospital?.customer ? hospital.customer.length : 0,
    }

    return successResponse(
      code.SUCCESS,
      successMessage(messageKey.data_retrieve, { ":data": "Hospital" }),
      hospitalWithPrimaryContacts,
    )
  }

  async update(id: number, updateHospitalDto: UpdateHospitalDto) {
    const hospital = await this.hospitalRepository.getByParams({
      where: { id },
      findOne: true,
    })

    if (isEmpty(hospital)) {
      return failureResponse(
        code.DATA_NOT_FOUND,
        errorMessage(messageKey.data_not_found, { ":data": "Hospital" }),
      )
    }

    // Check for duplicate name + address (excluding self)
    if (updateHospitalDto?.name && updateHospitalDto?.address) {
      const existingHospital = (await this.hospitalRepository.getByParams({
        where: {
          name: updateHospitalDto.name,
          address: updateHospitalDto.address,
        },
        findOne: true,
      })) as any

      if (!isEmpty(existingHospital) && existingHospital.id !== id) {
        return failureResponse(
          code.VALIDATION,
          validationMessage(messageKey.already_exist, {
            ":data": "Hospital Name",
          }),
        )
      }
    }

    // Check for duplicate email (excluding self)
    if (updateHospitalDto?.email) {
      const existingEmail = (await this.hospitalRepository.getByParams({
        where: { email: updateHospitalDto.email },
        findOne: true,
      })) as any

      if (!isEmpty(existingEmail) && existingEmail.id !== id) {
        return failureResponse(
          code.VALIDATION,
          validationMessage(messageKey.already_exist, {
            ":data": "Hospital Email",
          }),
        )
      }
    }

    // Check for duplicate phone number (excluding self)
    if (updateHospitalDto?.phone_number) {
      const existingPhone = (await this.hospitalRepository.getByParams({
        where: { phone_number: updateHospitalDto.phone_number },
        findOne: true,
      })) as any

      if (!isEmpty(existingPhone) && existingPhone.id !== id) {
        return failureResponse(
          code.VALIDATION,
          validationMessage(messageKey.already_exist, {
            ":data": "Hospital number",
          }),
        )
      }
    }

    // Validate state
    const state = await this.stateRepository.getByParams({
      where: { id: updateHospitalDto?.state_id },
      findOne: true,
    })

    if (isEmpty(state)) {
      return failureResponse(
        code.DATA_NOT_FOUND,
        errorMessage(messageKey.data_not_found, { ":data": "State" }),
      )
    }

    // Validate city
    const city = await this.cityRepository.getByParams({
      where: { id: updateHospitalDto?.city_id },
      findOne: true,
    })

    if (isEmpty(city)) {
      return failureResponse(
        code.DATA_NOT_FOUND,
        errorMessage(messageKey.data_not_found, { ":data": "City" }),
      )
    }

    const condition = { id: id }
    const hospitalEntity = new Hospital()
    Object.assign(hospitalEntity, updateHospitalDto)

    const hospitalData = await this.hospitalRepository.save(
      hospitalEntity,
      condition,
    )

    return successResponse(
      code.SUCCESS,
      successMessage(messageKey.data_update, { ":data": "Hospital" }),
      hospitalData,
    )
  }

  async remove(id: number) {
    const hospital = await this.hospitalRepository.getByParams({
      where: {
        id,
      },
      findOne: true,
    })

    if (isEmpty(hospital)) {
      return failureResponse(
        code.DATA_NOT_FOUND,
        errorMessage(messageKey.data_not_found, { ":data": "Hospital" }),
      )
    }

    await this.hospitalRepository.remove({ id }, null, false)

    return successResponse(
      code.SUCCESS,
      successMessage(messageKey.data_removed, { ":data": "Hospital" }),
    )
  }

  /**
   * Backfill place_id for hospitals that have an address but no place_id (via Google Geocoding API).
   */
  async backfillPlaceIds() {
    const rows = await this.hospitalRepository.getForPlaceIdBackfill()

    let updated = 0
    let failed = 0
    let skipped = 0

    const BACKFILL_DELAY_MS = 150
    const sleep = (ms: number) =>
      new Promise<void>((resolve) => setTimeout(resolve, ms))

    for (const row of rows) {
      const fullAddress = buildAddress([
        row.address,
        row.city_name,
        row.state_name,
        row.country_name,
        row.zip_code,
      ])

      if (!fullAddress) {
        skipped++
        continue
      }

      const result = await getPlaceIdFromAddress(fullAddress)
      await sleep(BACKFILL_DELAY_MS)

      if (!result) {
        failed++
        continue
      }

      try {
        await this.hospitalRepository.save({ place_id: result.place_id }, {
          id: row.id,
        } as any)
        updated++
      } catch {
        failed++
      }
    }

    return successResponse(
      code.SUCCESS,
      successMessage(messageKey.data_update, { ":data": "Place IDs" }),
      { updated, failed, skipped },
    )
  }

  async checkDataExist(
    hospitalData: string,
    field: string,
    excludeId?: number,
  ): Promise<boolean> {
    const whereCondition: any = {}
    const whereLikeCondition: any = {}

    whereLikeCondition[field] = hospitalData

    // Add exclusion logic if `excludeId` is provided
    if (excludeId) {
      whereCondition["id"] = {
        not: excludeId,
      }
    }

    const hospital = await this.hospitalRepository.getByParams({
      where: whereCondition,
      whereLower: whereLikeCondition,
      findOne: true,
    })

    return !isEmpty(hospital)
  }
}
