import type { Guild, GuildMember, MessageCreateOptions } from 'discord.js';
import {
  applyTagPlaceholders,
  componentsV2HasContent,
  embedHasContent,
  parseMessageComponentsV2,
  TagResponseTypeSchema,
  WelcomeEmbedSchema,
  type MessageComponentsV2,
  type TagResponseType,
  type WelcomeEmbed
} from '@nexumi/shared';
import type { Tag } from '@prisma/client';
import { ensureGuild } from '../../guild.js';
import { buildEmbedFromPayload } from '../../lib/embed-payload.js';
import { buildComponentsV2Payload } from '../../lib/components-v2-payload.js';
import { redis } from '../../redis.js';
import type { BotContext } from '../../types.js';
import { assertPremiumLimit, PremiumLimitError } from '../../premium.js';
import { matchesTriggerWord, tagCooldownKey } from './tag-helpers.js';

export { matchesTriggerWord, tagCooldownKey } from './tag-helpers.js';

export class TagError extends Error {
  constructor(
    public readonly code: string,
    public readonly retryAfterSeconds?: number
  ) {
    super(code);
  }
}

/** Returns remaining cooldown seconds, or 0 if ready. */
export async function getTagCooldownRemaining(
  guildId: string,
  tagId: string,
  scopeId: string
): Promise<number> {
  const ttl = await redis.ttl(tagCooldownKey(guildId, tagId, scopeId));
  return ttl > 0 ? ttl : 0;
}

export async function applyTagCooldown(
  guildId: string,
  tagId: string,
  scopeId: string,
  cooldownSeconds: number
): Promise<void> {
  if (cooldownSeconds <= 0) {
    return;
  }
  await redis.set(tagCooldownKey(guildId, tagId, scopeId), '1', 'EX', cooldownSeconds);
}

export function normalizeTagName(name: string): string {
  return name.trim().toLowerCase();
}

export function parseIdList(input: string | null | undefined): string[] {
  if (!input?.trim()) {
    return [];
  }
  return input
    .split(',')
    .map((part) => part.trim())
    .filter(Boolean);
}

function applyRandomPlaceholders(template: string): string {
  return template.replace(/\{random:([^}]+)\}/g, (_match, group: string) => {
    const options = group.split('|').map((part) => part.trim()).filter(Boolean);
    if (options.length === 0) {
      return '';
    }
    return options[Math.floor(Math.random() * options.length)]!;
  });
}

export function buildTagPlaceholderVars(
  member: GuildMember | null,
  guild: Guild,
  args: string
): Record<string, string> {
  return {
    user: member ? `<@${member.id}>` : '',
    'user.name': member?.displayName ?? '',
    'user.tag': member?.user.tag ?? '',
    'user.id': member?.id ?? '',
    'user.avatar': member?.user.displayAvatarURL({ size: 256 }) ?? '',
    server: guild.name,
    'server.icon': guild.iconURL({ size: 128 }) ?? '',
    args
  };
}

export function renderTagContent(template: string, vars: Record<string, string>): string {
  const withRandom = applyRandomPlaceholders(template);
  return applyTagPlaceholders(withRandom, vars);
}

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

export type TagReplyPayload = MessageCreateOptions;

export function buildTagReply(
  tag: Tag,
  member: GuildMember | null,
  guild: Guild,
  args: string
): TagReplyPayload {
  const vars = buildTagPlaceholderVars(member, guild, args);
  const responseType = TagResponseTypeSchema.parse(tag.responseType);

  if (responseType === 'COMPONENTS_V2') {
    const components = parseMessageComponentsV2(tag.components);
    const payload = buildComponentsV2Payload(components, {
      source: 't',
      ref: tag.id,
      renderText: (value) => renderTagContent(value, vars)
    });
    return payload ?? { content: '—' };
  }

  if (responseType === 'EMBED') {
    const embedData = parseTagEmbed(tag.embed);
    const embed = buildEmbedFromPayload(embedData, {
      renderText: (value) => renderTagContent(value, vars)
    });
    return embed ? { embeds: [embed] } : { content: '—' };
  }

  const content = renderTagContent(tag.content ?? '', vars);
  return { content: content || '—' };
}

export function memberCanUseTag(tag: Tag, member: GuildMember, channelId: string): boolean {
  if (tag.allowedRoleIds.length > 0) {
    const hasRole = tag.allowedRoleIds.some((roleId) => member.roles.cache.has(roleId));
    if (!hasRole) {
      return false;
    }
  }

  if (tag.allowedChannelIds.length > 0 && !tag.allowedChannelIds.includes(channelId)) {
    return false;
  }

  return true;
}

export async function getTagByName(
  context: BotContext,
  guildId: string,
  name: string
): Promise<Tag | null> {
  return context.prisma.tag.findUnique({
    where: {
      guildId_name: {
        guildId,
        name: normalizeTagName(name)
      }
    }
  });
}

