import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'
import { Model, Types } from 'mongoose'
import { AuditService, type AuditActor } from '../audit/audit.service.js'
import { StalwartClient } from '../integrations/stalwart.client.js'
import { Domain, DomainDocument } from '../schemas/domain.schema.js'
import { User, UserDocument } from '../schemas/user.schema.js'

// Tenant-scoped view of the bits of Stalwart mail config that are per-customer:
// aliases (extra addresses on a mailbox) and distribution lists. Both are keyed
// to one of the tenant's domains, so they're naturally isolated per workspace.

export interface AliasView {
  address: string // localPart@domain
  localPart: string
  domain: string
  destination: string // the mailbox it delivers to
  accountId: string // Stalwart account holding the alias
  enabled: boolean
}

export interface ListView {
  id: string
  name: string
  address: string
  domain: string
  recipients: string[]
  members: number
  description?: string
}

export interface TenantRef {
  _id: Types.ObjectId
  slug: string
}

@Injectable()
export class MailService {
  constructor(
    @InjectModel(Domain.name) private readonly domainModel: Model<DomainDocument>,
    @InjectModel(User.name) private readonly userModel: Model<UserDocument>,
    private readonly stalwart: StalwartClient,
    private readonly audit: AuditService,
  ) {}

  // Maps between a tenant's domain names and their Stalwart domain ids, both ways.
  private async tenantDomains(
    tenantId: Types.ObjectId,
  ): Promise<{ byStalwartId: Map<string, string>; byName: Map<string, string> }> {
    const domains = await this.domainModel
      .find({ tenantId, stalwartId: { $exists: true } }, { domain: 1, stalwartId: 1 })
      .exec()
    const byStalwartId = new Map<string, string>()
    const byName = new Map<string, string>()
    for (const d of domains) {
      if (d.stalwartId) {
        byStalwartId.set(d.stalwartId, d.domain)
        byName.set(d.domain, d.stalwartId)
      }
    }
    return { byStalwartId, byName }
  }

  // ── Aliases ────────────────────────────────────────────────────────────────

  async listAliases(tenant: TenantRef): Promise<AliasView[]> {
    if (!this.stalwart.configured) return []
    const { byStalwartId } = await this.tenantDomains(tenant._id)
    if (byStalwartId.size === 0) return []
    const accounts = await this.stalwart.listAccountsWithAliases()
    const out: AliasView[] = []
    for (const acc of accounts) {
      for (const a of acc.aliases) {
        const domain = byStalwartId.get(a.domainId)
        if (!domain) continue // alias on a domain this tenant doesn't own
        out.push({
          address: `${a.name}@${domain}`,
          localPart: a.name,
          domain,
          destination: acc.emailAddress,
          accountId: acc.id,
          enabled: a.enabled,
        })
      }
    }
    return out.sort((x, y) => x.address.localeCompare(y.address))
  }

  async createAlias(
    tenant: TenantRef,
    dto: { localPart: string; domain: string; destinationUserId: string },
    actor: AuditActor,
  ): Promise<AliasView> {
    const { byName } = await this.tenantDomains(tenant._id)
    const domain = dto.domain.trim().toLowerCase()
    const domainStalwartId = byName.get(domain)
    if (!domainStalwartId) {
      throw new BadRequestException(`Domain "${domain}" isn't part of this workspace.`)
    }
    const localPart = dto.localPart.trim().toLowerCase()
    const dest = await this.userModel
      .findOne({ _id: dto.destinationUserId, tenantIds: tenant._id })
      .exec()
    if (!dest?.stalwartAccountId) {
      throw new BadRequestException('The destination must be a mailbox user in this workspace.')
    }
    await this.stalwart.addAlias(dest.stalwartAccountId, localPart, domainStalwartId)
    const address = `${localPart}@${domain}`
    await this.audit.record(
      {
        action: 'mail.alias_created',
        resourceType: 'system',
        resourceId: address,
        resourceName: address,
        tenantSlug: tenant.slug,
        metadata: { destination: dest.email },
      },
      actor,
    )
    return { address, localPart, domain, destination: dest.email, accountId: dest.stalwartAccountId, enabled: true }
  }

