import { AppError, NotFoundError, ConflictError } from '../../http/errors'
import { getVehicleAvailabilitySummary } from '../../services/vehicleAvailabilityService'
import { sendNotification, sendTransactionalEmail } from '../../services/notificationService'
import { carplaceReservationEmail, type Lang } from '../../lib/emailTranslations'
import * as repo from './carplace.repo'

function normalizeLocation(value?: string | null) {
  const normalized = value?.trim()
  return normalized ? normalized : null
}

function normalizeLocationList(values?: string[] | null) {
  if (!Array.isArray(values)) return []
  return values
    .map((value) => normalizeLocation(value))
    .filter((value): value is string => Boolean(value))
}

function includesLocation(values: string[], target: string) {
  const key = target.toLocaleLowerCase()
  return values.some((value) => value.toLocaleLowerCase() === key)
}

function calculateOfferDiscount(
  offer: { type?: string; discountValue?: number; specialRate?: number | null; maxRedemptions?: number | null; redemptionCount?: number | null },
  baseSubtotal: number,
  dailyRate: number,
  rentalDays: number,
) {
  if (offer.maxRedemptions != null && (offer.redemptionCount ?? 0) >= offer.maxRedemptions) {
    throw new AppError('Offer code has reached its redemption limit', 409, 'offer_exhausted')
  }

  const value = Number(offer.discountValue ?? 0)
  let discount = 0
  if (offer.type === 'PERCENTAGE') discount = Math.round(baseSubtotal * (value / 100))
  else if (offer.type === 'FIXED_AMOUNT') discount = value
  else if (offer.type === 'FREE_DAY') discount = dailyRate * value
  else if (offer.type === 'SPECIAL_RATE') {
    const specialRate = Number(offer.specialRate ?? value)
    discount = Math.max(0, baseSubtotal - (specialRate * rentalDays))
  }
  return Math.min(baseSubtotal, Math.max(0, Math.round(discount)))
}

function matchesVehicleLocationRules(
  vehicle: { pickupLocations?: string[]; dropoffLocations?: string[]; allowDifferentDropoff?: boolean },
  params: { pickupLocation?: string; dropoffLocation?: string; dropoffMode?: 'same' | 'different' },
) {
  const pickupLocation = normalizeLocation(params.pickupLocation)
  const dropoffLocation = normalizeLocation(params.dropoffLocation)
  const pickupLocations = normalizeLocationList(vehicle.pickupLocations)
  const dropoffLocations = normalizeLocationList(vehicle.dropoffLocations)

  if (pickupLocation && pickupLocations.length > 0 && !includesLocation(pickupLocations, pickupLocation)) {
    return false
  }

  if (params.dropoffMode === 'different') {
    if (!vehicle.allowDifferentDropoff) return false
    if (dropoffLocation && dropoffLocations.length > 0 && !includesLocation(dropoffLocations, dropoffLocation)) {
      return false
    }
  }

  return true
}

export async function getPublicOffers() {
  return repo.findPublicOffers()
}

export async function getCities() {
  const rows = await repo.findCitiesFromCompanies()
  const map = new Map<string, string>()
  for (const row of rows) {
    const city = row.brand?.publicCity?.trim()
    if (!city) continue
    const key = city.toLocaleLowerCase()
    if (!map.has(key)) map.set(key, city)
  }
  return Array.from(map.values()).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
}

export async function getListedCompanies(params: {
  city?: string; hasOffer?: string; page?: number; pageSize?: number
}) {
  const page     = params.page     ?? 1
  const pageSize = params.pageSize ?? 20
  const where: any = { status: { in: ['ACTIVE', 'TRIALING'] }, brand: { isListedOnCarplace: true } }
  if (params.city) where.brand = { ...where.brand, publicCity: { contains: params.city, mode: 'insensitive' } }
  if (params.hasOffer === 'true') {
    where.offers = { some: { isPublic: true, isActive: true, validUntil: { gte: new Date() } } }
  }
  return repo.findListedCompanies(where, (page - 1) * pageSize, pageSize)
}

export async function searchVehicles(params: {
  city?: string; pickupLocation?: string; dropoffLocation?: string; dropoffMode?: 'same' | 'different'
  startDate?: string; endDate?: string; category?: string
  maxPrice?: number; transmission?: string; make?: string; model?: string
  page?: number; pageSize?: number
}) {
  const result = await searchVehiclesPage(params)
  return result.items
}

