import { Injectable, NotFoundException, BadRequestException, Logger } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'
import { Model, Types } from 'mongoose'
import * as nodemailer from 'nodemailer'
import { ConfigService } from '@nestjs/config'
import {
  Email, EmailDocument,
  UserEmailAccount, UserEmailAccountDocument,
  ClientFolder, ClientFolderDocument,
  Matter, MatterDocument,
  EmailServerSettings, EmailServerSettingsDocument,
} from '../common/schemas'
import { EmailAssociationService } from './email-association.service'
import { forwardRef, Inject } from '@nestjs/common'
import { ImapPollingService } from './imap-polling.service'
import { RegisterEmailAccountDto, ConfirmAssociationDto, SendEmailDto } from './dto/email.dto'

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

  constructor(
    @InjectModel(Email.name)            private emailModel:   Model<EmailDocument>,
    @InjectModel(UserEmailAccount.name) private accountModel: Model<UserEmailAccountDocument>,
    @InjectModel(ClientFolder.name)          private folderModel:    Model<ClientFolderDocument>,
    @InjectModel(Matter.name)               private matterModel:    Model<MatterDocument>,
    @InjectModel(EmailServerSettings.name)  private settingsModel:  Model<EmailServerSettingsDocument>,
    private readonly association:   EmailAssociationService,
    private readonly config:        ConfigService,
    @Inject(forwardRef(() => ImapPollingService))
    private readonly imapPolling:   ImapPollingService,
  ) {}

  // ── Inbound ingestion (called by SMTP listener) ──────────────────────────

  async ingestInbound(parsed: any, envelopeFrom: string): Promise<EmailDocument> {
    const messageId = parsed.messageId ?? `<generated-${Date.now()}@gps-legal>`

    // Deduplicate
    const existing = await this.emailModel.findOne({ messageId }).lean().exec()
    if (existing) return existing as unknown as EmailDocument

    // Compute threadId: follow In-Reply-To chain
    const threadId = await this.resolveThreadId(
      parsed.messageId,
      parsed.inReplyTo,
      parsed.references,
    )

    // Identify forwardedBy
    const forwardedBy = await this.resolveForwardedBy(envelopeFrom, parsed)

    const emailData: Partial<EmailDocument> = {
      messageId,
      threadId,
      inReplyTo:  parsed.inReplyTo,
      references: Array.isArray(parsed.references)
        ? parsed.references
        : parsed.references ? [parsed.references] : [],
      direction: 'inbound',
      from:      { address: parsed.from?.value?.[0]?.address ?? '', name: parsed.from?.value?.[0]?.name },
      to:        (parsed.to?.value ?? []).map((a: any) => ({ address: a.address, name: a.name })),
      cc:        (parsed.cc?.value ?? []).map((a: any) => ({ address: a.address, name: a.name })),
      bcc:       (parsed.bcc?.value ?? []).map((a: any) => ({ address: a.address, name: a.name })),
      subject:   parsed.subject ?? '(no subject)',
      bodyText:  parsed.text,
      bodyHtml:  parsed.html || undefined,
      attachments: (parsed.attachments ?? []).map((a: any) => ({
        filename:    a.filename ?? 'attachment',
        contentType: a.contentType ?? 'application/octet-stream',
        size:        a.size ?? a.content?.length ?? 0,
      })),
      receivedAt: parsed.date ?? new Date(),
      forwardedBy,
      isRead:     false,
      isArchived: false,
    } as any

    // Run association engine
    const assoc = await this.association.associate(emailData)
    Object.assign(emailData, assoc)

    const doc = new this.emailModel(emailData)
    const saved = await doc.save()

    this.logger.log(`Inbound email ingested: ${messageId} → status=${assoc.associationStatus}`)
    return saved
  }

  // ── Outbound sending ──────────────────────────────────────────────────────

  async sendEmail(dto: SendEmailDto): Promise<EmailDocument> {
    const folder = await this.folderModel.findById(dto.clientFolderId).lean().exec()
    if (!folder) throw new NotFoundException(`Client folder ${dto.clientFolderId} not found`)

    let matter: MatterDocument | null = null
    if (dto.matterId) {
      matter = await this.matterModel.findById(dto.matterId).lean().exec() as unknown as MatterDocument
    }

    // Build subject — always include matter reference for outbound
    // Format: "[ref: {matterNumber}] {subject}" so replies are thread-parseable
    const subjectRef = matter
      ? `[ref: ${matter.matterNumber}]`
      : `[ref: ${folder.folderNumber}]`
    const fullSubject = dto.subject.startsWith('[ref:')
      ? dto.subject
      : `${subjectRef} ${dto.subject}`

    const messageId = `<${Date.now()}.${Math.random().toString(36).slice(2)}@gps-legal>`
    const threadId = dto.inReplyTo
      ? await this.resolveThreadId(messageId, dto.inReplyTo, [])
      : messageId

    // Send via SMTP relay if configured, otherwise log only (dev mode)
    const smtpHost = this.config.get<string>('OUTBOUND_SMTP_HOST')
    if (smtpHost) {
      const transporter = nodemailer.createTransport({
        host: smtpHost,
        port: this.config.get<number>('OUTBOUND_SMTP_PORT', 587),
        secure: this.config.get<boolean>('OUTBOUND_SMTP_SECURE', false),
        auth: {
          user: this.config.get<string>('OUTBOUND_SMTP_USER'),
          pass: this.config.get<string>('OUTBOUND_SMTP_PASS'),
        },
      })
      await transporter.sendMail({
        messageId,
        from:     dto.from,
        to:       dto.to.map(t => t.name ? `"${t.name}" <${t.address}>` : t.address),
        cc:       dto.cc?.map(t => t.name ? `"${t.name}" <${t.address}>` : t.address),
        subject:  fullSubject,
        text:     dto.bodyText,
        html:     dto.bodyHtml,
        inReplyTo: dto.inReplyTo,
      })
    } else {
      this.logger.debug(`[DEV] Outbound email suppressed — no OUTBOUND_SMTP_HOST configured`)
      this.logger.debug(`  To: ${dto.to.map(t => t.address).join(', ')}`)
      this.logger.debug(`  Subject: ${fullSubject}`)
    }

    // Record in DB
    const doc = new this.emailModel({
      messageId,
      threadId,
      inReplyTo:    dto.inReplyTo,
      references:   dto.inReplyTo ? [dto.inReplyTo] : [],
      direction:    'outbound',
      from:         { address: dto.from },
      to:           dto.to,
      cc:           dto.cc ?? [],
      bcc:          [],
      subject:      fullSubject,
      bodyText:     dto.bodyText,
      bodyHtml:     dto.bodyHtml,
      attachments:  [],
      receivedAt:   new Date(),
      clientFolderId: dto.clientFolderId,
      matterId:       dto.matterId,
      associationStatus: 'confirmed',
      isRead:       true,
      isArchived:   false,
    })

    return doc.save()
  }

  // ── Queries ───────────────────────────────────────────────────────────────

  async findAll(filters: {
    clientFolderId?: string
    matterId?: string
    direction?: string
    associationStatus?: string
    unreadOnly?: boolean
    limit?: number
    offset?: number
  }): Promise<{ items: EmailDocument[]; total: number }> {
    const q: any = {}
    if (filters.clientFolderId) q.clientFolderId = filters.clientFolderId
    if (filters.matterId)       q.matterId       = filters.matterId
    if (filters.direction)      q.direction      = filters.direction
    if (filters.associationStatus) q.associationStatus = filters.associationStatus
    if (filters.unreadOnly)     q.isRead         = false

    const [items, total] = await Promise.all([
      this.emailModel.find(q)
        .sort({ receivedAt: -1 })
        .skip(filters.offset ?? 0)
        .limit(filters.limit ?? 50)
        .exec(),
      this.emailModel.countDocuments(q),
    ])
    return { items, total }
  }

  async findThread(threadId: string): Promise<EmailDocument[]> {
    return this.emailModel.find({ threadId }).sort({ receivedAt: 1 }).exec()
  }

  async findOne(id: string): Promise<EmailDocument> {
    const doc = await this.emailModel.findById(id).exec()
    if (!doc) throw new NotFoundException(`Email ${id} not found`)
    return doc
  }

  async markRead(id: string): Promise<EmailDocument> {
    const doc = await this.findOne(id)
    doc.isRead = true
    return doc.save()
  }

  async markArchived(id: string): Promise<EmailDocument> {
    const doc = await this.findOne(id)
    doc.isArchived = true
    return doc.save()
  }

  async confirmAssociation(id: string, dto: ConfirmAssociationDto): Promise<EmailDocument> {
    const doc = await this.findOne(id)
    doc.clientFolderId         = dto.clientFolderId
    doc.matterId               = dto.matterId
    doc.associationStatus      = 'confirmed'
    doc.associationConfirmedBy = dto.confirmedBy
    doc.associationConfirmedAt = new Date()

    // Propagate to entire thread if this was the first confirmed email
    await this.emailModel.updateMany(
      { threadId: doc.threadId, associationStatus: { $in: ['pending_review', 'unassociated'] } },
      {
        clientFolderId:    dto.clientFolderId,
        matterId:          dto.matterId,
        associationStatus: 'confirmed',
        associationConfirmedBy: dto.confirmedBy,
        associationConfirmedAt: new Date(),
      },
    ).exec()

    return doc.save()
  }

  // Pending review count — for dashboard badge
  async pendingCount(): Promise<number> {
    return this.emailModel.countDocuments({ associationStatus: 'pending_review' })
  }

  // ── User email accounts ───────────────────────────────────────────────────

  async registerAccount(dto: RegisterEmailAccountDto): Promise<UserEmailAccountDocument> {
    const existing = await this.accountModel.findOne({ emailAddress: dto.emailAddress.toLowerCase() })
    if (existing) throw new BadRequestException(`${dto.emailAddress} is already registered`)
    return new this.accountModel(dto).save()
  }

  async listAccounts(): Promise<UserEmailAccountDocument[]> {
    return this.accountModel.find({ isActive: true }).sort({ displayName: 1 }).exec()
  }

  async removeAccount(id: string): Promise<void> {
    await this.accountModel.findByIdAndUpdate(id, { isActive: false }).exec()
  }

  // ── Email server settings ─────────────────────────────────────────────────

  async getSettings(): Promise<EmailServerSettingsDocument> {
    const existing = await this.settingsModel.findOne({ key: 'email' }).exec()
    if (existing) return existing
    return new this.settingsModel({ key: 'email' }).save()
  }

  async testSettings(cfg: Partial<EmailServerSettings>): Promise<{
    imap:  { tested: boolean; success: boolean; message: string }
    smtp:  { tested: boolean; success: boolean; message: string }
  }> {
    const results = {
      imap: { tested: false, success: false, message: '' },
      smtp: { tested: false, success: false, message: '' },
    }

    // ── Test IMAP ────────────────────────────────────────────────────────────
    if (cfg.imapEnabled && cfg.imapHost && cfg.imapUser && cfg.imapPassword) {
      results.imap.tested = true
      const { ImapFlow } = await import('imapflow')
      const client = new ImapFlow({
        host:   cfg.imapHost,
        port:   cfg.imapPort ?? 993,
        secure: cfg.imapSsl ?? true,
        auth:   { user: cfg.imapUser, pass: cfg.imapPassword },
        logger: false,
      })
      try {
        await client.connect()
        const status = await client.status(cfg.imapFolder ?? 'INBOX', { messages: true, unseen: true })
        results.imap.success = true
        results.imap.message = `Connected — ${cfg.imapFolder ?? 'INBOX'}: ${status.messages} messages, ${status.unseen} unseen`
        await client.logout()
      } catch (err: any) {
        results.imap.success = false
        results.imap.message = err.message ?? 'Connection failed'
      }
    }

    // ── Test outbound SMTP ───────────────────────────────────────────────────
    if (cfg.outboundSmtpHost && cfg.outboundSmtpUser && cfg.outboundSmtpPassword) {
      results.smtp.tested = true
      const transporter = nodemailer.createTransport({
        host:   cfg.outboundSmtpHost,
        port:   cfg.outboundSmtpPort ?? 587,
        secure: cfg.outboundSmtpSsl ?? false,
        auth:   { user: cfg.outboundSmtpUser, pass: cfg.outboundSmtpPassword },
      })
      try {
        await transporter.verify()
        results.smtp.success = true
        results.smtp.message = `SMTP relay verified — ${cfg.outboundSmtpHost}:${cfg.outboundSmtpPort ?? 587}`
      } catch (err: any) {
        results.smtp.success = false
        results.smtp.message = err.message ?? 'SMTP verification failed'
      }
    }

    return results
  }

  async saveSettings(patch: Partial<EmailServerSettings>): Promise<EmailServerSettingsDocument> {
    const { key: _k, ...safe } = patch as any
    const saved = await this.settingsModel.findOneAndUpdate(
      { key: 'email' },
      { $set: safe },
      { upsert: true, new: true },
    ).exec() as EmailServerSettingsDocument
    // Restart IMAP polling loop with new settings
    await this.imapPolling.reload()
    return saved
  }

  // ── Private helpers ───────────────────────────────────────────────────────

  private async resolveThreadId(
    messageId: string,
    inReplyTo?: string,
    references?: string[],
  ): Promise<string> {
    // Check references chain to find the root of the thread
    const chain = [...(references ?? []), ...(inReplyTo ? [inReplyTo] : [])]
    if (!chain.length) return messageId

    const ancestor = await this.emailModel
      .findOne({ messageId: { $in: chain } })
      .select('threadId')
      .lean()
      .exec()

    return ancestor?.threadId ?? chain[0] ?? messageId
  }

  private async resolveForwardedBy(envelopeFrom: string, parsed: any): Promise<string | undefined> {
    // Check if the envelope sender is a registered account
    if (envelopeFrom) {
      const account = await this.accountModel
        .findOne({ emailAddress: envelopeFrom.toLowerCase(), isActive: true })
        .lean().exec()
      if (account) return account.emailAddress
    }

    // Fallback: check all To/From addresses against registered accounts
    const allAddrs = [
      ...(parsed.to?.value ?? []),
      ...(parsed.cc?.value ?? []),
    ].map((a: any) => a.address?.toLowerCase())

    for (const addr of allAddrs) {
      const account = await this.accountModel
        .findOne({ emailAddress: addr, isActive: true })
        .lean().exec()
      if (account) return account.emailAddress
    }

    return undefined
  }
}
