import {
  MessageComponentsV2Schema,
  WelcomeEmbedSchema,
  type MessageComponentsV2,
  type ScheduledMessageDashboard,
  type ScheduledMessageDashboardCreate,
  type WelcomeEmbed
} from '@nexumi/shared';
import { Prisma, type ScheduledMessage } from '@prisma/client';
import { prisma } from '../prisma';
import { getScheduleQueue } from '../queues';

export class SchedulerValidationError extends Error {
  constructor(public readonly code: string) {
    super(code);
  }
}

function parseEmbed(raw: unknown): WelcomeEmbed | null {
  const parsed = WelcomeEmbedSchema.safeParse(raw);
  return parsed.success ? parsed.data : null;
}

function parseComponents(raw: unknown): MessageComponentsV2 | null {
  const parsed = MessageComponentsV2Schema.safeParse(raw);
  return parsed.success ? parsed.data : null;
}

function toDashboard(schedule: ScheduledMessage): ScheduledMessageDashboard {
  return {
    id: schedule.id,
    channelId: schedule.channelId,
    creatorId: schedule.creatorId,
    content: schedule.content,
    embed: parseEmbed(schedule.embed),
    components: parseComponents(schedule.components),
    rolePingId: schedule.rolePingId,
    cron: schedule.cron,
    runAt: schedule.runAt?.toISOString() ?? null,
    enabled: schedule.enabled,
    createdAt: schedule.createdAt.toISOString()
  };
}

export async function listScheduledMessagesDashboard(guildId: string): Promise<ScheduledMessageDashboard[]> {
  const schedules = await prisma.scheduledMessage.findMany({
    where: { guildId, enabled: true },
    orderBy: { createdAt: 'asc' },
    take: 50
  });
  return schedules.map(toDashboard);
}

function isValidCron(pattern: string): boolean {
  const parts = pattern.trim().split(/\s+/);
  return parts.length >= 5 && parts.length <= 6;
}

/**
 * Mirrors `apps/bot/src/modules/scheduler/service.ts#createScheduledMessage`
 * job scheduling exactly (same queue, same `scheduleSend` job name, same
 * `schedule-{id}` job id convention) so schedules created from the
 * dashboard are picked up by the bot's existing `scheduleWorker` without any
 * changes on the bot side. Channel permission checks are skipped here since
 * the WebUI has no discord.js Client - the bot validates the channel again
 * when the job fires and disables the schedule if it is unusable.
 */
export async function createScheduledMessageDashboard(
  guildId: string,
  creatorId: string,
  input: ScheduledMessageDashboardCreate
): Promise<ScheduledMessageDashboard> {
  await prisma.guild.upsert({ where: { id: guildId }, update: {}, create: { id: guildId } });

  const cron = input.cron?.trim() || null;
  if (cron && !isValidCron(cron)) {
    throw new SchedulerValidationError('invalid_cron');
  }

  const runAt = !cron && input.runAt ? new Date(input.runAt) : null;
  if (!cron && !runAt) {
    throw new SchedulerValidationError('missing_schedule_time');
  }

  const schedule = await prisma.scheduledMessage.create({
    data: {
      guildId,
      channelId: input.channelId,
      creatorId,
      content: input.content?.trim() || null,
      embed:
        input.embed === undefined || input.embed === null
          ? Prisma.JsonNull
          : (input.embed as Prisma.InputJsonValue),
      components:
        input.components === undefined || input.components === null
          ? Prisma.JsonNull
          : (input.components as Prisma.InputJsonValue),
      rolePingId: input.rolePingId || null,
      cron,
      runAt
    }
  });

  const jobOptions: {
    jobId: string;
    removeOnComplete: boolean;
    removeOnFail: number;
    delay?: number;
    repeat?: { pattern: string };
  } = {
    jobId: `schedule-${schedule.id}`,
    removeOnComplete: true,
    removeOnFail: 50
  };
  if (cron) {
    jobOptions.repeat = { pattern: cron };
  } else if (runAt) {
    jobOptions.delay = Math.max(0, runAt.getTime() - Date.now());
  }

  const job = await getScheduleQueue().add('scheduleSend', { scheduleId: schedule.id }, jobOptions);
  const updated = await prisma.scheduledMessage.update({
    where: { id: schedule.id },
    data: { jobId: String(job.id) }
  });

  return toDashboard(updated);
}

export async function deleteScheduledMessageDashboard(guildId: string, scheduleId: string): Promise<boolean> {
  const existing = await prisma.scheduledMessage.findFirst({ where: { id: scheduleId, guildId } });
  if (!existing) {
    return false;
  }

  if (existing.jobId) {
    const job = await getScheduleQueue().getJob(existing.jobId);
    if (job) {
      try {
        await job.remove();
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        if (!message.includes('locked') && !message.includes('could not be removed')) {
          throw error;
        }
      }
    }
  }

  await prisma.scheduledMessage.delete({ where: { id: scheduleId } });
  return true;
}
