import { Worker } from 'bullmq';
import { PermissionFlagsBits } from 'discord.js';
import { mkdir, readdir, rm, stat } from 'node:fs/promises';
import { basename, join } from 'node:path';
import { spawn } from 'node:child_process';
import { redis } from './redis.js';
import { logger } from './logger.js';
import type { BotContext } from './types.js';
import { env } from './env.js';
import { refreshPhishingDomains } from './modules/automod/filters.js';
import { activateLockdown, deactivateLockdown } from './modules/automod/actions.js';
import {
  completeVerification,
  runVerificationPanelSyncJob
} from './modules/verification/handlers.js';
import { closePoll } from './modules/utility/poll.js';
import { sendReminder } from './modules/utility/reminders.js';
import {
  expireSelfRole,
  runSelfRolePanelCreateJob,
  runSelfRolePanelUpdateJob,
  runSelfRolePanelDeleteJob
} from './modules/selfroles/index.js';
import type { SelfRoleBehavior, SelfRoleEntry, SelfRoleMode } from '@nexumi/shared';
import {
  endGiveaway,
  runGiveawayCreateJob,
  runGiveawayPauseJob,
  runGiveawayDeleteJob,
  GiveawayError
} from './modules/giveaways/index.js';
import { closeInactiveTickets, runTicketDashboardActionJob, runTicketPanelSyncJob } from './modules/tickets/index.js';
import { expireBirthdayRole, runBirthdayCheck } from './modules/birthdays/index.js';
import { ensureStatsRecurringJobs, startStatsWorker } from './modules/stats/index.js';
import { pollAllFeeds } from './modules/feeds/index.js';
import { sendScheduledMessage } from './modules/scheduler/index.js';
import { sendDashboardMessage, type DashboardMessageSendJob } from './modules/components-v2/send.js';
import { runGuildBackupRestoreJob } from './modules/guildbackup/index.js';
import { runSuggestionStatusUpdateJob } from './modules/suggestions/index.js';
import { runPresenceRefreshJob } from './presence.js';
import {
  automodQueue,
  automodQueueName,
  backupQueue,
  backupQueueName,
  birthdayQueue,
  birthdayQueueName,
  feedsQueue,
  feedsQueueName,
  giveawayQueueName,
  guildBackupQueueName,
  messagesQueueName,
  moderationQueueName,
  presenceQueue,
  presenceQueueName,
  reminderQueueName,
  retentionQueue,
  retentionQueueName,
  scheduleQueueName,
  selfrolesQueueName,
  suggestionsQueueName,
  ticketQueue,
  ticketQueueName,
  verificationQueueName
} from './queues.js';

export {
  automodQueue,
  backupQueue,
  moderationQueue,
  moderationQueueName,
  reminderQueue,
  verificationQueue
} from './queues.js';

type TempBanJob = {
  guildId: string;
  userId: string;
  reason: string | null;
};

type VerificationCompleteJob = {
  guildId: string;
  userId: string;
  ipHash?: string | null;
  userAgentHash?: string | null;
  captchaProvider?: string | null;
};

type VerificationPanelSyncJob = {
  guildId: string;
};

type ReminderSendJob = {
  reminderId: string;
};

type PollCloseJob = {
  pollId: string;
};

type SelfRoleExpireJob = {
  guildId: string;
  userId: string;
  roleId: string;
};

type GiveawayEndJob = {
  giveawayId: string;
  /** When true, end even if the giveaway is paused (dashboard/slash paths). */
  force?: boolean;
};

type GiveawayCreateJob = {
  guildId: string;
  channelId: string;
  hostId: string;
  prize: string;
  winnerCount: number;
  durationMs: number;
  requiredRoleId?: string | null;
  requiredLevel?: number | null;
  requiredMemberDays?: number | null;
};

type GiveawayPauseJob = {
  giveawayId: string;
  paused: boolean;
};

type GiveawayDeleteJob = {
  giveawayId: string;
};


type BirthdayRoleExpireJob = {
  guildId: string;
  userId: string;
  roleId: string;
};

type SuggestionStatusUpdateJob = {
  suggestionId: string;
  guildId: string;
  status: 'OPEN' | 'APPROVED' | 'DENIED' | 'CONSIDERED' | 'IMPLEMENTED';
  reason: string;
};

