import {
  AttachmentBuilder,
  ContainerBuilder,
  EmbedBuilder,
  MessageFlags,
  PermissionFlagsBits,
  TextDisplayBuilder,
  type Guild,
  type GuildMember,
  type MessageCreateOptions,
  type User,
} from "discord.js";

import type { I18nService } from "../../i18n/index.js";
import type { SupportedLang } from "../../types/command.js";
import type {
  AssignWelcomeAutoRolesInput,
  AssignWelcomeAutoRolesResult,
  DispatchMemberMessageInput,
  DispatchMemberMessageResult,
  MemberMessageConfig,
  MemberMessageKind,
  MemberMessageRenderType,
  SendableChannel,
  TemplateSuffix,
} from "../../types/memberMessages.js";
import { sanitizeMemberMessageRoleIds } from "../../validators/memberMessages.js";
import type { MemberMessageRepository } from "./repository.js";

export type {
  DispatchMemberMessageFailureReason,
  DispatchMemberMessageResult,
} from "../../types/memberMessages.js";

const hasSendMethod = (value: unknown): value is SendableChannel => {
  if (!value || typeof value !== "object") {
    return false;
  }

  return (
    "send" in value && typeof (value as { send?: unknown }).send === "function"
  );
};

const hasCode = (error: unknown): error is { code: number } => {
  if (!error || typeof error !== "object") {
    return false;
  }

  return (
    "code" in error && typeof (error as { code?: unknown }).code === "number"
  );
};

const messageTemplateVars = (
  guild: Guild,
  user: User,
): Record<string, string> => ({
  user: `<@${user.id}>`,
  username: user.username,
  guild: guild.name,
});

const messageColor = (kind: MemberMessageKind): number => {
  return kind === "welcome" ? 0x57f287 : 0xed4245;
};

const resolveTemplate = (
  i18n: I18nService,
  lang: SupportedLang,
  kind: MemberMessageKind,
  suffix: TemplateSuffix,
  vars: Record<string, string>,
): string => {
  const key = `commands.${kind}.templates.${suffix}`;
  return i18n.t(lang, key, vars);
};

const resolveNonEmptyTemplate = (
  i18n: I18nService,
  lang: SupportedLang,
  kind: MemberMessageKind,
  suffix: TemplateSuffix,
  vars: Record<string, string>,
): string => {
  const resolved = resolveTemplate(i18n, lang, kind, suffix, vars).trim();
  if (resolved.length > 0) {
    return resolved;
  }

  const key = `commands.${kind}.templates.${suffix}`;
  const fallback = i18n.t("en", key, vars).trim();
  return fallback.length > 0 ? fallback : key;
};

const resolveAssignableRoleId = async (
  member: GuildMember,
  roleId: string,
): Promise<string | null> => {
  if (roleId === member.guild.id) {
    return null;
  }

  const role =
    member.guild.roles.cache.get(roleId) ??
    (await member.guild.roles.fetch(roleId).catch(() => null));
  if (!role || !role.editable) {
    return null;
  }

  return role.id;
};

const buildMemberMessagePayload = async (
  i18n: I18nService,
  lang: SupportedLang,
  kind: MemberMessageKind,
  renderType: MemberMessageRenderType,
  guild: Guild,
  user: User,
): Promise<MessageCreateOptions> => {
  const vars = messageTemplateVars(guild, user);
  const allowedMentions: NonNullable<MessageCreateOptions["allowedMentions"]> =
    {
      parse: [],
      users: [user.id],
    };

  if (renderType === "simple") {
    return {
      content: resolveTemplate(i18n, lang, kind, "simple", vars),
      allowedMentions,
    };
  }

  if (renderType === "embed") {
    return {
      allowedMentions,
      embeds: [
        new EmbedBuilder()
          .setColor(messageColor(kind))
          .setTitle(resolveTemplate(i18n, lang, kind, "embedTitle", vars))
          .setDescription(
            resolveTemplate(i18n, lang, kind, "embedDescription", vars),
          ),
      ],
    };
  }

  if (renderType === "container") {
    const container = new ContainerBuilder();
    container.addTextDisplayComponents(
      new TextDisplayBuilder().setContent(
        `## ${resolveTemplate(i18n, lang, kind, "containerTitle", vars)}\n${resolveTemplate(i18n, lang, kind, "containerDescription", vars)}`,
      ),
    );

    return {
      flags: MessageFlags.IsComponentsV2,
      components: [container],
      allowedMentions,
    };
  }

  if (renderType !== "image") {
    return {
      content: resolveTemplate(i18n, lang, kind, "simple", vars),
      allowedMentions,
    };
  }

  const imageTitle = resolveNonEmptyTemplate(
    i18n,
    lang,
    kind,
    "imageTitle",
    vars,
  );
  const imageSubtitle = resolveNonEmptyTemplate(
    i18n,
    lang,
    kind,
    "imageDescription",
    vars,
  );

  try {
    const { renderMemberMessageImage } = await import("./imageRenderer.js");

    const imageBuffer = await renderMemberMessageImage({
      kind,
      title: imageTitle,
      subtitle: imageSubtitle,
      username: user.globalName ?? user.username,
      avatarUrl: user.displayAvatarURL({ extension: "png", size: 512 }),
    });

    const fileName = `${kind}-${guild.id}-${user.id}.png`;

    return {
      allowedMentions,
      content: resolveTemplate(i18n, lang, kind, "simple", vars),
      files: [new AttachmentBuilder(imageBuffer, { name: fileName })],
    };
  } catch (error) {
    console.warn(
      "[memberMessages] image renderer unavailable, falling back to text",
      error,
    );
    return {
      content: resolveTemplate(i18n, lang, kind, "simple", vars),
      allowedMentions,
    };
  }
};