  async deleteAlias(tenant: TenantRef, address: string, actor: AuditActor): Promise<void> {
    const target = address.trim().toLowerCase()
    const match = (await this.listAliases(tenant)).find((a) => a.address === target)
    if (!match) throw new NotFoundException(`Alias "${address}" not found.`)
    const { byName } = await this.tenantDomains(tenant._id)
    await this.stalwart.removeAlias(match.accountId, match.localPart, byName.get(match.domain)!)
    await this.audit.record(
      {
        action: 'mail.alias_deleted',
        resourceType: 'system',
        resourceId: target,
        resourceName: target,
        tenantSlug: tenant.slug,
      },
      actor,
    )
  }

  // ── Distribution lists ───────────────────────────────────────────────────────

  async listLists(tenant: TenantRef): Promise<ListView[]> {
    if (!this.stalwart.configured) return []
    const { byStalwartId } = await this.tenantDomains(tenant._id)
    if (byStalwartId.size === 0) return []
    const lists = await this.stalwart.listMailingLists()
    return lists
      .filter((l) => byStalwartId.has(l.domainId))
      .map((l) => ({
        id: l.id,
        name: l.name,
        address: l.emailAddress || `${l.name}@${byStalwartId.get(l.domainId)}`,
        domain: byStalwartId.get(l.domainId)!,
        recipients: l.recipients,
        members: l.recipients.length,
        description: l.description,
      }))
      .sort((x, y) => x.address.localeCompare(y.address))
  }

  async createList(
    tenant: TenantRef,
    dto: { localPart: string; domain: string; recipients?: string[]; description?: string },
    actor: AuditActor,
  ): Promise<ListView> {
    const { byName } = await this.tenantDomains(tenant._id)
    const domain = dto.domain.trim().toLowerCase()
    const domainStalwartId = byName.get(domain)
    if (!domainStalwartId) {
      throw new BadRequestException(`Domain "${domain}" isn't part of this workspace.`)
    }
    const localPart = dto.localPart.trim().toLowerCase()
    const recipients = cleanRecipients(dto.recipients)
    const { id } = await this.stalwart.createMailingList({
      domainId: domainStalwartId,
      name: localPart,
      recipients,
      description: dto.description,
    })
    const address = `${localPart}@${domain}`
    await this.audit.record(
      {
        action: 'mail.list_created',
        resourceType: 'system',
        resourceId: address,
        resourceName: address,
        tenantSlug: tenant.slug,
        metadata: { members: recipients.length },
      },
      actor,
    )
    return { id, name: localPart, address, domain, recipients, members: recipients.length, description: dto.description }
  }

  async updateListRecipients(
    tenant: TenantRef,
    id: string,
    recipients: string[],
    actor: AuditActor,
  ): Promise<ListView> {
    const list = await this.assertListInTenant(tenant, id)
    const clean = cleanRecipients(recipients)
    await this.stalwart.updateMailingListRecipients(id, clean)
    await this.audit.record(
      {
        action: 'mail.list_updated',
        resourceType: 'system',
        resourceId: list.address,
        resourceName: list.address,
        tenantSlug: tenant.slug,
        metadata: { members: clean.length },
      },
      actor,
    )
    return { ...list, recipients: clean, members: clean.length }
  }

  async deleteList(tenant: TenantRef, id: string, actor: AuditActor): Promise<void> {
    const list = await this.assertListInTenant(tenant, id)
    await this.stalwart.deleteMailingList(id)
    await this.audit.record(
      {
        action: 'mail.list_deleted',
        resourceType: 'system',
        resourceId: list.address,
        resourceName: list.address,
        tenantSlug: tenant.slug,
      },
      actor,
    )
  }

  private async assertListInTenant(tenant: TenantRef, id: string): Promise<ListView> {
    const list = (await this.listLists(tenant)).find((l) => l.id === id)
    if (!list) throw new NotFoundException('Distribution list not found in this workspace.')
    return list
  }
}

function cleanRecipients(recipients?: string[]): string[] {
  return [...new Set((recipients ?? []).map((r) => r.trim().toLowerCase()).filter(Boolean))]
}
