import { PrismaClient, Prisma } from '@prisma/client';
import { execFileSync } from 'node:child_process';
import path from 'node:path';

const prisma = new PrismaClient();

const args = process.argv.slice(2);
const getArgValue = (name: string) => {
  const index = args.indexOf(name);
  if (index === -1) return undefined;
  return args[index + 1];
};

const sqlitePath = getArgValue('--sqlite') || process.env.MEETING_ATTENDANCE_SQLITE_PATH || '/home/chentao/Downloads/dev.db';
const dryRun = args.includes('--dry-run');
const includeQr = args.includes('--include-qr');
const truncateAuditSessionId = args.includes('--truncate-audit-session-id');
const batchSize = Number(process.env.MEETING_ATTENDANCE_BATCH_SIZE || 500);
const auditLogBatchSize = Number(process.env.MEETING_ATTENDANCE_AUDIT_LOG_BATCH_SIZE || 50);
const sqliteMaxBuffer = Number(process.env.MEETING_ATTENDANCE_SQLITE_MAX_BUFFER || 20 * 1024 * 1024);
const auditSessionIdMaxLength = 500;
const legacyDomain = '@faradayfuture.com';
const mappedDomain = '@ff.com';

const normalizeEmail = (value?: string | null) => String(value || '').trim().toLowerCase();

const explicitEmailMap: Record<string, string> = {
  'admin@ff.com': 'itadmin@ff.com',
  'zheng.yan@faradayfuture.com': 'c-zheng.yan@ff.com',
  'chloe.zhang@faradayfuture.com': 'xiaorong.zhang@ff.com',
  'luetian.sun@faradayfuture.com': 'mark.sun@ff.com',
  'cn-chongming.zhong@faradayfuture.com': 'chongming.zhong@ff.com',
  'cheng.shisheng@faradayfuture.com': 'shisheng.cheng@ff.com',
  'tin.mok@faradayfuture.com': 'chuitin.mok@ff.com',
  'sreedhar.gajjala@faradayfuture.com': 'sreedharreddy.gajjala@ff.com',
  'terry.wang@faradayfuture.com': 'chao.wang@ff.com',
  'jim.gao@faradayfuture.com': 'jianming.gao@ff.com',
  'sherry.ding@faradayfuture.com': 'xin.ding@ff.com',
  'bob.ding@faradayfuture.com': 'xinmin.ding@ff.com',
  'tim.vo@faradayfuture.com': 'timothy.vo@ff.com',
  'yf.j@faradayfuture.com': 'yf.jia@ff.com',
  'louis.medina@faradayfuture.com': 'louie.medina@ff.com',
  'carol.kong@faradayfuture.com': 'carol.kong@ff.com',
  'alexander.lagover@faradayfuture.com': 'alexander.lagover@ff.com',
  'llya.kazhokin@faradayfuture.com': 'ilya.kazhokin@ff.com',
  'guanxiong.zhong@faradayfuture.com': 'jeff.zhong@ff.com',
  'benjamin.kuo@faradayfuture.com': 'benjamin.kuo@ff.com',
  'lingyi.xu@faradayfuture.com': 'lingyi.xu@ff.com',
  'emily.yang@faradayfuture.com': 'emily.yang@ff.com',
};

const stripCnPrefix = (email: string) => {
  const [local, domain] = email.split('@');
  if (!local || !domain) return email;
  if (!local.startsWith('cn-')) return email;
  return `${local.slice(3)}@${domain}`;
};

const getEmailCandidates = (value?: string | null) => {
  const raw = normalizeEmail(value);
  if (!raw) return [] as string[];
  const mapped = explicitEmailMap[raw];
  const candidates = new Set([raw, stripCnPrefix(raw)]);
  if (mapped) {
    candidates.add(normalizeEmail(mapped));
  }
  if (raw.endsWith(legacyDomain)) {
    const swapped = raw.replace(legacyDomain, mappedDomain);
    candidates.add(swapped);
    candidates.add(stripCnPrefix(swapped));
  }
  if (raw.endsWith(mappedDomain)) {
    const swapped = raw.replace(mappedDomain, legacyDomain);
    candidates.add(swapped);
    candidates.add(stripCnPrefix(swapped));
  }
  return Array.from(candidates);
};