export async function searchVehiclesPage(params: {
  city?: string; pickupLocation?: string; dropoffLocation?: string; dropoffMode?: 'same' | 'different'
  startDate?: string; endDate?: string; category?: string
  maxPrice?: number; transmission?: string; make?: string; model?: string
  page?: number; pageSize?: number
}) {
  const page = params.page ?? 1
  const pageSize = params.pageSize ?? 20
  const where: any = {
    isPublished: true,
    status: 'AVAILABLE',
    company: { status: { in: ['ACTIVE', 'TRIALING'] }, brand: { isListedOnCarplace: true } },
  }
  if (params.category) where.category = params.category
  if (params.maxPrice !== undefined) where.dailyRate = { lte: params.maxPrice }
  if (params.transmission) where.transmission = params.transmission
  if (params.make) where.make = { contains: params.make, mode: 'insensitive' }
  if (params.model) where.model = { contains: params.model, mode: 'insensitive' }
  if (params.city) {
    where.company = { ...where.company, brand: { isListedOnCarplace: true, publicCity: { contains: params.city, mode: 'insensitive' } } }
  }

  let range: { startDate: Date; endDate: Date } | undefined
  if (params.startDate || params.endDate) {
    if (!params.startDate || !params.endDate) throw new AppError('Both start and end dates are required', 400, 'invalid_dates')
    const startDate = new Date(params.startDate)
    const endDate = new Date(params.endDate)
    if (!Number.isFinite(startDate.getTime()) || !Number.isFinite(endDate.getTime()) || endDate <= startDate) {
      throw new AppError('End date must be after start date', 400, 'invalid_dates')
    }
    range = { startDate, endDate }
  }

  const vehicles = await repo.findPublishedVehicles(where) as any[]
  const locationFiltered = vehicles.filter((vehicle: any) => matchesVehicleLocationRules(vehicle, params))
  const availabilityByVehicle = new Map<string, any>()

  let availableVehicles = locationFiltered
  if (range) {
    const availability = await Promise.all(
      locationFiltered.map(async (vehicle: any) => {
        const result = await getVehicleAvailabilitySummary(vehicle.id, { companyId: vehicle.companyId, range })
        availabilityByVehicle.set(vehicle.id, result)
        return result.available ? vehicle : null
      }),
    )
    availableVehicles = availability.filter((vehicle): vehicle is any => Boolean(vehicle))
  }

  const pageItems = availableVehicles.slice((page - 1) * pageSize, page * pageSize)
  const items = pageItems.map((vehicle: any) => {
    const availability = availabilityByVehicle.get(vehicle.id)
    return {
      ...vehicle,
      availability: availability?.available ?? true,
      availabilityStatus: availability?.status ?? 'AVAILABLE',
      nextAvailableAt: availability?.nextAvailableAt ?? null,
    }
  })

  const countValues = (key: 'category' | 'transmission' | 'make') => {
    const counts = new Map<string, number>()
    for (const vehicle of availableVehicles) {
      const value = typeof vehicle[key] === 'string' ? vehicle[key].trim() : ''
      if (value) counts.set(value, (counts.get(value) ?? 0) + 1)
    }
    return Array.from(counts, ([value, count]) => ({ value, count })).sort((a, b) => a.value.localeCompare(b.value))
  }
  const prices = availableVehicles.map((vehicle) => Number(vehicle.dailyRate)).filter(Number.isFinite)

  return {
    items,
    pagination: {
      page,
      pageSize,
      totalItems: availableVehicles.length,
      totalPages: Math.ceil(availableVehicles.length / pageSize),
    },
    facets: {
      categories: countValues('category'),
      transmissions: countValues('transmission'),
      makes: countValues('make'),
      price: {
        min: prices.length ? Math.min(...prices) : null,
        max: prices.length ? Math.max(...prices) : null,
      },
    },
  }
}

