import { prisma } from '../../lib/prisma'
import { AppError } from '../../http/errors'
import { validateAndFlagLicense } from '../../services/licenseValidationService'
import { applyPricingRules, calculateUpdatedInsuranceCharge, calculateUpdatedAdditionalDriverCharge } from './reservation.pricing.service'
import { applyInsurancesToReservation } from './reservation.insurance.service'
import { applyAdditionalDriversToReservation } from './reservation.additional-driver.service'
import { parseReservationExtras, normalizeOptionalString, serializeReservationForDashboard, buildReservationWorkflow } from './reservation.presenter'
import * as repo from './reservation.repo'

export async function listReservations(companyId: string, query: {
  status?: string; vehicleId?: string; source?: string
  startDate?: string; endDate?: string; page?: number; pageSize?: number
}) {
  const page     = query.page     ?? 1
  const pageSize = query.pageSize ?? 20
  const where: any = { companyId }
  if (query.status)    where.status    = query.status
  if (query.vehicleId) where.vehicleId = query.vehicleId
  if (query.source)    where.source    = query.source
  if (query.startDate) where.startDate = { gte: new Date(query.startDate) }
  if (query.endDate)   where.endDate   = { lte: new Date(query.endDate) }

  const [reservations, total] = await repo.findMany(where, (page - 1) * pageSize, pageSize)
  return {
    data: reservations.map(serializeReservationForDashboard),
    total,
    page,
    pageSize,
    totalPages: Math.ceil(total / pageSize),
  }
}

export async function getReservation(id: string, companyId: string) {
  const reservation = await repo.findById(id, companyId)
  return serializeReservationForDashboard(reservation)
}

export async function createReservation(companyId: string, body: {
  vehicleId: string; customerId: string; startDate: string; endDate: string
  pickupLocation?: string; returnLocation?: string; offerId?: string
  promoCodeUsed?: string; depositAmount?: number; paymentMode?: string; notes?: string
  selectedInsurancePolicyIds?: string[]; additionalDrivers?: any[]
}) {
  const vehicle = await repo.findVehicle(body.vehicleId, companyId)
  await repo.findCustomer(body.customerId, companyId)

  const start     = new Date(body.startDate)
  const end       = new Date(body.endDate)
  const totalDays = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24))

  const conflict = await repo.findConflict(body.vehicleId, start, end)
  if (conflict) throw new AppError('Vehicle is not available for the selected dates', 409, 'vehicle_unavailable')

  let discountAmount = 0
  let offerId: string | null = body.offerId ?? null

  if (body.promoCodeUsed) {
    const offer = await repo.findActiveOffer(companyId, body.promoCodeUsed)
    if (offer) {
      offerId = offer.id
      const base = vehicle.dailyRate * totalDays
      if (offer.type === 'PERCENTAGE')     discountAmount = Math.round(base * offer.discountValue / 100)
      else if (offer.type === 'FIXED_AMOUNT') discountAmount = offer.discountValue
      else if (offer.type === 'FREE_DAY')     discountAmount = vehicle.dailyRate * offer.discountValue
      await repo.incrementOfferRedemption(offer.id)
    }
  }

  const depositAmount = body.depositAmount ?? 0
  const additionalDriversList = body.additionalDrivers ?? []
  const baseAmount = vehicle.dailyRate * totalDays
  const { applied, total: pricingTotal } = await applyPricingRules(companyId, body.customerId, additionalDriversList, vehicle.dailyRate, totalDays)
  const totalAmount = baseAmount - discountAmount + pricingTotal + depositAmount

  const reservation = await repo.create({
    companyId,
    vehicleId:      body.vehicleId,
    customerId:     body.customerId,
    startDate:      start,
    endDate:        end,
    pickupLocation: body.pickupLocation ?? null,
    returnLocation: body.returnLocation ?? null,
    offerId,
    promoCodeUsed:  body.promoCodeUsed ?? null,
    source:         'DASHBOARD',
    dailyRate:      vehicle.dailyRate,
    discountAmount,
    totalDays,
    totalAmount,
    depositAmount,
    notes:          body.notes ?? null,
    extras:         body.paymentMode ? { paymentMode: body.paymentMode } : undefined,
    pricingRulesApplied: applied,
    pricingRulesTotal:   pricingTotal,
  })

  const insuranceIds = body.selectedInsurancePolicyIds ?? []
  const additionalDrivers = body.additionalDrivers ?? []
  if (insuranceIds.length > 0) {
    await applyInsurancesToReservation(reservation.id, companyId, insuranceIds, totalDays, baseAmount)
  }
  if (additionalDrivers.length > 0) {
    await applyAdditionalDriversToReservation(reservation.id, companyId, additionalDrivers, totalDays)
  }

  await validateAndFlagLicense(body.customerId, companyId).catch(() => null)
  return reservation
}