const toBool = (value: unknown, fallback = false) => {
  if (value === null || value === undefined) return fallback;
  if (typeof value === 'boolean') return value;
  if (typeof value === 'number') return value === 1;
  if (typeof value === 'string') {
    return value === '1' || value.toLowerCase() === 'true';
  }
  return fallback;
};

const toDate = (value: unknown): Date | null => {
  if (value === null || value === undefined) return null;
  if (typeof value === 'number') return new Date(value);
  if (typeof value === 'string') {
    const numeric = Number(value);
    if (!Number.isNaN(numeric) && value.trim() !== '') {
      return new Date(numeric);
    }
    return new Date(value);
  }
  return null;
};

const runSqliteQuery = (sql: string) => {
  const output = execFileSync('sqlite3', ['-json', sqlitePath, sql], {
    encoding: 'utf8',
    maxBuffer: sqliteMaxBuffer,
  }).trim();
  if (!output) return [] as any[];
  return JSON.parse(output) as any[];
};

const getRowCount = (table: string) => {
  const result = runSqliteQuery(`SELECT COUNT(*) as count FROM ${table};`);
  return Number(result?.[0]?.count || 0);
};

const runSqliteQueryInBatches = (
  baseQuery: string,
  total: number,
  handler: (rows: any[]) => Promise<void>,
  size = batchSize,
) =>
  new Promise<void>(async (resolve, reject) => {
    try {
      for (let offset = 0; offset < total; offset += size) {
        const pagedQuery = `${baseQuery} LIMIT ${size} OFFSET ${offset}`;
        const rows = runSqliteQuery(pagedQuery);
        if (rows.length === 0) break;
        await handler(rows);
      }
      resolve();
    } catch (error) {
      reject(error);
    }
  });

const logSection = (title: string) => {
  console.log(`\n== ${title} ==`);
};

const ensureSqliteAvailable = () => {
  const resolved = path.resolve(sqlitePath);
  console.log(`SQLite source: ${resolved}`);
};

const upsertById = async <T extends { id: string }>(
  model: {
    upsert: (args: { where: { id: string }; create: T; update: T }) => Promise<unknown>;
  },
  record: T,
) => {
  if (dryRun) return;
  await model.upsert({ where: { id: record.id }, create: record, update: record });
};