type SelfRolePanelCreateJob = {
  guildId: string;
  channelId: string;
  title: string;
  description?: string | null;
  mode: SelfRoleMode;
  behavior: SelfRoleBehavior;
  roles: SelfRoleEntry[];
  temporaryDurationMs?: number;
  expiresAt?: string | null;
};

type SelfRolePanelUpdateJob = {
  panelId: string;
  guildId: string;
  channelId?: string;
  title?: string;
  description?: string | null;
  mode?: SelfRoleMode;
  behavior?: SelfRoleBehavior;
  roles?: SelfRoleEntry[];
  temporaryDurationMs?: number;
  expiresAt?: string | null;
};

type SelfRolePanelDeleteJob = {
  panelId: string;
  guildId: string;
};

export function startWorkers(context: BotContext): Worker[] {
  const moderationWorker = new Worker<TempBanJob>(
    moderationQueueName,
    async (job) => {
      if (job.name !== 'tempBanExpire') {
        return;
      }

      const { guildId, userId, reason } = job.data;
      const guild = context.client.guilds.cache.get(guildId);
      if (!guild) {
        if (!context.client.shard) {
          throw new Error(`Guild ${guildId} not available for temp ban expiration`);
        }
        const results = await context.client.shard.broadcastEval(
          async (c, payload) => {
            const g = c.guilds.cache.get(payload.guildId);
            if (!g) {
              return false;
            }
            const me = await g.members.fetchMe();
            if (!me.permissions.has(PermissionFlagsBits.BanMembers)) {
              throw new Error('Missing BanMembers permission for temp ban expiration');
            }
            try {
              await g.members.unban(payload.userId, payload.reason ?? 'Temporary ban expired');
            } catch (error) {
              const code =
                typeof error === 'object' && error && 'code' in error
                  ? Number((error as { code: unknown }).code)
                  : 0;
              // Unknown ban (10026) — already unbanned
              if (code === 10026) {
                return true;
              }
              throw error;
            }
            return true;
          },
          { context: { guildId, userId, reason } }
        );
        if (!results.some(Boolean)) {
          throw new Error(`Guild ${guildId} not found on any shard for temp ban expiration`);
        }
      } else {
        const me = await guild.members.fetchMe();
        if (!me.permissions.has(PermissionFlagsBits.BanMembers)) {
          throw new Error('Missing BanMembers permission for temp ban expiration');
        }
        try {
          await guild.members.unban(userId, reason ?? 'Temporary ban expired');
        } catch (error) {
          const code =
            typeof error === 'object' && error && 'code' in error
              ? Number((error as { code: unknown }).code)
              : 0;
          if (code !== 10026) {
            throw error;
          }
        }
      }

      const { createCase } = await import('./modules/moderation/service.js');
      await createCase(context, {
        guildId,
        targetUserId: userId,
        moderatorId: context.client.user?.id ?? 'system',
        action: 'UNBAN',
        reason: reason ?? 'Temporary ban expired',
        source: 'job',
        metadata: { tempBanExpire: true }
      });
    },
    { connection: redis }
  );

  moderationWorker.on('failed', (job, error) => {
    logger.error({ jobId: job?.id, error }, 'Moderation job failed');
  });

  const backupWorker = new Worker(
    backupQueueName,
    async (job) => {
      if (job.name !== 'dailyPgDump') {
        return;
      }
      await runPgDumpBackup();
    },
    { connection: redis }
  );

  backupWorker.on('failed', (job, error) => {
    logger.error({ jobId: job?.id, error }, 'Backup job failed');
  });

  const automodWorker = new Worker(
    automodQueueName,
    async (job) => {
      if (job.name === 'refreshPhishingList') {
        const count = await refreshPhishingDomains(redis);
        logger.info({ domainCount: count }, 'Phishing domain list refreshed');
        return;
      }
      if (job.name === 'deactivateLockdown' || job.name === 'activateLockdown') {
        const guildId = (job.data as { guildId?: string }).guildId;
        if (!guildId) {
          return;
        }
        const guild = await context.client.guilds.fetch(guildId).catch(() => null);
        if (!guild) {
          return;
        }
        if (job.name === 'deactivateLockdown') {
          await deactivateLockdown(guild);
          logger.info({ guildId }, 'Lockdown deactivated via dashboard');
          return;
        }
        await activateLockdown(guild);
        logger.info({ guildId }, 'Lockdown activated via dashboard');
      }
    },
    { connection: redis }
  );

  automodWorker.on('failed', (job, error) => {
    logger.error({ jobId: job?.id, error }, 'AutoMod job failed');
  });

  const verificationWorker = new Worker<VerificationCompleteJob | VerificationPanelSyncJob>(
    verificationQueueName,
    async (job) => {
      if (job.name === 'verificationPanelSync') {
        return runVerificationPanelSyncJob(context, job.data as VerificationPanelSyncJob);
      }
      if (job.name !== 'verificationComplete') {
        return;
      }
      const data = job.data as VerificationCompleteJob;
      await completeVerification(context, data.guildId, data.userId, {
        ipHash: data.ipHash,
        userAgentHash: data.userAgentHash,
        captchaProvider: data.captchaProvider
      });
    },
    { connection: redis }
  );

  verificationWorker.on('failed', (job, error) => {
    logger.error({ jobId: job?.id, error }, 'Verification job failed');
  });

  const reminderWorker = new Worker<ReminderSendJob | PollCloseJob>(
    reminderQueueName,
    async (job) => {
      if (job.name === 'reminderSend') {
        await sendReminder(context, (job.data as ReminderSendJob).reminderId);
        return;
      }
      if (job.name === 'pollClose') {
        await closePoll(context, (job.data as PollCloseJob).pollId);
      }
    },
    { connection: redis }
  );

  reminderWorker.on('failed', (job, error) => {
    logger.error({ jobId: job?.id, error }, 'Reminder job failed');
  });

  const ticketWorker = new Worker(
    ticketQueueName,
    async (job) => {
      if (job.name === 'ticketPanelSync') {
        return runTicketPanelSyncJob(context, job.data as { guildId: string });
      }
      if (job.name === 'ticketDashboardAction') {
        return runTicketDashboardActionJob(
          context,
          job.data as {
            ticketId: string;
            guildId: string;
            actorUserId: string;
            action: 'close' | 'claim' | 'priority';
            reason?: string;
            priority?: string;
          }
        );
      }
      if (job.name === 'selfRoleExpire') {
        const data = job.data as SelfRoleExpireJob;
        await expireSelfRole(context, data.guildId, data.userId, data.roleId);
        return;
      }
      if (job.name === 'ticketAutoClose') {
        await closeInactiveTickets(context);
      }
    },
    { connection: redis }
  );

  ticketWorker.on('failed', (job, error) => {
    logger.error({ jobId: job?.id, error }, 'Ticket queue job failed');
  });

  const selfrolesWorker = new Worker<
    SelfRolePanelCreateJob | SelfRolePanelUpdateJob | SelfRolePanelDeleteJob
  >(
    selfrolesQueueName,
    async (job) => {
      if (job.name === 'selfRolePanelCreate') {
        return runSelfRolePanelCreateJob(context, job.data as SelfRolePanelCreateJob);
      }
      if (job.name === 'selfRolePanelUpdate') {
        return runSelfRolePanelUpdateJob(context, job.data as SelfRolePanelUpdateJob);
      }
      if (job.name === 'selfRolePanelDelete') {
        const data = job.data as SelfRolePanelDeleteJob;
        await runSelfRolePanelDeleteJob(context, data.panelId, data.guildId);
      }
    },
    { connection: redis }
  );

  selfrolesWorker.on('failed', (job, error) => {
    logger.error({ jobId: job?.id, error }, 'Self-roles queue job failed');
  });

  const giveawayWorker = new Worker<
    GiveawayEndJob | GiveawayCreateJob | GiveawayPauseJob | GiveawayDeleteJob
  >(
    giveawayQueueName,
    async (job) => {
      if (job.name === 'giveawayEnd') {
        try {
          const data = job.data as GiveawayEndJob;
          if (!data.force) {
            const current = await context.prisma.giveaway.findUnique({
              where: { id: data.giveawayId }
            });
            // Timer/recover must not end a paused giveaway.
            if (current?.paused) {
              return;
            }
          }
          await endGiveaway(context, data.giveawayId);
        } catch (error) {
          if (
            error instanceof GiveawayError &&
            (error.code === 'already_ended' || error.code === 'not_found')
          ) {
            return;
          }
          throw error;
        }
        return;
      }
      if (job.name === 'giveawayCreate') {
        return runGiveawayCreateJob(context, job.data as GiveawayCreateJob);
      }
      if (job.name === 'giveawayPause') {
        const data = job.data as GiveawayPauseJob;
        return runGiveawayPauseJob(context, data.giveawayId, data.paused);
      }
      if (job.name === 'giveawayDelete') {
        await runGiveawayDeleteJob(context, (job.data as GiveawayDeleteJob).giveawayId);
      }
    },
    { connection: redis }
  );

  giveawayWorker.on('failed', (job, error) => {
    logger.error({ jobId: job?.id, error }, 'Giveaway job failed');
  });

  const birthdayWorker = new Worker(
    birthdayQueueName,
    async (job) => {
      if (job.name === 'birthdayDailyCheck') {
        await runBirthdayCheck(context);
        return;
      }
      if (job.name === 'birthdayRoleExpire') {
        const data = job.data as BirthdayRoleExpireJob;
        await expireBirthdayRole(context, data.guildId, data.userId, data.roleId);
      }
    },
    { connection: redis }
  );

  birthdayWorker.on('failed', (job, error) => {
    logger.error({ jobId: job?.id, error }, 'Birthday job failed');
  });

  const statsWorker = startStatsWorker(context);

  const feedsWorker = new Worker(
    feedsQueueName,
    async (job) => {
      if (job.name !== 'pollFeeds') {
        return;
      }
      await pollAllFeeds(context);
    },
    { connection: redis }
  );
  feedsWorker.on('failed', (job, error) => {
    logger.error({ jobId: job?.id, error }, 'Feeds job failed');
  });

  const scheduleWorker = new Worker<{ scheduleId: string }>(
    scheduleQueueName,
    async (job) => {
      if (job.name !== 'scheduleSend') {
        return;
      }
      await sendScheduledMessage(context, job.data.scheduleId);
    },
    { connection: redis }
  );
  scheduleWorker.on('failed', (job, error) => {
    logger.error({ jobId: job?.id, error }, 'Schedule job failed');
  });

  const messagesWorker = new Worker<DashboardMessageSendJob>(
    messagesQueueName,
    async (job) => {
      if (job.name !== 'dashboardMessageSend') {
        return;
      }
      return sendDashboardMessage(context, job.data);
    },
    { connection: redis }
  );
  messagesWorker.on('failed', (job, error) => {
    logger.error({ jobId: job?.id, error }, 'Dashboard message send job failed');
  });

  const guildBackupWorker = new Worker<{ backupId: string; guildId: string }>(
    guildBackupQueueName,
    async (job) => {
      if (job.name !== 'restoreGuildBackup') {
        return;
      }
      await runGuildBackupRestoreJob(context, job.data.backupId, job.data.guildId);
    },
    { connection: redis }
  );
  guildBackupWorker.on('failed', (job, error) => {
    logger.error({ jobId: job?.id, error }, 'Guild backup restore job failed');
  });

  const suggestionsWorker = new Worker<SuggestionStatusUpdateJob>(
    suggestionsQueueName,
    async (job) => {
      if (job.name !== 'suggestionStatusUpdate') {
        return;
      }
      return runSuggestionStatusUpdateJob(context, job.data);
    },
    { connection: redis }
  );
  suggestionsWorker.on('failed', (job, error) => {
    logger.error({ jobId: job?.id, error }, 'Suggestion status update job failed');
  });

  const presenceWorker = new Worker(
    presenceQueueName,
    async (job) => {
      if (job.name !== 'presenceRefresh') {
        return;
      }
      await runPresenceRefreshJob(context);
    },
    { connection: redis }
  );
  presenceWorker.on('failed', (job, error) => {
    logger.error({ jobId: job?.id, error }, 'Presence refresh job failed');
  });

  const retentionWorker = new Worker(
    retentionQueueName,
    async (job) => {
      if (job.name !== 'purgeExpiredLogs') {
        return;
      }
      await runLogRetentionJob(context);
    },
    { connection: redis }
  );
  retentionWorker.on('failed', (job, error) => {
    logger.error({ jobId: job?.id, error }, 'Log retention job failed');
  });

  return [
    moderationWorker,
    backupWorker,
    automodWorker,
    verificationWorker,
    reminderWorker,
    ticketWorker,
    selfrolesWorker,
    giveawayWorker,
    birthdayWorker,
    statsWorker,
    feedsWorker,
    scheduleWorker,
    messagesWorker,
    guildBackupWorker,
    suggestionsWorker,
    presenceWorker,
    retentionWorker
  ];
}

