import DatabaseClient
import Dependencies
import ManualDClient
import ManualDCore

extension DatabaseClient {

  func calculateDuctSizes(
    details: Project.Detail
  ) async throws -> (DuctSizes, DuctSizeSharedRequest) {
    let (rooms, shared) = try await calculateRoomDuctSizes(details: details)
    let (trunks, _) = try await calculateTrunkDuctSizes(details: details)
    return (.init(rooms: rooms, trunks: trunks), shared)
  }

  func calculateDuctSizes(
    projectID: Project.ID
  ) async throws -> (DuctSizes, DuctSizeSharedRequest, [Room]) {
    @Dependency(\.manualD) var manualD

    let shared = try await sharedDuctRequest(projectID)
    let rooms = try await rooms.fetch(projectID)

    return try await (
      manualD.calculateDuctSizes(
        rooms: rooms,
        trunks: trunkSizes.fetch(projectID),
        sharedRequest: shared
      ),
      shared,
      rooms
    )
  }

  func calculateRoomDuctSizes(
    details: Project.Detail
  ) async throws -> ([DuctSizes.RoomContainer], DuctSizeSharedRequest) {
    @Dependency(\.manualD) var manualD

    let shared = try sharedDuctRequest(details: details)
    let rooms = try await manualD.calculateRoomSizes(rooms: details.rooms, sharedRequest: shared)
    return (rooms, shared)
  }

  func calculateRoomDuctSizes(
    projectID: Project.ID
  ) async throws -> ([DuctSizes.RoomContainer], DuctSizeSharedRequest) {
    @Dependency(\.manualD) var manualD

    let shared = try await sharedDuctRequest(projectID)

    return try await (
      manualD.calculateRoomSizes(
        rooms: rooms.fetch(projectID),
        sharedRequest: shared
      ),
      shared
    )
  }

  func calculateTrunkDuctSizes(
    details: Project.Detail
  ) async throws -> ([DuctSizes.TrunkContainer], DuctSizeSharedRequest) {
    @Dependency(\.manualD) var manualD

    let shared = try sharedDuctRequest(details: details)
    let trunks = try await manualD.calculateTrunkSizes(
      rooms: details.rooms,
      trunks: details.trunks,
      sharedRequest: shared
    )
    return (trunks, shared)
  }

  func calculateTrunkDuctSizes(
    projectID: Project.ID
  ) async throws -> ([DuctSizes.TrunkContainer], DuctSizeSharedRequest) {
    @Dependency(\.manualD) var manualD

    let shared = try await sharedDuctRequest(projectID)

    return try await (
      manualD.calculateTrunkSizes(
        rooms: rooms.fetch(projectID),
        trunks: trunkSizes.fetch(projectID),
        sharedRequest: shared
      ),
      shared
    )
  }

  func sharedDuctRequest(details: Project.Detail) throws -> DuctSizeSharedRequest {
    guard
      let dfrResponse = designFrictionRate(
        componentLosses: details.componentLosses,
        equipmentInfo: details.equipmentInfo,
        equivalentLengths: details.maxContainer
      )
    else {
      throw ProjectClientError("Project not complete.")
    }

    guard let projectSHR = details.project.sensibleHeatRatio else {
      throw ProjectClientError("Project sensible heat ratio not set.")
    }

    let ensuredTEL = try dfrResponse.ensureMaxContainer()

    return .init(
      equipmentInfo: dfrResponse.equipmentInfo,
      maxSupplyLength: ensuredTEL.supply,
      maxReturnLenght: ensuredTEL.return,
      designFrictionRate: dfrResponse.designFrictionRate,
      projectSHR: projectSHR
    )
  }

  func sharedDuctRequest(_ projectID: Project.ID) async throws -> DuctSizeSharedRequest {

    guard let dfrResponse = try await designFrictionRate(projectID: projectID) else {
      throw ProjectClientError("Project not complete.")
    }

    let ensuredTEL = try dfrResponse.ensureMaxContainer()

    return try await .init(
      equipmentInfo: dfrResponse.equipmentInfo,
      maxSupplyLength: ensuredTEL.supply,
      maxReturnLenght: ensuredTEL.return,
      designFrictionRate: dfrResponse.designFrictionRate,
      projectSHR: ensuredSHR(projectID)
    )

  }

  // Fetches the project sensible heat ratio or throws an error if it's nil.
  func ensuredSHR(_ projectID: Project.ID) async throws -> Double {
    guard let projectSHR = try await projects.getSensibleHeatRatio(projectID) else {
      throw ProjectClientError("Project sensible heat ratio not set.")
    }
    return projectSHR
  }

  // Internal container.
  struct DesignFrictionRateResponse: Equatable, Sendable {

    typealias EnsuredTEL = (supply: EquivalentLength, return: EquivalentLength)

    let designFrictionRate: Double
    let equipmentInfo: EquipmentInfo
    let telMaxContainer: EquivalentLength.MaxContainer

    func ensureMaxContainer() throws -> EnsuredTEL {

      guard let maxSupplyLength = telMaxContainer.supply else {
        throw ProjectClientError("Max supply TEL not found")
      }
      guard let maxReturnLength = telMaxContainer.return else {
        throw ProjectClientError("Max supply TEL not found")
      }

      return (maxSupplyLength, maxReturnLength)

    }
  }

  func designFrictionRate(
    componentLosses: [ComponentPressureLoss],
    equipmentInfo: EquipmentInfo,
    equivalentLengths: EquivalentLength.MaxContainer
  ) -> DesignFrictionRateResponse? {
    guard let tel = equivalentLengths.totalEquivalentLength,
      componentLosses.count > 0
    else { return nil }

    let availableStaticPressure = equipmentInfo.staticPressure - componentLosses.total

    return .init(
      designFrictionRate: (availableStaticPressure * 100) / tel,
      equipmentInfo: equipmentInfo,
      telMaxContainer: equivalentLengths
    )

  }

  func designFrictionRate(
    projectID: Project.ID
  ) async throws -> DesignFrictionRateResponse? {

    guard let equipmentInfo = try await equipment.fetch(projectID) else {
      return nil
    }

    return try await designFrictionRate(
      componentLosses: componentLoss.fetch(projectID),
      equipmentInfo: equipmentInfo,
      equivalentLengths: effectiveLength.fetchMax(projectID)
    )
  }
}
