import {
  PermissionFlagsBits,
  type GuildTextBasedChannel,
  type MessageCreateOptions,
  type TextChannel
} from 'discord.js';
import { WelcomeEmbedSchema, expandBracketChannelMentions, parseMessageComponentsV2, t, tf } from '@nexumi/shared';
import type { ScheduledMessage } from '@prisma/client';
import { ensureGuild } from '../../guild.js';
import { buildEmbedFromPayload } from '../../lib/embed-payload.js';
import { buildComponentsV2Payload } from '../../lib/components-v2-payload.js';
import { logger } from '../../logger.js';
import { scheduleQueue } from '../../queues.js';
import type { BotContext } from '../../types.js';
import { parseDuration } from '../moderation/duration.js';

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

export type AnnouncementInput = {
  guildId: string;
  channelId: string;
  creatorId: string;
  content: string;
  embedTitle?: string | null;
  embedDescription?: string | null;
  embedColor?: number | null;
  rolePingId?: string | null;
};

export type ScheduleCreateInput = AnnouncementInput & {
  whenInput?: string | null;
  cron?: string | null;
};

function parseScheduleWhen(input: string): Date {
  const trimmed = input.trim();
  if (/^\d+[smhd]$/i.test(trimmed)) {
    const ms = parseDuration(trimmed);
    if (ms <= 0) {
      throw new SchedulerError('invalid_when');
    }
    return new Date(Date.now() + ms);
  }

  const parsed = Date.parse(trimmed);
  if (Number.isNaN(parsed) || parsed <= Date.now()) {
    throw new SchedulerError('invalid_when');
  }
  return new Date(parsed);
}

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

function buildEmbedJson(input: {
  embedTitle?: string | null;
  embedDescription?: string | null;
  embedColor?: number | null;
}): { title?: string; description?: string; color?: number } | null {
  const title = input.embedTitle?.trim();
  const description = input.embedDescription?.trim();
  const color = input.embedColor ?? undefined;

  if (!title && !description && color === undefined) {
    return null;
  }

  const embed: { title?: string; description?: string; color?: number } = {};
  if (title) {
    embed.title = title;
  }
  if (description) {
    embed.description = description;
  }
  if (color !== undefined) {
    embed.color = color;
  }
  return embed;
}

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

export function buildAnnouncementPayload(schedule: {
  content: string | null;
  embed: unknown;
  components?: unknown;
  rolePingId: string | null;
  id?: string;
}): MessageCreateOptions {
  const componentsData = parseMessageComponentsV2(schedule.components);
  if (componentsData && schedule.id) {
    const componentsPayload = buildComponentsV2Payload(componentsData, {
      source: 's',
      ref: schedule.id,
      renderText: expandBracketChannelMentions
    });
    if (componentsPayload) {
      if (schedule.rolePingId) {
        // Components V2 cannot mix classic content; role ping is skipped.
      }
      return componentsPayload;
    }
  }

  const embedData = parseStoredEmbed(schedule.embed);
  const embed = buildEmbedFromPayload(embedData, {
    renderText: expandBracketChannelMentions
  });

  const baseContent = expandBracketChannelMentions(schedule.content?.trim() ?? '');
  const roleMention = schedule.rolePingId ? `<@&${schedule.rolePingId}>` : '';
  const content = [roleMention, baseContent].filter(Boolean).join(' ').trim();

  const payload: MessageCreateOptions = {};
  if (content) {
    payload.content = content;
  }
  if (embed) {
    payload.embeds = [embed];
  }
  if (schedule.rolePingId) {
    payload.allowedMentions = { roles: [schedule.rolePingId] };
  }

  return payload;
}

async function assertSendableChannel(
  context: BotContext,
  channelId: string
): Promise<GuildTextBasedChannel> {
  const channel = await context.client.channels.fetch(channelId);
  if (!channel?.isTextBased() || channel.isDMBased()) {
    throw new SchedulerError('invalid_channel');
  }

  const me = channel.guild.members.me;
  if (
    !me?.permissionsIn(channel).has(PermissionFlagsBits.SendMessages) ||
    !me.permissionsIn(channel).has(PermissionFlagsBits.ViewChannel)
  ) {
    throw new SchedulerError('channel_unavailable');
  }

  return channel;
}

async function enqueueScheduleJob(
  schedule: ScheduledMessage,
  cron: string | null,
  runAt: Date | null
): Promise<string> {
  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 scheduleQueue.add('scheduleSend', { scheduleId: schedule.id }, jobOptions);
  return String(job.id);
}

export async function removeScheduleJob(jobId: string | null): Promise<void> {
  if (!jobId) {
    return;
  }

  const job = await scheduleQueue.getJob(jobId);
  if (job) {
    await job.remove();
  }
}