export async function createTag(
  context: BotContext,
  params: {
    guildId: string;
    name: string;
    responseType: TagResponseType;
    content?: string | null;
    embed?: WelcomeEmbed | null;
    components?: MessageComponentsV2 | null;
    triggerWord?: string | null;
    cooldownSeconds?: number;
    allowedRoleIds?: string[];
    allowedChannelIds?: string[];
    createdById: string;
  }
): Promise<Tag> {
  await ensureGuild(context.prisma, params.guildId);
  const name = normalizeTagName(params.name);

  if (!name || !/^[a-z0-9_-]{1,32}$/.test(name)) {
    throw new TagError('invalid_name');
  }

  if (params.responseType === 'TEXT' && !params.content?.trim()) {
    throw new TagError('content_required');
  }

  if (params.responseType === 'EMBED' && !embedHasContent(params.embed) && params.embed?.color === undefined) {
    throw new TagError('embed_required');
  }

  if (params.responseType === 'COMPONENTS_V2' && !componentsV2HasContent(params.components)) {
    throw new TagError('components_required');
  }

  const currentCount = await context.prisma.tag.count({ where: { guildId: params.guildId } });
  try {
    await assertPremiumLimit(context.prisma, params.guildId, 'tags', currentCount);
  } catch (error) {
    if (error instanceof PremiumLimitError) {
      throw new TagError(error.code === 'feature_disabled' ? 'premium_feature' : 'premium_limit');
    }
    throw error;
  }

  return context.prisma.tag.create({
    data: {
      guildId: params.guildId,
      name,
      responseType: params.responseType,
      content: params.content ?? null,
      embed: params.embed ?? undefined,
      components: params.components ?? undefined,
      triggerWord: params.triggerWord?.trim() || null,
      cooldownSeconds: params.cooldownSeconds ?? 15,
      allowedRoleIds: params.allowedRoleIds ?? [],
      allowedChannelIds: params.allowedChannelIds ?? [],
      createdById: params.createdById
    }
  });
}

export async function updateTag(
  context: BotContext,
  guildId: string,
  name: string,
  updates: {
    newName?: string;
    responseType?: TagResponseType;
    content?: string | null;
    embed?: WelcomeEmbed | null;
    components?: MessageComponentsV2 | null;
    triggerWord?: string | null;
    cooldownSeconds?: number;
    allowedRoleIds?: string[];
    allowedChannelIds?: string[];
  }
): Promise<Tag> {
  const existing = await getTagByName(context, guildId, name);
  if (!existing) {
    throw new TagError('not_found');
  }

  const responseType = updates.responseType ?? TagResponseTypeSchema.parse(existing.responseType);
  const content = updates.content !== undefined ? updates.content : existing.content;
  const embed = updates.embed !== undefined ? updates.embed : parseTagEmbed(existing.embed);
  const components =
    updates.components !== undefined
      ? updates.components
      : parseMessageComponentsV2(existing.components);

  if (responseType === 'TEXT' && !content?.trim()) {
    throw new TagError('content_required');
  }

  if (responseType === 'EMBED' && !embedHasContent(embed) && embed?.color === undefined) {
    throw new TagError('embed_required');
  }

  if (responseType === 'COMPONENTS_V2' && !componentsV2HasContent(components)) {
    throw new TagError('components_required');
  }

  let newName: string | undefined;
  if (updates.newName) {
    newName = normalizeTagName(updates.newName);
    if (!newName || !/^[a-z0-9_-]{1,32}$/.test(newName)) {
      throw new TagError('invalid_name');
    }
  }

  return context.prisma.tag.update({
    where: { id: existing.id },
    data: {
      name: newName ?? existing.name,
      responseType,
      content,
      embed: embed ?? undefined,
      components: components ?? undefined,
      triggerWord:
        updates.triggerWord !== undefined ? updates.triggerWord?.trim() || null : existing.triggerWord,
      cooldownSeconds:
        updates.cooldownSeconds !== undefined ? updates.cooldownSeconds : existing.cooldownSeconds,
      allowedRoleIds: updates.allowedRoleIds ?? existing.allowedRoleIds,
      allowedChannelIds: updates.allowedChannelIds ?? existing.allowedChannelIds
    }
  });
}

export async function deleteTag(context: BotContext, guildId: string, name: string): Promise<void> {
  const existing = await getTagByName(context, guildId, name);
  if (!existing) {
    throw new TagError('not_found');
  }
  await context.prisma.tag.delete({ where: { id: existing.id } });
}

export async function listTags(context: BotContext, guildId: string): Promise<Tag[]> {
  return context.prisma.tag.findMany({
    where: { guildId },
    orderBy: { name: 'asc' }
  });
}

export async function executeTag(
  context: BotContext,
  guildId: string,
  name: string,
  member: GuildMember | null,
  guild: Guild,
  channelId: string,
  args: string
): Promise<TagReplyPayload> {
  const tag = await getTagByName(context, guildId, name);
  if (!tag) {
    throw new TagError('not_found');
  }

  if (member && !memberCanUseTag(tag, member, channelId)) {
    throw new TagError('not_allowed');
  }

  const scopeId = member?.id ?? channelId;
  if (tag.cooldownSeconds > 0) {
    const remaining = await getTagCooldownRemaining(guildId, tag.id, scopeId);
    if (remaining > 0) {
      throw new TagError('cooldown', remaining);
    }
  }

  await context.prisma.tag.update({
    where: { id: tag.id },
    data: { useCount: { increment: 1 } }
  });

  await applyTagCooldown(guildId, tag.id, scopeId, tag.cooldownSeconds);

  return buildTagReply(tag, member, guild, args);
}

export async function findTriggeredTag(
  context: BotContext,
  guildId: string,
  content: string
): Promise<Tag | null> {
  const tags = await context.prisma.tag.findMany({
    where: {
      guildId,
      triggerWord: { not: null }
    }
  });

  for (const tag of tags) {
    if (tag.triggerWord && matchesTriggerWord(content, tag.triggerWord)) {
      return tag;
    }
  }

  return null;
}