export async function ensureRecurringJobs(): Promise<void> {
  await backupQueue.add(
    'dailyPgDump',
    {},
    {
      repeat: { pattern: env.BACKUP_CRON },
      removeOnComplete: 100,
      removeOnFail: 100
    }
  );

  await automodQueue.add(
    'refreshPhishingList',
    {},
    {
      repeat: { pattern: '0 */6 * * *' },
      removeOnComplete: 20,
      removeOnFail: 20
    }
  );

  await automodQueue.add('refreshPhishingList', {}, { removeOnComplete: 20, removeOnFail: 20 });

  await ticketQueue.add(
    'ticketAutoClose',
    {},
    {
      repeat: { pattern: '0 * * * *' },
      removeOnComplete: 20,
      removeOnFail: 20
    }
  );

  await birthdayQueue.add(
    'birthdayDailyCheck',
    {},
    {
      repeat: { pattern: '0 * * * *' },
      removeOnComplete: 20,
      removeOnFail: 20
    }
  );

  await ensureStatsRecurringJobs();

  await feedsQueue.add(
    'pollFeeds',
    {},
    {
      repeat: { pattern: '*/5 * * * *' },
      removeOnComplete: 20,
      removeOnFail: 20
    }
  );

  await presenceQueue.add(
    'presenceRefresh',
    {},
    {
      repeat: { every: 60_000 },
      removeOnComplete: 5,
      removeOnFail: 5
    }
  );

  await retentionQueue.add(
    'purgeExpiredLogs',
    {},
    {
      repeat: { pattern: '15 4 * * *' },
      removeOnComplete: 20,
      removeOnFail: 20
    }
  );
}