export async function getCarplaceVehicleAvailability(
  slug: string,
  vehicleId: string,
  params: { startDate: string; endDate: string },
) {
  const vehicle = await repo.findVehicleForCarplace(vehicleId, slug)
  if (!vehicle) throw new NotFoundError('Vehicle not found')

  const startDate = new Date(params.startDate)
  const endDate = new Date(params.endDate)

  if (endDate <= startDate) {
    throw new AppError('End date must be after start date', 400, 'invalid_dates')
  }

  const availability = await getVehicleAvailabilitySummary(vehicle.id, {
    companyId: vehicle.companyId,
    range: { startDate, endDate },
  })

  return {
    available: availability.available,
    availabilityStatus: availability.status,
    nextAvailableAt: availability.nextAvailableAt?.toISOString() ?? null,
    blockingReason: availability.blockingReason,
  }
}

export async function createCarplaceQuote(body: {
  vehicleId: string
  startDate: string
  endDate: string
  pickupLocation?: string
  returnLocation?: string
  promoCode?: string
}) {
  const startDate = new Date(body.startDate)
  const endDate = new Date(body.endDate)
  if (!Number.isFinite(startDate.getTime()) || !Number.isFinite(endDate.getTime()) || endDate <= startDate) {
    throw new AppError('End date must be after start date', 400, 'invalid_dates')
  }

  const vehicle = await repo.findVehicleForCarplaceById(body.vehicleId)
  if (!vehicle) throw new NotFoundError('Vehicle not found')

  const pickupLocation = normalizeLocation(body.pickupLocation)
  const returnLocation = normalizeLocation(body.returnLocation) ?? pickupLocation
  const pickupLocations = normalizeLocationList(vehicle.pickupLocations)
  const dropoffLocations = normalizeLocationList(vehicle.dropoffLocations)

  if (pickupLocation && pickupLocations.length > 0 && !includesLocation(pickupLocations, pickupLocation)) {
    throw new AppError('Selected pickup location is not available for this vehicle', 400, 'invalid_pickup_location')
  }
  if (pickupLocation && returnLocation && pickupLocation.toLocaleLowerCase() !== returnLocation.toLocaleLowerCase()) {
    if (!vehicle.allowDifferentDropoff) throw new AppError('This vehicle must be returned to the same pickup location', 400, 'same_dropoff_required')
    if (dropoffLocations.length > 0 && !includesLocation(dropoffLocations, returnLocation)) {
      throw new AppError('Selected return location is not available for this vehicle', 400, 'invalid_return_location')
    }
  }

  const availability = await getVehicleAvailabilitySummary(vehicle.id, {
    companyId: vehicle.companyId,
    range: { startDate, endDate },
  })
  const rentalDays = Math.max(1, Math.ceil((endDate.getTime() - startDate.getTime()) / 86_400_000))
  const baseSubtotal = vehicle.dailyRate * rentalDays
  let discountAmount = 0
  const discounts: Array<{ label: string; amount: number }> = []

  if (body.promoCode) {
    const offer = await repo.findOfferByCode(body.promoCode.trim())
    if (!offer || offer.companyId !== vehicle.companyId) throw new AppError('Offer code is invalid or expired', 400, 'offer_invalid')
    discountAmount = calculateOfferDiscount(offer, baseSubtotal, vehicle.dailyRate, rentalDays)
    discounts.push({ label: offer.title, amount: discountAmount })
  }

  return {
    available: availability.available,
    availabilityStatus: availability.status,
    nextAvailableAt: availability.nextAvailableAt?.toISOString() ?? null,
    currency: 'MAD' as const,
    rentalDays,
    dailyRate: vehicle.dailyRate,
    baseSubtotal,
    discounts,
    fees: [],
    taxes: [],
    estimatedTotal: Math.max(0, baseSubtotal - discountAmount),
    securityDeposit: null,
    finalPriceRequiresCompanyConfirmation: true as const,
  }
}

function generateBookingReference() {
  const year = new Date().getUTCFullYear()
  const random = Math.random().toString(36).slice(2, 8).toUpperCase()
  return `BK-${year}-${random}`
}