export async function updateReservation(id: string, companyId: string, body: {
  startDate?: string; endDate?: string; pickupLocation?: string | null
  returnLocation?: string | null; depositAmount?: number; notes?: string | null
  paymentMode?: string | null; damageChargeAmount?: number; damageChargeNote?: string | null
}) {
  const reservation = await repo.findByIdWithRelations(id, companyId)

  if (reservation.status === 'CANCELLED' || reservation.status === 'NO_SHOW') {
    throw new AppError('This reservation can no longer be edited', 400, 'invalid_status')
  }

  const workflow = buildReservationWorkflow(reservation)
  const requested = Object.keys(body).filter((k) => body[k as keyof typeof body] !== undefined)
  const bookingFields = ['startDate', 'endDate', 'pickupLocation', 'returnLocation', 'depositAmount', 'notes', 'paymentMode']
  const returnFields  = ['returnLocation', 'damageChargeAmount', 'damageChargeNote']

  if (!workflow.coreEditable && !workflow.returnEditable) {
    throw new AppError('This reservation is locked for editing', 400, 'reservation_locked')
  }

  if (workflow.coreEditable) {
    const invalid = requested.filter((f) => !bookingFields.includes(f))
    if (invalid.length > 0) throw new AppError('Only booking details can be edited at this stage', 400, 'invalid_fields')

    const nextStart = body.startDate ? new Date(body.startDate) : reservation.startDate
    const nextEnd   = body.endDate   ? new Date(body.endDate)   : reservation.endDate
    const totalDays = Math.ceil((nextEnd.getTime() - nextStart.getTime()) / (1000 * 60 * 60 * 24))

    if (nextEnd <= nextStart || totalDays <= 0) throw new AppError('End date must be after start date', 400, 'invalid_dates')

    if (body.startDate || body.endDate) {
      const conflict = await repo.findConflict(reservation.vehicleId, nextStart, nextEnd, reservation.id)
      if (conflict) throw new AppError('Vehicle is not available for the selected dates', 409, 'vehicle_unavailable')
    }

    const baseAmount = reservation.dailyRate * totalDays
    const { applied, total: pricingRulesTotal } = await applyPricingRules(
      companyId,
      reservation.customerId,
      reservation.additionalDrivers.map((d: any) => ({ dateOfBirth: d.dateOfBirth, licenseIssuedAt: d.licenseIssuedAt })),
      reservation.dailyRate,
      totalDays,
    )

    const insuranceUpdates = reservation.insurances.map((ins: any) => ({
      id: ins.id,
      totalCharge: calculateUpdatedInsuranceCharge(ins.chargeType, ins.chargeValue, totalDays, baseAmount),
    }))
    const driverUpdates = reservation.additionalDrivers.map((d: any) => ({
      id: d.id,
      totalCharge: calculateUpdatedAdditionalDriverCharge(d.chargeType, d.chargeValue, totalDays),
    }))

    const insuranceTotal       = insuranceUpdates.reduce((s: number, i: any) => s + i.totalCharge, 0)
    const additionalDriverTotal = driverUpdates.reduce((s: number, d: any) => s + d.totalCharge, 0)
    const depositAmount        = body.depositAmount ?? reservation.depositAmount
    const totalAmount = baseAmount - reservation.discountAmount + pricingRulesTotal + insuranceTotal + additionalDriverTotal + depositAmount

    const extras = parseReservationExtras(reservation.extras)
    if (body.paymentMode !== undefined) {
      const next = normalizeOptionalString(body.paymentMode)
      if (next) extras.paymentMode = next
      else delete extras.paymentMode
    }

    await prisma.$transaction([
      prisma.reservation.update({
        where: { id: reservation.id },
        data: {
          startDate:  nextStart,
          endDate:    nextEnd,
          totalDays,
          pickupLocation: body.pickupLocation !== undefined ? normalizeOptionalString(body.pickupLocation) : reservation.pickupLocation,
          returnLocation: body.returnLocation !== undefined ? normalizeOptionalString(body.returnLocation) : reservation.returnLocation,
          depositAmount,
          notes: body.notes !== undefined ? normalizeOptionalString(body.notes) : reservation.notes,
          pricingRulesApplied: applied,
          pricingRulesTotal,
          insuranceTotal,
          additionalDriverTotal,
          totalAmount,
          extras: (Object.keys(extras).length > 0 ? extras : {}) as any,
        },
      }),
      ...insuranceUpdates.map((ins: any) =>
        prisma.reservationInsurance.update({ where: { id: ins.id }, data: { totalCharge: ins.totalCharge } })
      ),
      ...driverUpdates.map((d: any) =>
        prisma.additionalDriver.update({ where: { id: d.id }, data: { totalCharge: d.totalCharge } })
      ),
    ])
  } else {
    const invalid = requested.filter((f) => !returnFields.includes(f))
    if (invalid.length > 0) throw new AppError('Only return details can be edited after vehicle return', 400, 'invalid_fields')

    await prisma.reservation.update({
      where: { id: reservation.id },
      data: {
        returnLocation:     body.returnLocation !== undefined ? normalizeOptionalString(body.returnLocation) : reservation.returnLocation,
        damageChargeAmount: body.damageChargeAmount !== undefined ? body.damageChargeAmount : reservation.damageChargeAmount,
        damageChargeNote:   body.damageChargeNote  !== undefined ? normalizeOptionalString(body.damageChargeNote) : reservation.damageChargeNote,
      },
    })
  }

  return repo.findById(id, companyId).then(serializeReservationForDashboard)
}