export class MemberMessageService {
  public constructor(private readonly repository: MemberMessageRepository) {}

  public resolveBotId(
    client: DispatchMemberMessageInput["client"],
  ): string | null {
    return client.user?.id ?? null;
  }

  public async getConfig(
    botId: string,
    guildId: string,
    kind: MemberMessageKind,
  ) {
    return this.repository.getByBotGuildKind(botId, guildId, kind);
  }

  public async saveConfig(
    botId: string,
    guildId: string,
    kind: MemberMessageKind,
    config: MemberMessageConfig,
  ): Promise<void> {
    await this.repository.upsertByBotGuildKind(botId, guildId, kind, config);
  }

  public async cleanupGuild(botId: string, guildId: string): Promise<void> {
    await this.repository.deleteByBotGuild(botId, guildId);
  }

  public async assignWelcomeAutoRoles(
    input: AssignWelcomeAutoRolesInput,
  ): Promise<AssignWelcomeAutoRolesResult> {
    const botId = this.resolveBotId(input.client);
    if (!botId) {
      return {
        assigned: false,
        reason: "bot_not_ready",
        configuredRoleIds: [],
        appliedRoleIds: [],
        skippedRoleIds: [],
      };
    }

    const config = await this.repository.getByBotGuildKind(
      botId,
      input.member.guild.id,
      "welcome",
    );
    const configuredRoleIds = sanitizeMemberMessageRoleIds(config.autoRoleIds);

    if (configuredRoleIds.length === 0) {
      return {
        assigned: false,
        reason: "no_roles_configured",
        configuredRoleIds,
        appliedRoleIds: [],
        skippedRoleIds: [],
      };
    }

    const me = input.member.guild.members.me;
    if (!me || !me.permissions.has(PermissionFlagsBits.ManageRoles)) {
      return {
        assigned: false,
        reason: "missing_permissions",
        configuredRoleIds,
        appliedRoleIds: [],
        skippedRoleIds: [...configuredRoleIds],
      };
    }

    if (!input.member.manageable) {
      return {
        assigned: false,
        reason: "member_not_manageable",
        configuredRoleIds,
        appliedRoleIds: [],
        skippedRoleIds: [...configuredRoleIds],
      };
    }

    const resolvedRoleIds = await Promise.all(
      configuredRoleIds.map((roleId) =>
        resolveAssignableRoleId(input.member, roleId),
      ),
    );

    const appliedRoleIds = resolvedRoleIds.filter(
      (roleId): roleId is string => roleId !== null,
    );
    const skippedRoleIds = configuredRoleIds.filter(
      (roleId) => !appliedRoleIds.includes(roleId),
    );

    if (appliedRoleIds.length === 0) {
      return {
        assigned: false,
        reason: "no_assignable_roles",
        configuredRoleIds,
        appliedRoleIds: [],
        skippedRoleIds,
      };
    }

    try {
      await input.member.roles.add(appliedRoleIds, "welcome auto roles");
      return {
        assigned: true,
        reason: "assigned",
        configuredRoleIds,
        appliedRoleIds,
        skippedRoleIds,
      };
    } catch (error) {
      if (hasCode(error) && error.code === 50013) {
        return {
          assigned: false,
          reason: "missing_permissions",
          configuredRoleIds,
          appliedRoleIds: [],
          skippedRoleIds: [...configuredRoleIds],
        };
      }

      return {
        assigned: false,
        reason: "assign_failed",
        configuredRoleIds,
        appliedRoleIds: [],
        skippedRoleIds: [...configuredRoleIds],
      };
    }
  }

  public async dispatch(
    input: DispatchMemberMessageInput,
  ): Promise<DispatchMemberMessageResult> {
    const botId = this.resolveBotId(input.client);
    if (!botId) {
      return {
        sent: false,
        reason: "bot_not_ready",
        channelId: null,
      };
    }

    const config = await this.repository.getByBotGuildKind(
      botId,
      input.guild.id,
      input.kind,
    );

    if (!input.ignoreEnabled && !config.enabled) {
      return {
        sent: false,
        reason: "disabled",
        channelId: config.channelId,
      };
    }

    if (!config.channelId) {
      return {
        sent: false,
        reason: "missing_channel",
        channelId: null,
      };
    }

    const channel = await input.guild.channels
      .fetch(config.channelId)
      .catch(() => null);
    if (!channel) {
      return {
        sent: false,
        reason: "channel_not_found",
        channelId: config.channelId,
      };
    }

    if (!hasSendMethod(channel)) {
      return {
        sent: false,
        reason: "channel_not_sendable",
        channelId: config.channelId,
      };
    }

    const me = input.guild.members.me;
    if (
      me &&
      "permissionsFor" in channel &&
      typeof channel.permissionsFor === "function"
    ) {
      const permissions = channel.permissionsFor(me);
      if (
        !permissions ||
        !permissions.has(PermissionFlagsBits.ViewChannel) ||
        !permissions.has(PermissionFlagsBits.SendMessages)
      ) {
        return {
          sent: false,
          reason: "missing_permissions",
          channelId: config.channelId,
        };
      }
    }

    const lang = input.i18n.resolveLang(input.guild.preferredLocale ?? null);
    const payload = await buildMemberMessagePayload(
      input.i18n,
      lang,
      input.kind,
      config.messageType,
      input.guild,
      input.user,
    );

    try {
      await channel.send(payload);
      return {
        sent: true,
        reason: "sent",
        channelId: config.channelId,
      };
    } catch (error) {
      if (hasCode(error) && error.code === 50013) {
        return {
          sent: false,
          reason: "missing_permissions",
          channelId: config.channelId,
        };
      }

      return {
        sent: false,
        reason: "send_failed",
        channelId: config.channelId,
      };
    }
  }
}
