import type { OwnerGuildListItem } from '@nexumi/shared';
import { discordBotFetch } from './discord-guild-resources';
import { redis } from './redis';

const META_CACHE_TTL_SECONDS = 300;
const INVITE_CHANNEL_TYPES = new Set([0, 2, 5, 13]); // text, voice, announcement, stage

interface DiscordGuildRest {
  id: string;
  name: string;
  icon: string | null;
  system_channel_id?: string | null;
  approximate_member_count?: number;
}

interface DiscordChannelRest {
  id: string;
  name: string;
  type: number;
  position?: number;
}

interface DiscordInviteRest {
  code: string;
  channel?: { id: string };
}

export type OwnerGuildDiscordMeta = {
  id: string;
  name: string;
  icon: string | null;
  iconUrl: string | null;
  memberCount: number | null;
};

function guildIconUrl(guildId: string, icon: string | null): string | null {
  if (!icon) {
    return null;
  }
  const ext = icon.startsWith('a_') ? 'gif' : 'png';
  return `https://cdn.discordapp.com/icons/${guildId}/${icon}.${ext}?size=64`;
}

export async function getOwnerGuildDiscordMeta(guildId: string): Promise<OwnerGuildDiscordMeta | null> {
  const cacheKey = `owner:guild:${guildId}:meta:v1`;
  const cached = await redis.get(cacheKey);
  if (cached) {
    return JSON.parse(cached) as OwnerGuildDiscordMeta;
  }

  try {
    const guild = await discordBotFetch<DiscordGuildRest>(`/guilds/${guildId}?with_counts=true`);
    const meta: OwnerGuildDiscordMeta = {
      id: guild.id,
      name: guild.name,
      icon: guild.icon,
      iconUrl: guildIconUrl(guild.id, guild.icon),
      memberCount: guild.approximate_member_count ?? null
    };
    await redis.set(cacheKey, JSON.stringify(meta), 'EX', META_CACHE_TTL_SECONDS);
    return meta;
  } catch {
    return null;
  }
}

async function mapPool<T, R>(items: T[], concurrency: number, fn: (item: T) => Promise<R>): Promise<R[]> {
  const results = new Array<R>(items.length);
  let nextIndex = 0;

  async function worker(): Promise<void> {
    while (nextIndex < items.length) {
      const index = nextIndex;
      nextIndex += 1;
      results[index] = await fn(items[index]!);
    }
  }

  const workers = Array.from({ length: Math.min(concurrency, Math.max(items.length, 1)) }, () => worker());
  await Promise.all(workers);
  return results;
}

export async function enrichOwnerGuildListItems(
  base: Array<{
    id: string;
    locale: string;
    createdAt: string;
    blacklisted: boolean;
  }>
): Promise<OwnerGuildListItem[]> {
  const metas = await mapPool(base, 8, (guild) => getOwnerGuildDiscordMeta(guild.id));
  return base.map((guild, index) => {
    const meta = metas[index];
    return {
      id: guild.id,
      name: meta?.name ?? null,
      iconUrl: meta?.iconUrl ?? null,
      memberCount: meta?.memberCount ?? null,
      locale: guild.locale,
      createdAt: guild.createdAt,
      blacklisted: guild.blacklisted
    };
  });
}

export async function createOwnerGuildInvite(guildId: string): Promise<{
  url: string;
  code: string;
  channelId: string;
}> {
  const guild = await discordBotFetch<DiscordGuildRest>(`/guilds/${guildId}?with_counts=true`);
  const channels = await discordBotFetch<DiscordChannelRest[]>(`/guilds/${guildId}/channels`);
  const candidates = channels
    .filter((channel) => INVITE_CHANNEL_TYPES.has(channel.type))
    .sort((a, b) => (a.position ?? 0) - (b.position ?? 0));

  const ordered = [
    ...candidates.filter((channel) => channel.id === guild.system_channel_id),
    ...candidates.filter((channel) => channel.id !== guild.system_channel_id)
  ];

  let lastError: Error | null = null;
  for (const channel of ordered) {
    try {
      const invite = await discordBotFetch<DiscordInviteRest>(`/channels/${channel.id}/invites`, {
        method: 'POST',
        body: {
          max_age: 86_400,
          max_uses: 5,
          unique: true
        }
      });
      return {
        code: invite.code,
        url: `https://discord.gg/${invite.code}`,
        channelId: channel.id
      };
    } catch (error) {
      lastError = error instanceof Error ? error : new Error(String(error));
    }
  }

  throw lastError ?? new Error('Could not create an invite for this guild');
}
