import { Injectable } from '@nestjs/common';
import type { Response } from 'express';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { LocalDiskStorage } from './storage/local-disk.storage';
import { attachmentNotFound } from '../errors/agenda.error';
import { MeetingAttendanceError } from '../errors/meeting-attendance.error';

/**
 * 附件下载服务：流式回写 + RFC 5987 Content-Disposition。
 *
 * - 不一次性载入内存，直接 `createReadStream().pipe(res)`
 * - filename ASCII fallback + percent-encoded UTF-8（兼容老浏览器 + 正确显示中文/特殊字符）
 * - Content-Type = attachment.mimeType，Content-Length = attachment.size
 * - 参会身份校验在 controller 层完成，service 只负责拿数据 + 流
 */
@Injectable()
export class AttachmentDownloadService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly storage: LocalDiskStorage,
  ) {}

  /** [120] 议程项级附件下载。 */
  async downloadAgendaItemAttachment(
    attachmentId: string,
    res: Response,
    actorIsAttendeeChecker: (meetingId: string) => Promise<boolean>,
  ): Promise<void> {
    const attachment = await this.prisma.meetingAgendaItemAttachment.findFirst({
      where: { id: attachmentId, deletedAt: null },
      include: { agendaItem: { include: { section: { select: { meetingId: true } } } } },
    });
    if (!attachment) throw attachmentNotFound();

    const meetingId = attachment.agendaItem.section.meetingId;
    const ok = await actorIsAttendeeChecker(meetingId);
    if (!ok) {
      throw new MeetingAttendanceError(
        403,
        'Not in the meeting attendee list',
        'MEETING_ATTENDANCE_011',
      );
    }

    this.writeStream(res, attachment.filename, attachment.mimeType, attachment.size, attachment.storagePath);
  }

  /** [121] 会议级附件下载。 */
  async downloadMeetingAttachment(
    attachmentId: string,
    res: Response,
    actorIsAttendeeChecker: (meetingId: string) => Promise<boolean>,
  ): Promise<void> {
    const attachment = await this.prisma.meetingAttachment.findFirst({
      where: { id: attachmentId, deletedAt: null },
    });
    if (!attachment) throw attachmentNotFound();

    const ok = await actorIsAttendeeChecker(attachment.meetingId);
    if (!ok) {
      throw new MeetingAttendanceError(
        403,
        'Not in the meeting attendee list',
        'MEETING_ATTENDANCE_011',
      );
    }

    this.writeStream(res, attachment.filename, attachment.mimeType, attachment.size, attachment.storagePath);
  }

  private writeStream(
    res: Response,
    filename: string,
    mimeType: string,
    size: bigint,
    storagePath: string,
  ): void {
    const asciiFallback = filename.replace(/[^\x20-\x7E]/g, '_') || 'download';
    const encoded = encodeURIComponent(filename);
    res.setHeader('Content-Type', mimeType);
    res.setHeader('Content-Length', size.toString());
    res.setHeader(
      'Content-Disposition',
      `attachment; filename="${asciiFallback}"; filename*=UTF-8''${encoded}`,
    );

    const stream = this.storage.createReadStream(storagePath);
    stream.on('error', (err) => {
      if (!res.headersSent) {
        res.status(500).json({ error: 'Failed to read file', detail: err.message });
        return;
      }
      try {
        res.destroy(err);
      } catch {
        // ignore
      }
    });
    stream.pipe(res);
  }
}
