import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { UploadTaskStatus } 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 {
  agendaItemNotFound,
  attachmentDeleteForbidden,
  attachmentNotFound,
  uploadTaskNotOwned,
} from '../errors/agenda.error';

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` → 直接放行
 * - `meeting:attachment:upload:assigned` + 当前用户在该 item 下有 PENDING task → auto-flip
 *
 * auto-flip：service 在同事务内
 *   `UPDATE ... WHERE id = task.id AND status='PENDING'`，affectedRows = 1 时刷 completedAt；
 *   = 0 时退一格选下一条 PENDING 重试；都查不到则 attachment 挂 item 不 flip。
 */
@Injectable()
export class AgendaItemAttachmentService {
  private readonly logger = new Logger(AgendaItemAttachmentService.name);

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

  /** [114] 上传议程项级附件。 */
  async upload(
    itemId: string,
    file: MulterFileLike,
    actor: ActorContext,
    req?: Request,
  ) {
    // 任何失败路径都要确保 tmp 被清掉
    const cleanupTmp = async () => {
      try {
        await this.storage.safeUnlink(file.path);
      } catch {
        // ignore
      }
    };

    try {
      // 1) 议程项存在性
      const item = await this.prisma.meetingAgendaItem.findFirst({
        where: { id: itemId, deletedAt: null },
        select: { id: true },
      });
      if (!item) throw agendaItemNotFound();

      // 2) MIME / 大小校验
      this.validator.validateMimeHeader(file.mimetype);
      this.validator.validateSize(file.size);
      await this.validator.validateMagicBytes(file.path, file.mimetype);

      // 3) 鉴权 + auto-flip task
      const hasAny = actor.permissions.includes('meeting:attachment:upload:any');
      const hasAssigned = actor.permissions.includes('meeting:attachment:upload:assigned');

      let candidateTaskId: string | null = null;
      if (!hasAny) {
        if (!hasAssigned) {
          throw uploadTaskNotOwned();
        }
        // 找最早的 PENDING task
        const candidate = await this.prisma.meetingAgendaItemUploadTask.findFirst({
          where: {
            agendaItemId: itemId,
            assigneeUserId: actor.userId,
            status: UploadTaskStatus.PENDING,
            deletedAt: null,
          },
          orderBy: { assignedAt: 'asc' },
          select: { id: true },
        });
        if (!candidate) throw uploadTaskNotOwned();
        candidateTaskId = candidate.id;
      }

      // 4) 落盘 + 事务
      const storagePath = this.storage.buildStoragePath(file.mimetype);
      await this.storage.commitUpload(file.path, storagePath);

      const result = await this.prisma.$transaction(async (tx) => {
        const attachment = await tx.meetingAgendaItemAttachment.create({
          data: {
            agendaItemId: itemId,
            uploadedById: actor.userId,
            filename: file.originalname,
            mimeType: file.mimetype,
            size: BigInt(file.size),
            storagePath,
            createdById: actor.userId,
            organizationId: actor.organizationId ?? null,
          },
        });

        let taskUpdated: any | undefined;
        if (candidateTaskId) {
          // auto-flip：检查 affectedRows；如果被并发抢先 → 退一格
          let flipped = await tx.meetingAgendaItemUploadTask.updateMany({
            where: { id: candidateTaskId, status: UploadTaskStatus.PENDING },
            data: { status: UploadTaskStatus.UPLOADED, completedAt: new Date() },
          });
          if (flipped.count === 0) {
            const next = await tx.meetingAgendaItemUploadTask.findFirst({
              where: {
                agendaItemId: itemId,
                assigneeUserId: actor.userId,
                status: UploadTaskStatus.PENDING,
                deletedAt: null,
              },
              orderBy: { assignedAt: 'asc' },
              select: { id: true },
            });
            if (next) {
              flipped = await tx.meetingAgendaItemUploadTask.updateMany({
                where: { id: next.id, status: UploadTaskStatus.PENDING },
                data: { status: UploadTaskStatus.UPLOADED, completedAt: new Date() },
              });
              if (flipped.count > 0) {
                taskUpdated = await tx.meetingAgendaItemUploadTask.findUnique({
                  where: { id: next.id },
                });
              }
            }
          } else {
            taskUpdated = await tx.meetingAgendaItemUploadTask.findUnique({
              where: { id: candidateTaskId },
            });
          }
        }

        return { attachment, taskUpdated };
      });

      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: result.attachment.id,
        changes: {
          entityId: result.attachment.id,
          entityType: 'AGENDA_ITEM_ATTACHMENT',
          after: {
            id: result.attachment.id,
            filename: result.attachment.filename,
            size: result.attachment.size.toString(),
            taskUpdatedId: result.taskUpdated?.id,
          },
        },
      });

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

  /** [115] 拉议程项下附件元数据列表。 */
  async listByItem(itemId: string) {
    const item = await this.prisma.meetingAgendaItem.findFirst({
      where: { id: itemId, deletedAt: null },
      select: { id: true },
    });
    if (!item) throw agendaItemNotFound();

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

  /** [116] 软删议程项附件（uploader 本人或 manager）。 */
  async deleteAttachment(
    itemId: string,
    attachmentId: string,
    actor: ActorContext & { isAdminOrManager: boolean },
    req?: Request,
  ): Promise<void> {
    const existing = await this.prisma.meetingAgendaItemAttachment.findFirst({
      where: { id: attachmentId, agendaItemId: itemId, deletedAt: null },
    });
    if (!existing) throw attachmentNotFound();

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

    await this.prisma.meetingAgendaItemAttachment.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: 'AGENDA_ITEM_ATTACHMENT',
        before: { id: existing.id, filename: existing.filename },
      },
    });
  }

  /** download controller 用：根据 id 查附件（含 meetingId 反查）。 */
  async findForDownload(attachmentId: string) {
    const attachment = await this.prisma.meetingAgendaItemAttachment.findFirst({
      where: { id: attachmentId, deletedAt: null },
      include: { agendaItem: { include: { section: { select: { meetingId: true } } } } },
    });
    if (!attachment) throw attachmentNotFound();
    return {
      attachment,
      meetingId: attachment.agendaItem.section.meetingId,
    };
  }
}
