import { Injectable, Logger } from '@nestjs/common'
import type { HostCalendarAccess } from '../stalwart-calendar/calendar-gateway.types.js'

// Sends dezky-branded booking emails via JMAP through the host's own mailbox
// (From = host address). Uses the same app-password access as the calendar
// gateway — no separate SMTP surface. Flow: upload the .ics blob → Email/set a
// draft with text+html bodies and the .ics attachment → EmailSubmission/set to
// send. Never logs message bodies or credentials.
const CORE = 'urn:ietf:params:jmap:core'
const MAIL = 'urn:ietf:params:jmap:mail'
const SUBMISSION = 'urn:ietf:params:jmap:submission'

interface Session {
  apiUrl: string
  uploadUrl: string
  primaryAccounts: Record<string, string>
}

type MethodResponse = [string, Record<string, any>, string]

export interface OutboundEmail {
  to: string
  toName: string
  subject: string
  text: string
  html: string
  ics: string
  icsFilename: string
}

@Injectable()
export class JmapMailer {
  private readonly logger = new Logger(JmapMailer.name)

  async send(access: HostCalendarAccess, msg: OutboundEmail): Promise<void> {
    const auth = `Basic ${Buffer.from(`${access.email}:${access.secret}`).toString('base64')}`
    const origin = new URL(access.jmapSessionUrl).origin

    const session = (await (await fetch(access.jmapSessionUrl, { headers: { Authorization: auth } })).json()) as Session
    const accountId = session.primaryAccounts[MAIL]
    if (!accountId) throw new Error('Host mailbox has no mail capability')
    const apiUrl = origin + new URL(session.apiUrl, origin).pathname

    // 1. Upload the .ics as a blob (internal hostname).
    const uploadPath = new URL(session.uploadUrl.replace('{accountId}', accountId), origin).pathname
    const upRes = await fetch(origin + uploadPath, {
      method: 'POST',
      headers: { Authorization: auth, 'Content-Type': 'text/calendar' },
      body: msg.ics,
    })
    if (!upRes.ok) throw new Error(`blob upload ${upRes.status}`)
    const blobId = ((await upRes.json()) as { blobId: string }).blobId

    // 2. Resolve a filing mailbox (prefer Sent, else Drafts) and the submission
    // identity (required by EmailSubmission/set) for the host address.
    const meta = await this.call(apiUrl, auth, [CORE, MAIL, SUBMISSION], [
      ['Mailbox/get', { accountId, properties: ['role'] }, 'm'],
      ['Identity/get', { accountId, properties: ['email'] }, 'i'],
    ])
    const mailboxes = (meta.find((r) => r[0] === 'Mailbox/get')?.[1]?.list ?? []) as Array<{ id: string; role?: string }>
    const fileMailbox =
      mailboxes.find((m) => m.role === 'sent')?.id ?? mailboxes.find((m) => m.role === 'drafts')?.id ?? mailboxes[0]?.id
    if (!fileMailbox) throw new Error('Host mailbox has no filing folder')
    const identities = (meta.find((r) => r[0] === 'Identity/get')?.[1]?.list ?? []) as Array<{ id: string; email?: string }>
    const identityId =
      identities.find((i) => i.email?.toLowerCase() === access.email.toLowerCase())?.id ?? identities[0]?.id
    if (!identityId) throw new Error('Host mailbox has no submission identity')

    // 3. Create the message + 4. submit it, in one request (back-referencing #msg).
    const resp = await this.call(
      apiUrl,
      auth,
      [CORE, MAIL, SUBMISSION],
      [
        [
          'Email/set',
          {
            accountId,
            create: {
              msg: {
                mailboxIds: { [fileMailbox]: true },
                keywords: { $seen: true },
                from: [{ email: access.email }],
                to: [{ email: msg.to, name: msg.toName }],
                subject: msg.subject,
                bodyValues: {
                  t: { value: msg.text },
                  h: { value: msg.html },
                },
                textBody: [{ partId: 't', type: 'text/plain' }],
                htmlBody: [{ partId: 'h', type: 'text/html' }],
                attachments: [
                  { blobId, type: 'text/calendar; method=PUBLISH; charset=utf-8', name: msg.icsFilename, disposition: 'attachment' },
                ],
              },
            },
          },
          'e',
        ],
        [
          'EmailSubmission/set',
          {
            accountId,
            create: {
              sub: {
                emailId: '#msg',
                identityId,
                envelope: { mailFrom: { email: access.email }, rcptTo: [{ email: msg.to }] },
              },
            },
          },
          's',
        ],
      ],
    )

    const setRes = resp.find((r) => r[0] === 'Email/set')?.[1]
    if (!setRes?.created?.msg) throw new Error(`Email/set failed: ${JSON.stringify(setRes?.notCreated ?? resp)}`)
    const subRes = resp.find((r) => r[0] === 'EmailSubmission/set')?.[1]
    if (!subRes?.created?.sub) throw new Error(`EmailSubmission/set failed: ${JSON.stringify(subRes?.notCreated ?? resp)}`)
  }

  private async call(apiUrl: string, auth: string, using: string[], methodCalls: any[]): Promise<MethodResponse[]> {
    const res = await fetch(apiUrl, {
      method: 'POST',
      headers: { Authorization: auth, 'Content-Type': 'application/json' },
      body: JSON.stringify({ using, methodCalls }),
    })
    const text = await res.text()
    if (!res.ok) throw new Error(`JMAP ${res.status}: ${text.slice(0, 200)}`)
    const json = JSON.parse(text) as { methodResponses?: MethodResponse[] }
    if (!json.methodResponses) throw new Error(`JMAP error: ${text.slice(0, 200)}`)
    return json.methodResponses
  }
}
