import { PermissionFlagsBits, type TextChannel } from 'discord.js';
import type { SocialFeed } from '@prisma/client';
import { applyFeedTemplate, SocialFeedTypeSchema } from '@nexumi/shared';
import { z } from 'zod';
import { ensureGuild } from '../../guild.js';
import { logger } from '../../logger.js';
import { assertPremiumLimit, PremiumLimitError } from '../../premium.js';
import type { BotContext } from '../../types.js';
import { fetchFeedItems, type FeedItem, type FetchResult } from './fetchers.js';

export const FeedAddSchema = z.object({
  type: SocialFeedTypeSchema,
  source: z.string().min(1).max(512),
  channelId: z.string().min(1),
  rolePingId: z.string().min(1).optional(),
  template: z.string().min(1).max(2000).optional()
});

export type FeedAddInput = z.infer<typeof FeedAddSchema>;

export class FeedError extends Error {
  constructor(public readonly code: string) {
    super(code);
    this.name = 'FeedError';
  }
}

const DEFAULT_TEMPLATE = '{title}\n{url}';

export function normalizeFeedSource(type: z.infer<typeof SocialFeedTypeSchema>, source: string): string {
  const trimmed = source.trim();
  switch (type) {
    case 'TWITCH':
      return trimmed.replace(/^@/, '').toLowerCase();
    case 'REDDIT':
      return trimmed.replace(/^r\//i, '').toLowerCase();
    case 'YOUTUBE':
    case 'RSS':
      return trimmed;
    default:
      return trimmed;
  }
}

export async function addFeed(
  prisma: BotContext['prisma'],
  guildId: string,
  input: FeedAddInput
): Promise<SocialFeed> {
  await ensureGuild(prisma, guildId);
  const sourceId = normalizeFeedSource(input.type, input.source);

  const currentCount = await prisma.socialFeed.count({ where: { guildId } });
  try {
    await assertPremiumLimit(prisma, guildId, 'feeds', currentCount);
  } catch (error) {
    if (error instanceof PremiumLimitError) {
      throw new FeedError(error.code === 'feature_disabled' ? 'premium_feature' : 'premium_limit');
    }
    throw error;
  }

  return prisma.socialFeed.create({
    data: {
      guildId,
      type: input.type,
      sourceId,
      channelId: input.channelId,
      rolePingId: input.rolePingId ?? null,
      template: input.template ?? null
    }
  });
}

export async function removeFeed(
  prisma: BotContext['prisma'],
  guildId: string,
  feedId: string
): Promise<SocialFeed | null> {
  const existing = await prisma.socialFeed.findFirst({
    where: { id: feedId, guildId }
  });
  if (!existing) {
    return null;
  }
  await prisma.socialFeed.delete({ where: { id: feedId } });
  return existing;
}

export async function listFeeds(prisma: BotContext['prisma'], guildId: string): Promise<SocialFeed[]> {
  await ensureGuild(prisma, guildId);
  return prisma.socialFeed.findMany({
    where: { guildId },
    orderBy: { createdAt: 'asc' }
  });
}

export async function getFeedById(
  prisma: BotContext['prisma'],
  guildId: string,
  feedId: string
): Promise<SocialFeed | null> {
  return prisma.socialFeed.findFirst({
    where: { id: feedId, guildId }
  });
}

function buildFeedMessage(feed: SocialFeed, item: FeedItem): string {
  const body = applyFeedTemplate(feed.template?.trim() || DEFAULT_TEMPLATE, {
    title: item.title,
    url: item.url
  });
  if (feed.rolePingId) {
    return `<@&${feed.rolePingId}>\n${body}`;
  }
  return body;
}

async function resolveTextChannel(
  context: BotContext,
  guildId: string,
  channelId: string
): Promise<TextChannel | null> {
  const guild = await context.client.guilds.fetch(guildId).catch(() => null);
  if (!guild) {
    return null;
  }

  const channel = await guild.channels.fetch(channelId).catch(() => null);
  if (!channel?.isTextBased() || channel.isDMBased()) {
    return null;
  }

  const me = guild.members.me;
  if (!me?.permissionsIn(channel).has(PermissionFlagsBits.SendMessages)) {
    return null;
  }

  return channel as TextChannel;
}

async function postFeedItem(
  context: BotContext,
  feed: SocialFeed,
  item: FeedItem
): Promise<boolean> {
  const channel = await resolveTextChannel(context, feed.guildId, feed.channelId);
  if (!channel) {
    logger.warn({ feedId: feed.id, channelId: feed.channelId }, 'Feed channel invalid');
    return false;
  }

  await channel.send({ content: buildFeedMessage(feed, item) });
  return true;
}

function selectNewItems(items: FeedItem[], lastItemId: string | null): FeedItem[] {
  if (items.length === 0) {
    return [];
  }

  if (!lastItemId) {
    return [items[0]!];
  }

  const lastIndex = items.findIndex((item) => item.id === lastItemId);
  if (lastIndex === -1) {
    return [items[0]!];
  }

  return items.slice(0, lastIndex).reverse();
}

export async function processFeedFetch(
  context: BotContext,
  feed: SocialFeed,
  result: FetchResult,
  options: { forceLatest?: boolean } = {}
): Promise<{ posted: number; skipped?: boolean; message?: string }> {
  if (!result.ok) {
    return {
      posted: 0,
      skipped: result.skipped,
      message: result.message
    };
  }

  const items = result.items;
  if (items.length === 0) {
    return { posted: 0 };
  }

  const candidates = options.forceLatest ? [items[0]!] : selectNewItems(items, feed.lastItemId);
  let posted = 0;

  for (const item of candidates) {
    const sent = await postFeedItem(context, feed, item);
    if (sent) {
      posted += 1;
    }
  }

  const newestId = items[0]!.id;
  if (posted > 0 || feed.lastItemId !== newestId) {
    await context.prisma.socialFeed.update({
      where: { id: feed.id },
      data: { lastItemId: newestId }
    });
  }

  return { posted };
}

export async function pollFeed(context: BotContext, feed: SocialFeed): Promise<void> {
  const result = await fetchFeedItems(feed.type, feed.sourceId);
  await processFeedFetch(context, feed, result);
}

export async function testFeed(
  context: BotContext,
  feed: SocialFeed
): Promise<{ posted: number; skipped?: boolean; message?: string }> {
  const result = await fetchFeedItems(feed.type, feed.sourceId);
  return processFeedFetch(context, feed, result, { forceLatest: true });
}

export async function pollAllFeeds(context: BotContext): Promise<void> {
  const feeds = await context.prisma.socialFeed.findMany({
    where: { enabled: true }
  });

  for (const feed of feeds) {
    try {
      await pollFeed(context, feed);
    } catch (error) {
      logger.error({ error, feedId: feed.id, guildId: feed.guildId }, 'Feed poll failed');
    }
  }
}
