import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { LocalDiskStorage } from './storage/local-disk.storage';
import {
  getGcIntervalHours,
  getPhysicalDeleteDays,
  getStorageRoot,
  getTmpUploadDir,
} from '../constants/attachment';

/**
 * 议程附件 cron GC（详见 docs/modules/meeting-attendance/06-data-model.md §文件存储约定）。
 *
 * 三类清理任务：
 * 1) 物理删 30 天前软删的 DB 行 + unlink 对应文件
 * 2) 正向扫盘 → root 下不在 DB 集合 + mtime < now()-1h 的文件 → 物理删（孤儿清理）
 * 3) 反向扫 DB → 行存在但文件不存在 → log warn（不删 DB，避免误删）
 *
 * cron 周期由 env MEETING_ATTACHMENT_GC_INTERVAL_HOURS 控制，默认 1 小时。
 * 注：@nestjs/schedule 的 @Cron decorator 表达式在 class 定义时求值（无法用动态
 *     env 改），这里固定 hourly cron 表达式 `0 * * * *`，env 仅作未来配置占位。
 *
 * 注 2：物理删天数 MEETING_ATTACHMENT_PHYSICAL_DELETE_DAYS 可热改（每次 cron 重读 env）。
 */
@Injectable()
export class AttachmentGcService implements OnModuleInit {
  private readonly logger = new Logger(AttachmentGcService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly storage: LocalDiskStorage,
  ) {}

  async onModuleInit() {
    // 启动时确保目录存在
    try {
      await this.storage.ensureDirectories();
    } catch (err: any) {
      this.logger.warn(`ensureDirectories failed: ${err?.message ?? err}`);
    }
  }

  /** 每小时整点跑一次。 */
  @Cron('0 * * * *')
  async runHourly() {
    const intervalHours = getGcIntervalHours();
    // 兼容运维改了 env 但 cron 表达式只在 class 定义时确定的现实：
    // 当 env != 1 时按整除关系跳过非整点；默认 1 等价每小时跑。
    if (intervalHours > 1) {
      const hour = new Date().getUTCHours();
      if (hour % intervalHours !== 0) return;
    }

    await this.gcOnce();
  }

  /** 手动入口（测试 / 手工触发），不被 cron 周期约束。 */
  async gcOnce(): Promise<{
    purgedDbRows: number;
    purgedOrphanFiles: number;
    missingFileWarnings: number;
  }> {
    const days = getPhysicalDeleteDays();
    const purgedDbRows = await this.purgeSoftDeleted(days);
    const purgedOrphanFiles = await this.purgeOrphanFiles();
    const missingFileWarnings = await this.warnMissingFiles();
    this.logger.log(
      `GC done: purgedDbRows=${purgedDbRows} purgedOrphanFiles=${purgedOrphanFiles} missingFileWarnings=${missingFileWarnings}`,
    );
    return { purgedDbRows, purgedOrphanFiles, missingFileWarnings };
  }

  /** 1) 物理删 `deletedAt < now() - N days` 的 DB 行 + unlink。 */
  private async purgeSoftDeleted(days: number): Promise<number> {
    const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);

    let count = 0;

    const itemAttachments = await this.prisma.meetingAgendaItemAttachment.findMany({
      where: { deletedAt: { lt: cutoff } },
      select: { id: true, storagePath: true },
    });
    for (const row of itemAttachments) {
      try {
        await this.storage.safeUnlink(row.storagePath);
        await this.prisma.meetingAgendaItemAttachment.delete({ where: { id: row.id } });
        count++;
      } catch (err: any) {
        this.logger.warn(`purge agenda item attachment ${row.id} failed: ${err?.message ?? err}`);
      }
    }

    const meetingAttachments = await this.prisma.meetingAttachment.findMany({
      where: { deletedAt: { lt: cutoff } },
      select: { id: true, storagePath: true },
    });
    for (const row of meetingAttachments) {
      try {
        await this.storage.safeUnlink(row.storagePath);
        await this.prisma.meetingAttachment.delete({ where: { id: row.id } });
        count++;
      } catch (err: any) {
        this.logger.warn(`purge meeting attachment ${row.id} failed: ${err?.message ?? err}`);
      }
    }

    return count;
  }

  /** 2) 正向扫盘 → 不在 DB 集合 + mtime < now()-1h 的文件 → 物理删。 */
  private async purgeOrphanFiles(): Promise<number> {
    const root = getStorageRoot();
    const tmp = getTmpUploadDir();

    const all = await this.storage.listAllFiles(root);
    if (all.length === 0) return 0;

    const knownPaths = new Set<string>();
    const itemRows = await this.prisma.meetingAgendaItemAttachment.findMany({
      select: { storagePath: true },
    });
    const meetingRows = await this.prisma.meetingAttachment.findMany({
      select: { storagePath: true },
    });
    for (const r of itemRows) knownPaths.add(r.storagePath);
    for (const r of meetingRows) knownPaths.add(r.storagePath);

    const cutoff = Date.now() - 60 * 60 * 1000; // 1 小时
    let count = 0;
    for (const f of all) {
      // tmp 目录下的文件 mtime < now()-1h 即清（与正常文件统一规则）
      const rel = this.storage.toRelativePath(f.absPath);
      const isInTmp = f.absPath.startsWith(tmp);
      if (!isInTmp && knownPaths.has(rel)) continue;
      if (f.mtimeMs > cutoff) continue;
      try {
        await this.storage.safeUnlink(f.absPath);
        count++;
      } catch (err: any) {
        this.logger.warn(`unlink orphan ${f.absPath} failed: ${err?.message ?? err}`);
      }
    }
    return count;
  }

  /** 3) 反向扫 DB → 行存在但文件不存在 → log warn（不删 DB）。 */
  private async warnMissingFiles(): Promise<number> {
    let warned = 0;
    const itemRows = await this.prisma.meetingAgendaItemAttachment.findMany({
      where: { deletedAt: null },
      select: { id: true, storagePath: true },
    });
    for (const r of itemRows) {
      const ok = await this.storage.exists(r.storagePath);
      if (!ok) {
        this.logger.warn(`AgendaItemAttachment ${r.id} missing file at ${r.storagePath}`);
        warned++;
      }
    }
    const meetingRows = await this.prisma.meetingAttachment.findMany({
      where: { deletedAt: null },
      select: { id: true, storagePath: true },
    });
    for (const r of meetingRows) {
      const ok = await this.storage.exists(r.storagePath);
      if (!ok) {
        this.logger.warn(`MeetingAttachment ${r.id} missing file at ${r.storagePath}`);
        warned++;
      }
    }
    return warned;
  }
}
