import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { MeetingAttachmentCategory, Prisma } from '@prisma/client';
import type { Request } from 'express';
import { LocalDiskStorage } from './storage/local-disk.storage';
import { AttachmentValidator } from './attachment-validator.service';
import { MeetingAttendanceAuditLogWriter } from './audit-log-writer.service';
import {
  MEETING_ATTENDANCE_AUDIT_ACTIONS,
  MEETING_ATTENDANCE_AUDIT_RESOURCES,
} from '../constants/audit';
import {
  attachmentDeleteForbidden,
  attachmentNotFound,
} from '../errors/agenda.error';
import { MeetingAttendanceError } from '../errors/meeting-attendance.error';
import { QueryMeetingAttachmentsDto } from '../dto/agenda.dto';

interface ActorContext {
  userId: string;
  organizationId?: string | null;
  permissions: string[];
}

interface MulterFileLike {
  path: string;
  originalname: string;
  mimetype: string;
  size: number;
}

/**
 * 会议级附件 v1.0：上传 / 列表 / 软删。
 *
 * 仅 `meeting:attachment:upload:any` 可上传（无 task 命中路径）。
 * 删除规则同议程项附件（uploader 本人或 manager）。
 */
@Injectable()
export class MeetingAttachmentService {
  private readonly logger = new Logger(MeetingAttachmentService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly storage: LocalDiskStorage,
    private readonly validator: AttachmentValidator,
    private readonly auditLogWriter: MeetingAttendanceAuditLogWriter,
  ) {}

  /** [117] 上传会议级附件。 */
  async upload(
    meetingId: string,
    file: MulterFileLike,
    category: MeetingAttachmentCategory | undefined,
    actor: ActorContext,
    req?: Request,
  ) {
    const cleanupTmp = async () => {
      try {
        await this.storage.safeUnlink(file.path);
      } catch {
        // ignore
      }
    };

    try {
      const meeting = await this.prisma.meeting.findUnique({
        where: { id: meetingId },
        select: { id: true },
      });
      if (!meeting) {
        throw new MeetingAttendanceError(404, 'Meeting not found', 'MEETING_ATTENDANCE_003');
      }

      this.validator.validateMimeHeader(file.mimetype);
      this.validator.validateSize(file.size);
      await this.validator.validateMagicBytes(file.path, file.mimetype);

      const storagePath = this.storage.buildStoragePath(file.mimetype);
      await this.storage.commitUpload(file.path, storagePath);

      const attachment = await this.prisma.meetingAttachment.create({
        data: {
          meetingId,
          uploadedById: actor.userId,
          filename: file.originalname,
          mimeType: file.mimetype,
          size: BigInt(file.size),
          storagePath,
          category: category ?? null,
          createdById: actor.userId,
          organizationId: actor.organizationId ?? null,
        },
      });

      await this.auditLogWriter.log({
        request: req,
        actor: { userId: actor.userId },
        action: MEETING_ATTENDANCE_AUDIT_ACTIONS.ATTACHMENT_UPLOADED,
        resource: MEETING_ATTENDANCE_AUDIT_RESOURCES.ATTACHMENT,
        statusCode: 201,
        resourceId: attachment.id,
        changes: {
          entityId: attachment.id,
          entityType: 'MEETING_ATTACHMENT',
          after: {
            id: attachment.id,
            filename: attachment.filename,
            category: attachment.category,
            size: attachment.size.toString(),
          },
        },
      });

      return { attachment };
    } catch (err) {
      await cleanupTmp();
      throw err;
    }
  }

  /** [118] 会议级附件列表（uploadedAt 倒序，可选按 category 过滤）。 */
  async listByMeeting(meetingId: string, query: QueryMeetingAttachmentsDto) {
    const meeting = await this.prisma.meeting.findUnique({
      where: { id: meetingId },
      select: { id: true },
    });
    if (!meeting) {
      throw new MeetingAttendanceError(404, 'Meeting not found', 'MEETING_ATTENDANCE_003');
    }

    const where: Prisma.MeetingAttachmentWhereInput = {
      meetingId,
      deletedAt: null,
    };
    if (query.category) where.category = query.category;

    const items = await this.prisma.meetingAttachment.findMany({
      where,
      orderBy: { uploadedAt: 'desc' },
      include: {
        uploadedBy: { select: { id: true, displayName: true, email: true } },
      },
    });
    return { items };
  }

  /** [119] 软删会议级附件。 */
  async deleteAttachment(
    meetingId: string,
    attachmentId: string,
    actor: ActorContext & { isAdminOrManager: boolean },
    req?: Request,
  ): Promise<void> {
    const existing = await this.prisma.meetingAttachment.findFirst({
      where: { id: attachmentId, meetingId, deletedAt: null },
    });
    if (!existing) throw attachmentNotFound();

    const isOwner = existing.uploadedById === actor.userId;
    if (!isOwner && !actor.isAdminOrManager) {
      throw attachmentDeleteForbidden();
    }

    await this.prisma.meetingAttachment.update({
      where: { id: attachmentId },
      data: { deletedAt: new Date() },
    });

    await this.auditLogWriter.log({
      request: req,
      actor: { userId: actor.userId },
      action: MEETING_ATTENDANCE_AUDIT_ACTIONS.ATTACHMENT_DELETED,
      resource: MEETING_ATTENDANCE_AUDIT_RESOURCES.ATTACHMENT,
      statusCode: 204,
      resourceId: attachmentId,
      changes: {
        entityId: attachmentId,
        entityType: 'MEETING_ATTACHMENT',
        before: { id: existing.id, filename: existing.filename, category: existing.category },
      },
    });
  }

  async findForDownload(attachmentId: string) {
    const attachment = await this.prisma.meetingAttachment.findFirst({
      where: { id: attachmentId, deletedAt: null },
    });
    if (!attachment) throw attachmentNotFound();
    return { attachment, meetingId: attachment.meetingId };
  }
}
