import { Injectable, NotFoundException, ServiceUnavailableException } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'
import { Model, Types } from 'mongoose'
import { AvailabilitySchedule, AvailabilityScheduleDocument } from '../../schemas/availability-schedule.schema.js'
import { Booking, BookingDocument } from '../../schemas/booking.schema.js'
import { EventTypeDocument } from '../../schemas/event-type.schema.js'
import { Host, HostDocument } from '../../schemas/scheduling-host.schema.js'
import { SlotLock, SlotLockDocument } from '../../schemas/slot-lock.schema.js'
import { CredentialProvisioner } from '../stalwart-calendar/credential-provisioner.service.js'
import { JmapCalendarGateway } from '../stalwart-calendar/jmap-calendar.gateway.js'
import { computeSlots, ComputedSlot } from './slot-computer.js'

@Injectable()
export class SlotService {
  constructor(
    @InjectModel(AvailabilitySchedule.name) private readonly scheduleModel: Model<AvailabilityScheduleDocument>,
    @InjectModel(Booking.name) private readonly bookingModel: Model<BookingDocument>,
    @InjectModel(SlotLock.name) private readonly lockModel: Model<SlotLockDocument>,
    @InjectModel(Host.name) private readonly hostModel: Model<HostDocument>,
    private readonly provisioner: CredentialProvisioner,
    private readonly gateway: JmapCalendarGateway,
  ) {}

  // ── Round-robin (team) availability ─────────────────────────────────────────
  // Resolves the effective pool for a round-robin event type: the configured
  // hostPool restricted to ACTIVE hosts in the event type's tenant, with the
  // owning host always included. Single-host event types never call this.
  async resolvePoolHosts(eventType: EventTypeDocument, owner: HostDocument): Promise<HostDocument[]> {
    const poolIds = (eventType.hostPool ?? []).map((id) => id.toString())
    // Always fold in the event type's owning host and the caller-supplied owner
    // (these can differ on the reschedule path, where `owner` is the host the
    // existing booking was assigned to). Dedup by id.
    const ids = new Set<string>([eventType.hostId.toString(), owner._id.toString(), ...poolIds])
    const hosts = await this.hostModel
      .find({ _id: { $in: [...ids].map((id) => new Types.ObjectId(id)) }, tenantId: eventType.tenantId, isActive: true })
      .exec()
    // Owner may have been deactivated; if nothing active remains, fall back to the
    // owner so the page degrades to single-host rather than 500-ing.
    return hosts.length ? hosts : [owner]
  }

  // Union of every pooled host's free slots: a start time is offered if it is free
  // for AT LEAST ONE pooled host. Per-host calendar errors are tolerated (that host
  // simply contributes no slots) UNLESS every host fails, in which case we fail
  // closed (503) exactly like the single-host path — we never show a slot we cannot
  // verify for anyone. The chosen host is decided later, at booking time.
  async unionAvailableSlots(
    hosts: HostDocument[],
    eventType: EventTypeDocument,
    fromUtc: Date,
    toUtc: Date,
    now: Date = new Date(),
  ): Promise<ComputedSlot[]> {
    const results = await Promise.allSettled(
      hosts.map((h) => this.availableSlots(h, eventType, fromUtc, toUtc, now)),
    )
    const fulfilled = results.filter((r): r is PromiseFulfilledResult<ComputedSlot[]> => r.status === 'fulfilled')
    if (!fulfilled.length) {
      throw new ServiceUnavailableException('Calendar is temporarily unavailable — please retry.')
    }
    const byStart = new Map<number, ComputedSlot>()
    for (const r of fulfilled) {
      for (const slot of r.value) byStart.set(slot.startUtc.getTime(), slot)
    }
    return [...byStart.values()].sort((a, b) => a.startUtc.getTime() - b.startUtc.getTime())
  }

  // Free UTC slots for a host+event-type within [fromUtc, toUtc). Fails CLOSED on
  // calendar errors (§9): rather than show slots we can't verify against live
  // free/busy — risking a double-book — we surface 503 so the UI shows a retry.
  async availableSlots(
    host: HostDocument,
    eventType: EventTypeDocument,
    fromUtc: Date,
    toUtc: Date,
    now: Date = new Date(),
  ): Promise<ComputedSlot[]> {
    const schedule = await this.scheduleModel.findById(eventType.availabilityScheduleId).exec()
    if (!schedule) throw new NotFoundException('Event type has no availability schedule')

    const access = await this.provisioner.resolveAccess(host)

    let calendarBusy
    try {
      calendarBusy = await this.gateway.getBusyIntervals(access, fromUtc, toUtc, {
        ignoreAllDayEvents: eventType.ignoreAllDayEvents,
      })
    } catch {
      throw new ServiceUnavailableException('Calendar is temporarily unavailable — please retry.')
    }

    const [bookingBusy, lockBusy] = await Promise.all([
      this.confirmedBookingIntervals(host._id, fromUtc, toUtc),
      this.activeLockIntervals(host._id, fromUtc, toUtc, now),
    ])

    return computeSlots({
      durationMinutes: eventType.durationMinutes,
      slotIntervalMinutes: eventType.slotIntervalMinutes,
      bufferBeforeMinutes: eventType.bufferBeforeMinutes,
      bufferAfterMinutes: eventType.bufferAfterMinutes,
      minimumNoticeMinutes: eventType.minimumNoticeMinutes,
      maximumDaysInFuture: eventType.maximumDaysInFuture,
      scheduleTimezone: schedule.timezone,
      weeklyRules: schedule.weeklyRules,
      dateOverrides: schedule.dateOverrides,
      busy: [...calendarBusy, ...bookingBusy, ...lockBusy],
      now,
      fromUtc,
      toUtc,
    })
  }

  private async confirmedBookingIntervals(hostId: Types.ObjectId, from: Date, to: Date) {
    const bookings = await this.bookingModel
      .find({ hostId, status: { $in: ['confirmed', 'pending'] }, startUtc: { $lt: to }, endUtc: { $gt: from } })
      .select('startUtc endUtc')
      .exec()
    return bookings.map((b) => ({ startUtc: b.startUtc, endUtc: b.endUtc }))
  }

  private async activeLockIntervals(hostId: Types.ObjectId, from: Date, to: Date, now: Date) {
    const locks = await this.lockModel
      .find({
        hostId,
        startUtc: { $lt: to },
        endUtc: { $gt: from },
        $or: [{ expiresAt: null }, { expiresAt: { $gt: now } }],
      })
      .select('startUtc endUtc')
      .exec()
    return locks.map((l) => ({ startUtc: l.startUtc, endUtc: l.endUtc }))
  }
}