export async function createCarplaceReservation(body: {
  vehicleId: string; companySlug?: string; firstName: string; lastName: string
  email: string; phone?: string; driverAge?: number; dateOfBirth?: string; nationality?: string
  identityDocumentNumber?: string; fullAddress?: string
  driverLicense?: string; licenseExpiry?: string; licenseIssuedAt?: string
  licenseCountry?: string; licenseCategory?: string; internationalLicenseNumber?: string
  startDate: string; endDate: string
  pickupLocation?: string; returnLocation?: string; promoCode?: string; notes?: string; language?: string
}) {
  const startDate = new Date(body.startDate)
  const endDate   = new Date(body.endDate)

  if (endDate <= startDate) {
    throw new AppError('End date must be after start date', 400, 'invalid_dates')
  }

  const vehicle = await repo.findVehicleForCarplaceById(body.vehicleId)
  if (!vehicle) throw new NotFoundError('Vehicle not found')

  const pickupLocation = normalizeLocation(body.pickupLocation)
  const returnLocation = normalizeLocation(body.returnLocation) ?? pickupLocation
  const pickupLocations = normalizeLocationList(vehicle.pickupLocations)
  const dropoffLocations = normalizeLocationList(vehicle.dropoffLocations)

  if (pickupLocation && pickupLocations.length > 0 && !includesLocation(pickupLocations, pickupLocation)) {
    throw new AppError('Selected pickup location is not available for this vehicle', 400, 'invalid_pickup_location')
  }

  if (pickupLocation && returnLocation && pickupLocation.toLocaleLowerCase() !== returnLocation.toLocaleLowerCase()) {
    if (!vehicle.allowDifferentDropoff) {
      throw new AppError('This vehicle must be returned to the same pickup location', 400, 'same_dropoff_required')
    }
    if (dropoffLocations.length > 0 && !includesLocation(dropoffLocations, returnLocation)) {
      throw new AppError('Selected return location is not available for this vehicle', 400, 'invalid_return_location')
    }
  }

  const availability = await getVehicleAvailabilitySummary(vehicle.id, { companyId: vehicle.companyId, range: { startDate, endDate } })
  if (!availability.available) {
    throw new AppError('Vehicle is not available for the selected dates', 409, 'unavailable', {
      nextAvailableAt: availability.nextAvailableAt?.toISOString() ?? null,
    })
  }

  const customer = await repo.upsertCarplaceCustomer(vehicle.companyId, {
    email: body.email,
    firstName: body.firstName,
    lastName: body.lastName,
    phone: body.phone,
    dateOfBirth: body.dateOfBirth,
    nationality: body.nationality,
    identityDocumentNumber: body.identityDocumentNumber,
    fullAddress: body.fullAddress,
    driverLicense: body.driverLicense,
    licenseExpiry: body.licenseExpiry,
    licenseIssuedAt: body.licenseIssuedAt,
    licenseCountry: body.licenseCountry,
    licenseCategory: body.licenseCategory,
    internationalLicenseNumber: body.internationalLicenseNumber,
  })

  const totalDays = Math.max(1, Math.ceil((endDate.getTime() - startDate.getTime()) / 86_400_000))
  const baseSubtotal = vehicle.dailyRate * totalDays
  let discountAmount = 0
  if (body.promoCode) {
    const offer = await repo.findOfferByCode(body.promoCode.trim())
    if (!offer || offer.companyId !== vehicle.companyId) throw new AppError('Offer code is invalid or expired', 400, 'offer_invalid')
    discountAmount = calculateOfferDiscount(offer, baseSubtotal, vehicle.dailyRate, totalDays)
  }
  const totalAmount = Math.max(0, baseSubtotal - discountAmount)

  const reservationNotes = [body.notes?.trim(), body.driverAge ? `Driver age: ${body.driverAge}` : null].filter(Boolean).join('\n').slice(0, 500) || undefined

  const reservation = await repo.createCarplaceReservation({
    companyId: vehicle.companyId,
    vehicleId: body.vehicleId,
    customerId: customer.id,
    startDate,
    endDate,
    pickupLocation,
    returnLocation,
    dailyRate: vehicle.dailyRate,
    totalDays,
    totalAmount,
    notes: reservationNotes,
    bookingReference: generateBookingReference(),
  })

  const lang        = (body.language ?? 'fr') as Lang
  const companyName = vehicle.company.brand?.displayName ?? (vehicle.company as any).name
  const emailOpts   = {
    firstName: body.firstName, vehicleYear: vehicle.year, vehicleMake: vehicle.make, vehicleModel: vehicle.model,
    companyName, startDate, endDate, totalDays,
    rateDisplay: (vehicle.dailyRate / 100).toFixed(2),
    totalDisplay: (totalAmount / 100).toFixed(2),
    email: body.email, phone: body.phone,
  }

  sendTransactionalEmail({
    to: body.email,
    subject: carplaceReservationEmail.subject(`${vehicle.make} ${vehicle.model}`, lang),
    html: carplaceReservationEmail.html(emailOpts, lang),
    text: carplaceReservationEmail.text(emailOpts, lang),
  }).catch(() => null)

  sendNotification({
    type: 'NEW_BOOKING',
    title: 'New Carplace Reservation Request',
    body: `${body.firstName} ${body.lastName} requested ${vehicle.make} ${vehicle.model} from ${startDate.toISOString().slice(0, 10)} to ${endDate.toISOString().slice(0, 10)}.`,
    companyId: vehicle.companyId,
    channels: ['IN_APP'],
  }).catch(() => null)

  return {
    reservationId: reservation.id,
    bookingReference: (reservation as any).bookingReference ?? null,
    companyName,
    vehicleName: `${vehicle.year} ${vehicle.make} ${vehicle.model}`,
    startDate: startDate.toISOString(),
    endDate: endDate.toISOString(),
    status: 'PENDING',
    message: 'The company will review your request.',
  }
}