async function runLogRetentionJob(context: BotContext): Promise<void> {
  const configs = await context.prisma.loggingConfig.findMany({
    where: { retentionDays: { gt: 0 } },
    select: { guildId: true, retentionDays: true }
  });

  for (const config of configs) {
    const cutoff = new Date(Date.now() - config.retentionDays * 24 * 60 * 60 * 1000);
    const [cases, audits] = await Promise.all([
      context.prisma.case.deleteMany({
        where: { guildId: config.guildId, createdAt: { lt: cutoff } }
      }),
      context.prisma.dashboardAuditLog.deleteMany({
        where: { guildId: config.guildId, createdAt: { lt: cutoff } }
      })
    ]);
    if (cases.count > 0 || audits.count > 0) {
      logger.info(
        { guildId: config.guildId, cases: cases.count, audits: audits.count, retentionDays: config.retentionDays },
        'Log retention purge completed'
      );
    }
  }
}

async function runPgDumpBackup(): Promise<void> {
  await mkdir(env.BACKUP_DIR, { recursive: true });
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
  const filename = `nexumi-${timestamp}.sql`;
  const outputPath = join(env.BACKUP_DIR, filename);

  await new Promise<void>((resolve, reject) => {
    const dump = spawn('pg_dump', ['--dbname', env.DATABASE_URL, '--file', outputPath], {
      stdio: 'pipe'
    });
    let stderr = '';
    dump.stderr.on('data', (chunk: Buffer) => {
      stderr += chunk.toString('utf8');
    });
    dump.on('close', (code) => {
      if (code === 0) {
        resolve();
        return;
      }
      reject(new Error(stderr || `pg_dump exited with code ${code ?? 'unknown'}`));
    });
  });

  await cleanupOldBackups();
  logger.info({ backupFile: basename(outputPath) }, 'Database backup finished');
}

async function cleanupOldBackups(): Promise<void> {
  const files = await readdir(env.BACKUP_DIR, { withFileTypes: true });
  const cutoff = Date.now() - env.BACKUP_RETENTION_DAYS * 24 * 60 * 60 * 1000;
  await Promise.all(
    files
      .filter((entry) => entry.isFile() && entry.name.endsWith('.sql'))
      .map(async (entry) => {
        const fullPath = join(env.BACKUP_DIR, entry.name);
        const fileStat = await stat(fullPath);
        const created = fileStat.mtime.getTime();
        if (Number.isNaN(created) || created >= cutoff) {
          return;
        }
        await rm(fullPath, { force: true });
      })
  );
}
