import { Injectable, Logger, NotFoundException } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
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 { StalwartCredential, StalwartCredentialDocument } from '../../schemas/stalwart-credential.schema.js'
import { CredentialCipher } from '../crypto/credential-cipher.js'
import type { HostCalendarAccess } from './calendar-gateway.types.js'

// Owns the lifecycle of a host's encrypted Stalwart calendar credential. At
// onboarding it mints an app password on-behalf via the admin JMAP and stores it
// AES-256-GCM-encrypted; at read time it decrypts into a HostCalendarAccess the
// gateway can use. No user-facing "connect calendar" step — this is the whole
// point of owning both identity and the calendar server (CLAUDE.md §rationale).
@Injectable()
export class CredentialProvisioner {
  private readonly logger = new Logger(CredentialProvisioner.name)
  private readonly sessionUrl: string

  constructor(
    private readonly stalwart: StalwartClient,
    private readonly cipher: CredentialCipher,
    config: ConfigService,
    @InjectModel(StalwartCredential.name)
    private readonly credModel: Model<StalwartCredentialDocument>,
  ) {
    // .well-known/jmap on the internal Stalwart hostname (STALWART_API_URL).
    const base = config.getOrThrow<string>('STALWART_API_URL').replace(/\/$/, '')
    this.sessionUrl = `${base}/.well-known/jmap`
  }

  // Mint + persist (or rotate) a host's app-password credential. Destroys any
  // prior app password first so we never leak orphaned credentials. Returns the
  // freshly built access so the caller can immediately list calendars.
  async provisionForHost(host: HostDocument): Promise<HostCalendarAccess> {
    const existing = await this.credModel.findOne({ hostId: host._id }).exec()
    if (existing?.appPasswordId) {
      await this.stalwart
        .deleteAppPassword(host.stalwartAccountId, existing.appPasswordId)
        .catch((e) => this.logger.warn(`Could not remove old app password for ${host.email}: ${e.message}`))
    }

    const { id, secret } = await this.stalwart.createAppPassword(
      host.stalwartAccountId,
      `dezky-scheduling:${host.slug}`,
    )
    const sealed = this.cipher.seal(secret)

    await this.credModel.updateOne(
      { hostId: host._id },
      {
        $set: {
          tenantId: host.tenantId,
          type: 'app_password',
          encryptedSecret: sealed.encryptedSecret,
          iv: sealed.iv,
          authTag: sealed.authTag,
          appPasswordId: id,
          jmapSessionUrl: this.sessionUrl,
          lastValidatedAt: new Date(),
        },
      },
      { upsert: true },
    )
    this.logger.log(`Provisioned scheduling credential for host ${host.email}`)

    return {
      email: host.email,
      secret,
      jmapSessionUrl: this.sessionUrl,
      defaultCalendarId: host.defaultCalendarId,
      busyCalendarIds: host.busyCalendarIds?.length ? host.busyCalendarIds : host.defaultCalendarId ? [host.defaultCalendarId] : [],
    }
  }

  // Decrypt a host's stored credential into ready-to-use access. Throws if the
  // host was never provisioned.
  async resolveAccess(host: HostDocument): Promise<HostCalendarAccess> {
    const cred = await this.credModel.findOne({ hostId: host._id }).exec()
    if (!cred) throw new NotFoundException(`Host ${host.email} has no scheduling credential — provision first`)
    const secret = this.cipher.open({ encryptedSecret: cred.encryptedSecret, iv: cred.iv, authTag: cred.authTag })
    return {
      email: host.email,
      secret,
      jmapSessionUrl: cred.jmapSessionUrl,
      defaultCalendarId: host.defaultCalendarId,
      busyCalendarIds: host.busyCalendarIds?.length ? host.busyCalendarIds : host.defaultCalendarId ? [host.defaultCalendarId] : [],
    }
  }

  // Remove a host's app password from Stalwart and delete the stored credential.
  async deprovisionForHost(hostId: Types.ObjectId, stalwartAccountId: string): Promise<void> {
    const cred = await this.credModel.findOne({ hostId }).exec()
    if (cred?.appPasswordId) {
      await this.stalwart
        .deleteAppPassword(stalwartAccountId, cred.appPasswordId)
        .catch((e) => this.logger.warn(`Could not remove app password: ${e.message}`))
    }
    await this.credModel.deleteOne({ hostId }).exec()
  }
}