export async function trackCarplaceFunnelEvent(body: {
  eventName: string
  companySlug: string
  vehicleId: string
  sessionId?: string
  path?: string
  metadata?: Record<string, string | number | boolean | null>
}, renterId?: string) {
  await repo.createCarplaceFunnelEvent({
    ...body,
    renterId: renterId ?? null,
  })

  return { success: true }
}

export async function getCompanyPage(slug: string) {
  const company = await repo.findCompanyPage(slug)
  if (!company) throw new NotFoundError('Company not found')

  const vehicles = await Promise.all(
    (company.vehicles as any[]).map(async (v: any) => {
      const a = await getVehicleAvailabilitySummary(v.id, { companyId: v.companyId })
      return { ...v, availability: a.available, availabilityStatus: a.status, nextAvailableAt: a.nextAvailableAt }
    }),
  )
  return { ...company, vehicles }
}

export async function getCompanyReviews(slug: string) {
  const company = await repo.findCompanyBySlug(slug)
  return repo.findCompanyReviews(company.id)
}

export async function getCompanyVehicles(slug: string) {
  const company = await repo.findCompanyBySlug(slug)
  return repo.findCompanyVehicles(company.id)
}

export async function getVehicleDetail(slug: string, vehicleId: string) {
  const vehicle    = await repo.findVehicleById(slug, vehicleId)
  const availability = await getVehicleAvailabilitySummary(vehicle.id, { companyId: vehicle.companyId })
  return { ...vehicle, availability: availability.available, availabilityStatus: availability.status, nextAvailableAt: availability.nextAvailableAt }
}

export async function getCompanyOffers(slug: string) {
  const company = await repo.findCompanyBySlug(slug)
  return repo.findCompanyOffers(company.id)
}

export async function getReviewContext(token: string) {
  const reservation = await repo.findReservationByReviewToken(token)
  if (!reservation) throw new NotFoundError('Review link is invalid or has expired')
  if (reservation.review) throw new ConflictError('A review has already been submitted for this reservation')

  return {
    reservationId:  reservation.id,
    companyName:    reservation.company.brand?.displayName ?? (reservation.company as any).name,
    companyLogoUrl: reservation.company.brand?.logoUrl ?? null,
    vehicle:        `${reservation.vehicle.year} ${reservation.vehicle.make} ${reservation.vehicle.model}`,
    vehiclePhoto:   reservation.vehicle.photos[0] ?? null,
  }
}

export async function submitReview(token: string, body: {
  overallRating: number; vehicleRating?: number; serviceRating?: number; comment?: string
}) {
  const reservation = await repo.findReservationForReviewSubmit(token)
  if (!reservation) throw new NotFoundError('Review link is invalid or has expired')
  if (reservation.review) throw new ConflictError('A review has already been submitted for this reservation')

  const review = await repo.createReview({
    reservationId: reservation.id,
    companyId:     reservation.companyId,
    renterId:      reservation.renterId,
    ...body,
  })
  await repo.invalidateReviewToken(reservation.id)
  return { reviewId: review.id }
}

export async function validateOfferCode(code: string) {
  const offer = await repo.findOfferByCode(code)
  if (!offer) throw new NotFoundError('Promo code not found or expired')
  if (offer.maxRedemptions && offer.redemptionCount >= offer.maxRedemptions) {
    throw new ConflictError('Promo code has reached its maximum redemptions')
  }
  return offer
}