function validateAnnouncementContent(input: AnnouncementInput): void {
  const hasContent = input.content.trim().length > 0;
  const hasEmbed =
    Boolean(input.embedTitle?.trim()) ||
    Boolean(input.embedDescription?.trim()) ||
    input.embedColor !== null && input.embedColor !== undefined;

  if (!hasContent && !hasEmbed) {
    throw new SchedulerError('content_required');
  }
}

export async function createScheduledMessage(
  context: BotContext,
  input: ScheduleCreateInput
): Promise<ScheduledMessage> {
  await ensureGuild(context.prisma, input.guildId);
  validateAnnouncementContent(input);

  const whenInput = input.whenInput?.trim();
  const cron = input.cron?.trim();

  if (!whenInput && !cron) {
    throw new SchedulerError('missing_schedule_time');
  }

  let runAt: Date | null = null;
  let cronPattern: string | null = null;

  if (cron) {
    if (!isValidCron(cron)) {
      throw new SchedulerError('invalid_cron');
    }
    cronPattern = cron;
  } else if (whenInput) {
    runAt = parseScheduleWhen(whenInput);
  }

  await assertSendableChannel(context, input.channelId);

  const embed = buildEmbedJson(input);

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

  const jobId = await enqueueScheduleJob(schedule, cronPattern, runAt);
  return context.prisma.scheduledMessage.update({
    where: { id: schedule.id },
    data: { jobId }
  });
}

export async function listScheduledMessages(
  context: BotContext,
  guildId: string,
  locale: 'de' | 'en'
): Promise<string> {
  const schedules = await context.prisma.scheduledMessage.findMany({
    where: { guildId, enabled: true },
    orderBy: { createdAt: 'asc' },
    take: 20
  });

  if (schedules.length === 0) {
    return t(locale, 'scheduler.list.empty');
  }

  return schedules
    .map((schedule) => {
      const preview =
        schedule.content?.slice(0, 60) ??
        (schedule.components
          ? t(locale, 'scheduler.list.componentsPreview')
          : schedule.embed
            ? t(locale, 'scheduler.list.embedPreview')
            : '—');
      const timing = schedule.cron
        ? tf(locale, 'scheduler.list.cron', { cron: schedule.cron })
        : schedule.runAt
          ? `<t:${Math.floor(schedule.runAt.getTime() / 1000)}:R>`
          : '—';

      return tf(locale, 'scheduler.list.line', {
        id: schedule.id.slice(0, 8),
        channel: `<#${schedule.channelId}>`,
        timing,
        preview
      });
    })
    .join('\n');
}

export async function deleteScheduledMessage(
  context: BotContext,
  guildId: string,
  scheduleIdPrefix: string
): Promise<boolean> {
  const schedules = await context.prisma.scheduledMessage.findMany({
    where: { guildId, enabled: true }
  });
  const match = schedules.find((entry) => entry.id.startsWith(scheduleIdPrefix));
  if (!match) {
    return false;
  }

  await removeScheduleJob(match.jobId);
  await context.prisma.scheduledMessage.delete({ where: { id: match.id } });
  return true;
}

export async function sendAnnouncement(
  context: BotContext,
  input: AnnouncementInput
): Promise<void> {
  validateAnnouncementContent(input);
  const channel = await assertSendableChannel(context, input.channelId);
  const payload = buildAnnouncementPayload({
    content: input.content.trim() || null,
    embed: buildEmbedJson(input),
    rolePingId: input.rolePingId ?? null
  });
  await (channel as TextChannel).send(payload);
}

export async function sendScheduledMessage(context: BotContext, scheduleId: string): Promise<void> {
  const schedule = await context.prisma.scheduledMessage.findUnique({ where: { id: scheduleId } });
  if (!schedule || !schedule.enabled) {
    return;
  }

  try {
    const channel = await assertSendableChannel(context, schedule.channelId);
    const payload = buildAnnouncementPayload(schedule);
    if (!payload.content && !payload.embeds?.length && !payload.components?.length) {
      throw new SchedulerError('content_required');
    }
    await (channel as TextChannel).send(payload);
  } catch (error) {
    logger.error({ scheduleId, error }, 'Failed to send scheduled message');
    if (error instanceof SchedulerError && error.code === 'channel_unavailable') {
      await context.prisma.scheduledMessage.update({
        where: { id: scheduleId },
        data: { enabled: false }
      });
    }
    throw error;
  }

  if (!schedule.cron) {
    await removeScheduleJob(schedule.jobId);
    await context.prisma.scheduledMessage.delete({ where: { id: scheduleId } });
  }
}
