import { BadRequestException, ConflictException, Injectable, Logger, NotFoundException } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'
import { Model, Types } from 'mongoose'
import { StalwartClient } from '../../integrations/stalwart.client.js'
import { Host, HostDocument } from '../../schemas/scheduling-host.schema.js'
import { User, UserDocument } from '../../schemas/user.schema.js'
import { CredentialProvisioner } from '../stalwart-calendar/credential-provisioner.service.js'
import { JmapCalendarGateway } from '../stalwart-calendar/jmap-calendar.gateway.js'

export interface CreateHostInput {
  // Mongo id of the workspace user to make bookable. Mailbox, OIDC subject and
  // display name are derived from this user, never supplied by the caller.
  userId: string
  slug: string
  timezone: string
}

// Manages bookable hosts. Making a user bookable auto-provisions calendar access
// (app password) and discovers the host's default calendar — no user-facing
// "connect calendar" step. This is the integration coherence that is the product
// moat (CLAUDE.md §rationale).
@Injectable()
export class HostsService {
  private readonly logger = new Logger(HostsService.name)

  constructor(
    @InjectModel(Host.name) private readonly hostModel: Model<HostDocument>,
    @InjectModel(User.name) private readonly userModel: Model<UserDocument>,
    private readonly stalwart: StalwartClient,
    private readonly provisioner: CredentialProvisioner,
    private readonly gateway: JmapCalendarGateway,
  ) {}

  async create(tenantId: Types.ObjectId, input: CreateHostInput): Promise<HostDocument> {
    // Resolve the user (must belong to this tenant) and derive their identity.
    const user = await this.userModel.findOne({ _id: input.userId, tenantIds: tenantId }).exec()
    if (!user) throw new BadRequestException('User not found in this tenant.')
    if (!user.mailboxAddress) {
      throw new BadRequestException(`${user.name} has no workspace mailbox — only users with a mailbox can be hosts.`)
    }
    const email = user.mailboxAddress
    // Prefer the account id captured at user provisioning; fall back to a lookup.
    const accountId = user.stalwartAccountId ?? (await this.stalwart.findAccountIdByEmail(email))
    if (!accountId) {
      throw new BadRequestException(`No Stalwart mailbox found for ${email} — provision the mailbox first.`)
    }

    let host: HostDocument
    try {
      host = await this.hostModel.create({
        tenantId,
        authentikUserId: user.authentikSubjectId,
        email,
        displayName: user.name,
        slug: input.slug,
        timezone: input.timezone,
        stalwartAccountId: accountId,
        busyCalendarIds: [],
        isActive: true,
      })
    } catch (err: any) {
      if (err?.code === 11000) throw new ConflictException('A host with that slug or user already exists in this tenant.')
      throw err
    }

    // Provision the credential, then discover + persist the default calendar.
    try {
      const access = await this.provisioner.provisionForHost(host)
      const calendars = await this.gateway.listCalendars(access)
      const defaultCalendarId = calendars[0]?.id
      if (defaultCalendarId) {
        host.defaultCalendarId = defaultCalendarId
        host.busyCalendarIds = [defaultCalendarId]
        await host.save()
      } else {
        this.logger.warn(`Host ${email} has no calendar to write to.`)
      }
    } catch (err) {
      // Roll back the host row so a failed provisioning doesn't leave a half-host.
      await this.hostModel.deleteOne({ _id: host._id }).exec()
      throw new BadRequestException(`Could not provision calendar access for ${email}: ${(err as Error).message}`)
    }

    return host
  }

  list(tenantId: Types.ObjectId): Promise<HostDocument[]> {
    return this.hostModel.find({ tenantId }).sort({ displayName: 1 }).exec()
  }

  async getById(tenantId: Types.ObjectId, hostId: string): Promise<HostDocument> {
    const host = await this.hostModel.findOne({ _id: hostId, tenantId }).exec()
    if (!host) throw new NotFoundException('Host not found')
    return host
  }

  async getBySlug(tenantId: Types.ObjectId, slug: string): Promise<HostDocument> {
    const host = await this.hostModel.findOne({ tenantId, slug }).exec()
    if (!host) throw new NotFoundException('Host not found')
    return host
  }

  async setActive(tenantId: Types.ObjectId, hostId: string, isActive: boolean): Promise<HostDocument> {
    const host = await this.getById(tenantId, hostId)
    host.isActive = isActive
    return host.save()
  }

  async rotateCredential(tenantId: Types.ObjectId, hostId: string): Promise<void> {
    const host = await this.getById(tenantId, hostId)
    await this.provisioner.provisionForHost(host)
  }

  async remove(tenantId: Types.ObjectId, hostId: string): Promise<void> {
    const host = await this.getById(tenantId, hostId)
    await this.provisioner.deprovisionForHost(host._id, host.stalwartAccountId)
    await this.hostModel.deleteOne({ _id: host._id }).exec()
  }
}