async function main() {
  ensureSqliteAvailable();
  console.log(`Dry run: ${dryRun ? 'YES' : 'NO'}`);

  const platformUsers = await prisma.user.findMany({ select: { id: true, email: true } });
  const platformUserMap = new Map(platformUsers.map((user) => [normalizeEmail(user.email), user.id]));

  logSection('Users');
  const users = runSqliteQuery('SELECT * FROM users;');
  console.log(`Loaded ${users.length} users.`);

  const sqliteUsersById = new Map<string, { email: string; name?: string }>();
  const sqliteUserEmails = new Map<string, string>();
  for (const row of users) {
    const originalEmail = String(row.email || '').trim();
    const email = normalizeEmail(originalEmail);
    if (!email) continue;
    sqliteUsersById.set(row.id, { email, name: row.name ?? undefined });
    sqliteUserEmails.set(email, originalEmail);
  }

  const attendanceUserRows = runSqliteQuery('SELECT DISTINCT userId FROM attendances;');
  const attendanceUserIds = new Set(attendanceUserRows.map((row) => String(row.userId)));
  const attendanceEmails = new Set(
    Array.from(attendanceUserIds)
      .map((id) => sqliteUsersById.get(id)?.email)
      .filter((email): email is string => !!email),
  );
  const auditUserRows = runSqliteQuery('SELECT DISTINCT userEmail FROM audit_logs;');
  const auditEmails = new Set<string>();
  const emailOriginalMap = new Map(sqliteUserEmails);
  for (const row of auditUserRows) {
    const originalEmail = String(row.userEmail || '').trim();
    const email = normalizeEmail(originalEmail);
    if (!email) continue;
    auditEmails.add(email);
    if (!emailOriginalMap.has(email)) {
      emailOriginalMap.set(email, originalEmail);
    }
  }
  const businessUserIds = new Set<string>();
  const businessEmails = new Set<string>();
  const addBusinessUser = (userId?: string | null, email?: string | null) => {
    if (userId) {
      businessUserIds.add(String(userId));
      const sqliteEmail = sqliteUsersById.get(String(userId))?.email;
      if (sqliteEmail) businessEmails.add(sqliteEmail);
    }
    const normalizedEmail = normalizeEmail(email);
    if (normalizedEmail) businessEmails.add(normalizedEmail);
  };
  for (const userId of attendanceUserIds) {
    addBusinessUser(userId, null);
  }
  const requiredAttendeeRows = runSqliteQuery('SELECT DISTINCT userId, email FROM meeting_required_attendees;');
  for (const row of requiredAttendeeRows) {
    addBusinessUser(row.userId, row.email ?? null);
  }
  const templateAttendeeRows = runSqliteQuery('SELECT DISTINCT userId FROM meeting_template_attendees;');
  for (const row of templateAttendeeRows) {
    addBusinessUser(row.userId, null);
  }
  const leaveRecordRows = runSqliteQuery('SELECT DISTINCT userId FROM leave_records;');
  for (const row of leaveRecordRows) {
    addBusinessUser(row.userId, null);
  }
  const seriesCreatorRows = runSqliteQuery('SELECT DISTINCT creatorId FROM meeting_series;');
  for (const row of seriesCreatorRows) {
    addBusinessUser(row.creatorId, null);
  }
  const meetingCreatorRows = runSqliteQuery('SELECT DISTINCT creatorId FROM meetings;');
  for (const row of meetingCreatorRows) {
    addBusinessUser(row.creatorId, null);
  }
  const templateCreatorRows = runSqliteQuery('SELECT DISTINCT creatorId FROM meeting_templates;');
  for (const row of templateCreatorRows) {
    addBusinessUser(row.creatorId, null);
  }
  const auditUserIdRows = runSqliteQuery('SELECT DISTINCT userId, userEmail FROM audit_logs;');
  for (const row of auditUserIdRows) {
    addBusinessUser(row.userId ?? null, row.userEmail ?? null);
  }
  const dataAccessUserRows = runSqliteQuery('SELECT DISTINCT actorId, actorEmail FROM data_access_logs;');
  for (const row of dataAccessUserRows) {
    addBusinessUser(row.actorId ?? null, row.actorEmail ?? null);
  }

  const shouldSkipLegacyUser = (_userId?: string | null, _fallbackEmail?: string | null) => false;

  const isAttendedUser = (userId?: string | null, email?: string | null) => {
    const normalized = normalizeEmail(email);
    return (userId ? attendanceUserIds.has(String(userId)) : false) || (normalized ? attendanceEmails.has(normalized) : false);
  };

  const resolvePlatformUserId = (userId?: string | null, fallbackEmail?: string | null) => {
    const rawEmail = normalizeEmail(fallbackEmail) || sqliteUsersById.get(String(userId || ''))?.email || '';
    if (!rawEmail) return null;
    const candidates = getEmailCandidates(rawEmail);
    for (const candidate of candidates) {
      const found = platformUserMap.get(candidate);
      if (found) return found;
    }
    return null;
  };

  const targetEmails = new Set<string>([...businessEmails, ...auditEmails]);
  const missingUsers = Array.from(targetEmails)
    .map((email) => {
      if (shouldSkipLegacyUser(null, email)) {
        return null;
      }
      const candidates = getEmailCandidates(email);
      const matched = candidates.find((candidate) => platformUserMap.has(candidate));
      if (matched) return null;
      const displayCandidates = candidates.filter((candidate) => candidate !== email);
      return { email, original: emailOriginalMap.get(email) ?? email, candidates: displayCandidates };
    })
    .filter((entry): entry is { email: string; original: string; candidates: string[] } => !!entry);
  if (missingUsers.length > 0) {
    console.error(`Missing platform users: ${missingUsers.length}`);
    console.error(
      missingUsers
        .map((entry) =>
          `${entry.original} -> ${
            entry.candidates.length > 0 ? entry.candidates.join(' | ') : '(no alternate domain candidate)'
          }`,
        )
        .join('\n'),
    );
    throw new Error('Platform users missing. Sync from Azure Entra ID and retry.');
  }

  logSection('Meeting Attendance Roles');
  const meetingRoleCodes = ['Administrator', 'MeetingManager', 'Leader', 'Employee'];
  const meetingRoles = await prisma.role.findMany({
    where: { code: { in: meetingRoleCodes } },
    select: { id: true, code: true },
  });
  const roleIdByCode = new Map(meetingRoles.map((role) => [role.code, role.id]));
  const employeeRoleId = roleIdByCode.get('Employee');
  if (!employeeRoleId) {
    throw new Error('Employee role not found. Run role seeds before migration.');
  }

  const meetingPlatformUserIds = new Set<string>();
  for (const email of businessEmails) {
    const candidates = getEmailCandidates(email);
    const matched = candidates.find((candidate) => platformUserMap.has(candidate));
    if (matched) {
      meetingPlatformUserIds.add(platformUserMap.get(matched)!);
    }
  }
  for (const userId of businessUserIds) {
    const resolved = resolvePlatformUserId(userId, null);
    if (resolved) {
      meetingPlatformUserIds.add(resolved);
    }
  }

  if (meetingPlatformUserIds.size > 0) {
    const attendeeIds = Array.from(meetingPlatformUserIds);
    const attendeeRoles = await prisma.userRole.findMany({
      where: {
        userId: { in: attendeeIds },
        role: { code: { in: meetingRoleCodes } },
      },
      select: { userId: true, role: { select: { code: true } } },
    });

    const userRoleMap = new Map<string, Set<string>>();
    for (const entry of attendeeRoles) {
      const set = userRoleMap.get(entry.userId) ?? new Set<string>();
      set.add(entry.role.code);
      userRoleMap.set(entry.userId, set);
    }

    let assignedCount = 0;
    for (const userId of attendeeIds) {
      const roles = userRoleMap.get(userId);
      if (roles && roles.size > 0) {
        continue;
      }
      if (!dryRun) {
        await prisma.userRole.create({
          data: {
            userId,
            roleId: employeeRoleId,
            organizationId: null,
          },
        });
      }
      assignedCount += 1;
    }

    console.log(`Assigned Employee role to ${assignedCount} meeting-attendance user(s) without meeting roles.`);
  } else {
    console.log('No meeting-attendance users found for role assignment.');
  }

  const requirePlatformUserId = (userId?: string | null, fallbackEmail?: string | null, context?: string) => {
    const resolved = resolvePlatformUserId(userId, fallbackEmail);
    if (!resolved) {
      const sqliteEmail = fallbackEmail || sqliteUsersById.get(String(userId || ''))?.email || 'unknown';
      throw new Error(`Missing platform user for ${context || 'record'}: ${String(userId || '')} (${sqliteEmail})`);
    }
    return resolved;
  };

  logSection('Meeting Series');
  const series = runSqliteQuery('SELECT * FROM meeting_series;');
  console.log(`Loaded ${series.length} series.`);
  for (const row of series) {
    const record = {
      id: row.id,
      title: row.title,
      description: row.description ?? null,
      pattern: row.pattern,
      frequency: row.frequency,
      startDate: toDate(row.startDate) ?? new Date(),
      endDate: toDate(row.endDate),
      maxOccurrences: row.maxOccurrences ?? null,
      timezone: row.timezone ?? 'UTC',
      location: row.location ?? null,
      type: row.type ?? 'OFFLINE',
      creatorId: requirePlatformUserId(row.creatorId, null, 'meeting_series.creator_id'),
      isActive: toBool(row.isActive, true),
      createdAt: toDate(row.createdAt) ?? new Date(),
      updatedAt: toDate(row.updatedAt) ?? new Date(),
    } as Prisma.MeetingSeriesUncheckedCreateInput;

    await upsertById(prisma.meetingSeries, record as any);
  }

  logSection('Meetings');
  const meetingQuery = includeQr
    ? 'SELECT * FROM meetings;'
    : [
        'SELECT',
        'id, title, description, startTime, endTime, timezone, location, type, status,',
        'creatorId, seriesId, instanceNumber, isSeriesMaster, hasCustomAttendees, createdAt, updatedAt',
        'FROM meetings;',
      ].join(' ');
  const meetings = runSqliteQuery(meetingQuery);
  console.log(`Loaded ${meetings.length} meetings.`);
  for (const row of meetings) {
    const record = {
      id: row.id,
      title: row.title,
      description: row.description ?? null,
      startTime: toDate(row.startTime) ?? new Date(),
      endTime: toDate(row.endTime) ?? new Date(),
      timezone: row.timezone ?? 'UTC',
      location: row.location ?? null,
      type: row.type ?? 'OFFLINE',
      status: row.status ?? 'SCHEDULED',
      qrCodeOnline: includeQr ? (row.qrCodeOnline ?? null) : null,
      qrCodeOffline: includeQr ? (row.qrCodeOffline ?? null) : null,
      creatorId: requirePlatformUserId(row.creatorId, null, 'meetings.creator_id'),
      seriesId: row.seriesId ?? null,
      instanceNumber: row.instanceNumber ?? null,
      isSeriesMaster: toBool(row.isSeriesMaster, false),
      hasCustomAttendees: toBool(row.hasCustomAttendees, false),
      createdAt: toDate(row.createdAt) ?? new Date(),
      updatedAt: toDate(row.updatedAt) ?? new Date(),
    } as Prisma.MeetingUncheckedCreateInput;

    await upsertById(prisma.meeting, record as any);
  }

  logSection('Meeting Required Attendees');
  const requiredAttendeeCount = getRowCount('meeting_required_attendees');
  console.log(`Loaded ${requiredAttendeeCount} required attendees.`);
  await runSqliteQueryInBatches(
    'SELECT * FROM meeting_required_attendees ORDER BY id',
    requiredAttendeeCount,
    async (rows) => {
      for (const row of rows) {
        if (shouldSkipLegacyUser(row.userId, row.email ?? null)) {
          continue;
        }
        const record = {
          id: row.id,
          meetingId: row.meetingId,
          userId: requirePlatformUserId(row.userId, row.email ?? null, 'meeting_required_attendees.user_id'),
          role: row.role,
          createdAt: toDate(row.createdAt) ?? new Date(),
        } as Prisma.MeetingRequiredAttendeeUncheckedCreateInput;

        if (dryRun) continue;
        await prisma.meetingRequiredAttendee.upsert({
          where: {
            meetingId_userId: {
              meetingId: record.meetingId,
              userId: record.userId,
            },
          },
          create: record,
          update: {
            role: record.role,
            createdAt: record.createdAt,
          },
        });
      }
    },
  );

  logSection('Attendances');
  const attendanceCount = getRowCount('attendances');
  console.log(`Loaded ${attendanceCount} attendances.`);
  await runSqliteQueryInBatches('SELECT * FROM attendances ORDER BY id', attendanceCount, async (rows) => {
    for (const row of rows) {
      const record = {
        id: row.id,
        userId: requirePlatformUserId(row.userId, null, 'attendances.user_id'),
        meetingId: row.meetingId,
        status: row.status,
        checkinTime: toDate(row.checkinTime),
        checkoutTime: toDate(row.checkoutTime),
        isLate: toBool(row.isLate, false),
        isEarlyLeave: toBool(row.isEarlyLeave, false),
        notes: row.notes ?? null,
        checkinType: row.checkinType ?? null,
        deviceId: row.deviceId ?? null,
        createdAt: toDate(row.createdAt) ?? new Date(),
        updatedAt: toDate(row.updatedAt) ?? new Date(),
      } as Prisma.MeetingAttendanceUncheckedCreateInput;

      if (dryRun) continue;
      await prisma.meetingAttendance.upsert({
        where: {
          userId_meetingId: {
            userId: record.userId,
            meetingId: record.meetingId,
          },
        },
        create: record,
        update: {
          status: record.status,
          checkinTime: record.checkinTime,
          checkoutTime: record.checkoutTime,
          isLate: record.isLate,
          isEarlyLeave: record.isEarlyLeave,
          notes: record.notes,
          checkinType: record.checkinType,
          deviceId: record.deviceId,
          createdAt: record.createdAt,
          updatedAt: record.updatedAt,
        },
      });
    }
  });

  logSection('Meeting Templates');
  const templates = runSqliteQuery('SELECT * FROM meeting_templates;');
  console.log(`Loaded ${templates.length} templates.`);
  for (const row of templates) {
    const record = {
      id: row.id,
      name: row.name,
      title: row.title,
      description: row.description ?? null,
      duration: row.duration,
      timezone: row.timezone ?? 'UTC',
      location: row.location ?? null,
      type: row.type ?? 'OFFLINE',
      creatorId: requirePlatformUserId(row.creatorId, null, 'meeting_templates.creator_id'),
      isPublic: toBool(row.isPublic, false),
      createdAt: toDate(row.createdAt) ?? new Date(),
      updatedAt: toDate(row.updatedAt) ?? new Date(),
    } as Prisma.MeetingTemplateUncheckedCreateInput;

    await upsertById(prisma.meetingTemplate, record as any);
  }

  logSection('Meeting Template Attendees');
  const templateAttendees = runSqliteQuery('SELECT * FROM meeting_template_attendees;');
  console.log(`Loaded ${templateAttendees.length} template attendees.`);
  for (const row of templateAttendees) {
    const record = {
      id: row.id,
      templateId: row.templateId,
      userId: requirePlatformUserId(row.userId, null, 'meeting_template_attendees.user_id'),
      createdAt: toDate(row.createdAt) ?? new Date(),
    } as Prisma.MeetingTemplateAttendeeUncheckedCreateInput;

    if (shouldSkipLegacyUser(row.userId, null)) {
      continue;
    }
    await upsertById(prisma.meetingTemplateAttendee, record as any);
  }

  logSection('Leave Records');
  const leaveRecords = runSqliteQuery('SELECT * FROM leave_records;');
  console.log(`Loaded ${leaveRecords.length} leave records.`);
  for (const row of leaveRecords) {
    const record = {
      id: row.id,
      userId: requirePlatformUserId(row.userId, null, 'leave_records.user_id'),
      type: row.type,
      startDate: toDate(row.startDate) ?? new Date(),
      endDate: toDate(row.endDate) ?? new Date(),
      reason: row.reason ?? null,
      status: row.status ?? 'PENDING',
      createdAt: toDate(row.createdAt) ?? new Date(),
      updatedAt: toDate(row.updatedAt) ?? new Date(),
    } as Prisma.MeetingAttendanceLeaveRecordUncheckedCreateInput;

    if (shouldSkipLegacyUser(row.userId, null)) {
      continue;
    }
    await upsertById(prisma.meetingAttendanceLeaveRecord, record as any);
  }

  logSection('Audit Logs');
  const auditLogCount = getRowCount('audit_logs');
  console.log(`Loaded ${auditLogCount} audit logs.`);
  await runSqliteQueryInBatches(
    'SELECT * FROM audit_logs ORDER BY id',
    auditLogCount,
    async (rows) => {
      for (const row of rows) {
        const rawSessionId = row.sessionId ?? null;
        if (rawSessionId && String(rawSessionId).length > auditSessionIdMaxLength && !truncateAuditSessionId) {
          throw new Error(
            `audit_logs.sessionId length ${String(rawSessionId).length} exceeds ${auditSessionIdMaxLength} (id=${row.id}).`,
          );
        }

        const record = {
          id: row.id,
          userId: requirePlatformUserId(row.userId, row.userEmail ?? null, 'audit_logs.user_id'),
          userEmail: row.userEmail,
          userName: row.userName,
          userRole: row.userRole,
          action: row.action,
          resource: row.resource,
          resourceId: row.resourceId ?? null,
          method: row.method,
          endpoint: row.endpoint,
          statusCode: row.statusCode,
          source: row.source ?? 'WEB',
          deviceId: row.deviceId ?? null,
          sessionId: rawSessionId
            ? String(rawSessionId).slice(0, auditSessionIdMaxLength)
            : null,
          requestId: row.requestId ?? null,
          traceId: row.traceId ?? null,
          geoLocation: row.geoLocation ?? null,
          ipAddress: row.ipAddress ?? null,
          userAgent: row.userAgent ?? null,
          fieldDiffs: row.fieldDiffs ?? null,
          requestBody: row.requestBody ?? null,
          changes: row.changes ?? null,
          reasonCode: row.reasonCode ?? null,
          reasonText: row.reasonText ?? null,
          errorMessage: row.errorMessage ?? null,
          prevHash: row.prevHash ?? null,
          hash: row.hash ?? null,
          isRedacted: toBool(row.isRedacted, false),
          isTombstoned: toBool(row.isTombstoned, false),
          retentionUntil: toDate(row.retentionUntil),
          duration: row.duration ?? null,
          createdAt: toDate(row.createdAt) ?? new Date(),
        } as Prisma.MeetingAttendanceAuditLogUncheckedCreateInput;

        await upsertById(prisma.meetingAttendanceAuditLog, record as any);
      }
    },
    auditLogBatchSize,
  );

  logSection('Data Access Logs');
  const dataAccessLogCount = getRowCount('data_access_logs');
  console.log(`Loaded ${dataAccessLogCount} data access logs.`);
  await runSqliteQueryInBatches('SELECT * FROM data_access_logs ORDER BY id', dataAccessLogCount, async (rows) => {
    for (const row of rows) {
      const record = {
        id: row.id,
        actorId: resolvePlatformUserId(row.actorId ?? null, row.actorEmail ?? null),
        actorEmail: row.actorEmail,
        actorName: row.actorName,
        actorRole: row.actorRole,
        accessType: row.accessType,
        entityType: row.entityType,
        entityIds: row.entityIds ?? null,
        resultCount: row.resultCount,
        hasSensitiveData: toBool(row.hasSensitiveData, false),
        sensitiveFields: row.sensitiveFields ?? null,
        endpoint: row.endpoint,
        method: row.method,
        statusCode: row.statusCode,
        ipAddress: row.ipAddress ?? null,
        sessionId: row.sessionId ?? null,
        requestId: row.requestId ?? null,
        deviceId: row.deviceId ?? null,
        createdAt: toDate(row.createdAt) ?? new Date(),
      } as Prisma.MeetingAttendanceDataAccessLogUncheckedCreateInput;

      await upsertById(prisma.meetingAttendanceDataAccessLog, record as any);
    }
  });

  logSection('Audit Alert Rules');
  const auditAlertRules = runSqliteQuery('SELECT * FROM audit_alert_rules;');
  console.log(`Loaded ${auditAlertRules.length} audit alert rules.`);
  for (const row of auditAlertRules) {
    const record = {
      id: row.id,
      name: row.name,
      description: row.description ?? null,
      ruleType: row.ruleType,
      conditions: row.conditions,
      threshold: row.threshold ?? null,
      timeWindow: row.timeWindow ?? null,
      severity: row.severity,
      isEnabled: toBool(row.isEnabled, true),
      notifyEmail: toBool(row.notifyEmail, true),
      notifyInApp: toBool(row.notifyInApp, true),
      notifyWebhook: toBool(row.notifyWebhook, false),
      webhookUrl: row.webhookUrl ?? null,
      suppressionWindow: row.suppressionWindow ?? null,
      lastTriggered: toDate(row.lastTriggered),
      triggerCount: row.triggerCount ?? 0,
      createdAt: toDate(row.createdAt) ?? new Date(),
      updatedAt: toDate(row.updatedAt) ?? new Date(),
    } as Prisma.MeetingAttendanceAuditAlertRuleUncheckedCreateInput;

    await upsertById(prisma.meetingAttendanceAuditAlertRule, record as any);
  }

  logSection('Audit Alerts');
  const auditAlertCount = getRowCount('audit_alerts');
  console.log(`Loaded ${auditAlertCount} audit alerts.`);
  await runSqliteQueryInBatches('SELECT * FROM audit_alerts ORDER BY id', auditAlertCount, async (rows) => {
    for (const row of rows) {
      const record = {
        id: row.id,
        ruleId: row.ruleId,
        severity: row.severity,
        title: row.title,
        description: row.description,
        triggerData: row.triggerData,
        matchedLogs: row.matchedLogs,
        affectedEntities: row.affectedEntities ?? null,
        status: row.status ?? 'OPEN',
        acknowledgedBy: row.acknowledgedBy ?? null,
        acknowledgedAt: toDate(row.acknowledgedAt),
        resolvedBy: row.resolvedBy ?? null,
        resolvedAt: toDate(row.resolvedAt),
        resolution: row.resolution ?? null,
        createdAt: toDate(row.createdAt) ?? new Date(),
        updatedAt: toDate(row.updatedAt) ?? new Date(),
      } as Prisma.MeetingAttendanceAuditAlertUncheckedCreateInput;

      await upsertById(prisma.meetingAttendanceAuditAlert, record as any);
    }
  });

  logSection('Retention Policies');
  const retentionPolicies = runSqliteQuery('SELECT * FROM retention_policies;');
  console.log(`Loaded ${retentionPolicies.length} retention policies.`);
  for (const row of retentionPolicies) {
    const record = {
      id: row.id,
      entityType: row.entityType,
      hotDays: row.hotDays ?? 365,
      warmDays: row.warmDays ?? 730,
      coldDays: row.coldDays ?? 2555,
      autoArchive: toBool(row.autoArchive, true),
      autoDelete: toBool(row.autoDelete, false),
      isActive: toBool(row.isActive, true),
      createdAt: toDate(row.createdAt) ?? new Date(),
      updatedAt: toDate(row.updatedAt) ?? new Date(),
    } as Prisma.MeetingAttendanceRetentionPolicyUncheckedCreateInput;

    await upsertById(prisma.meetingAttendanceRetentionPolicy, record as any);
  }

  console.log('\nMigration completed.');
}

main()
  .catch((error) => {
    console.error('Migration failed:', error);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });
