import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, Client, GatewayIntentBits, Message, Partials } from "discord.js";
import { readdirSync } from "fs";
import { CUser, EventSOn, KomandNaExport, Komand, ListenerFunkce, Modul, SRecord, SuperListenerFunkce, CustomKomandy } from "./utils/types";
import { adminLog, getFirstArg, formatCas, loadEnv, oddiakritikovat, sortArr, log } from "./utils/utils.js";
import levenshtein from "js-levenshtein";
import { emouty } from "./utils/emotes";

const helpServer = require("./utils/helpServer");
const custom: CustomKomandy = require("./utils/customCommands");

loadEnv();

const ints = GatewayIntentBits;
const client = new Client({
  partials: [Partials.Channel],
  intents: [ints.Guilds, ints.GuildVoiceStates, ints.GuildPresences, ints.GuildMessages, ints.DirectMessages, ints.MessageContent]
});
const prefix = process.env.prefix || "more";
const modulFolder = `${__dirname}/modules/`;
const eventy: SRecord<ListenerFunkce[]> = {};
const superEventy: SRecord<SuperListenerFunkce[]> = {};
const komandy: SRecord<Komand> = {};
const aliasy: SRecord<string> = {};
const kuldan_log: SRecord<SRecord<number>> = {};
const komandyNaPoslani: KomandNaExport[] = [];
let customKomandy: SRecord<Komand> = custom.cKomandy;
let customAliasy: SRecord<string> = custom.cAliasy;

custom.emitter.on("komandi", (komandi, aliasi, naPoslani) => {
  customKomandy = komandi;
  customAliasy = aliasi;
  zpracovatAliasyAKomandy(aliasi, naPoslani);
});

function zpracovatAliasyAKomandy(aliasi: SRecord<string>, naPoslani: KomandNaExport[]) {
  Object.keys(aliasi).forEach((a: string) => {
    const i = komandyNaPoslani.findIndex(k => k.name == aliasi[a]);
    if (!komandyNaPoslani[i]) return;
    const arr = komandyNaPoslani[i].als;
    if (arr && !arr.includes(a)) arr.push(a);
    if (!arr) komandyNaPoslani[i].als = [a];
  });
  helpServer.cmds = sortArr([...komandyNaPoslani, ...naPoslani]);
}

const runEvent = (name: string, args: unknown[]) => {
  for (const listener of superEventy[name] || []) {
    if (!listener) continue; // [after] pozice v superEventy arrayi se dají nastavovat a teoreticky může nastat mezera
    try {
      if (listener(...args)) return true; // if listener returns true, it means, we shouldn't continue with execustion
    } catch (e) {
      if (process.env.dieOnError) throw e;
      log("error pri spusteni super listeneru", e as Error);
      adminLog(client, "error pri supereventu");
    }
  }

  eventy[name]?.forEach(listener => {
    try {
      listener(...args);
    } catch (e) {
      if (process.env.dieOnError) throw e;
      log("error pri spousteni eventu", e as Error);
      adminLog(client, "error pri eventu");
    }
  });
};

// Loading of modules
readdirSync(modulFolder).forEach(soubor => {
  if (!soubor.endsWith(".js")) return;
  const modul: Modul = require(`${modulFolder}${soubor}`);
  log(`Loaded: ${modulFolder}${soubor}`);

  modul.client = client;
  Object.keys(modul).forEach(name => {
    const prefix = /^(?<s>super_)?on_(?<h>.+)/.exec(name);
    if (prefix) {
      const groups = prefix.groups!;
      const ev = groups.s ? superEventy : eventy;
      if (!ev[groups.h]) {
        ev[groups.h] = [];
        if (!["messageCreate", "userPresenceUpdate"].includes(groups.h)) client.on(groups.h, (...args) => void runEvent(groups.h, args));
      }

      const n = modul[name as EventSOn]!;
      if (typeof n == "object") {
        const prev = ev[groups.h][n.pos];
        ev[groups.h][n.pos] = n.fun;
        if (prev) ev[groups.h].push(prev);
      }
      else ev[groups.h].push(n);
    } else if (name === "more_komandy") {
      const n = modul[name]!;
      Object.keys(n).forEach(cmdName => {
        const value = n[cmdName];
        const toCoExportuju: KomandNaExport = { name: cmdName };
        let hide = false;
        if (typeof value !== "object") {
          komandy[cmdName] = { run: value };
          toCoExportuju.arg = getFirstArg(value);
        } else {
          komandy[cmdName] = { run: value.run, cd: value.cd, DMUnsafe: value.DMUnsafe };
          Object.assign(toCoExportuju, { als: value.als, arg: value.arg ?? getFirstArg(value.run) });
          hide = !!value.hidden;
          value.als?.forEach(al => aliasy[al] = cmdName);
        }
        if (!hide) komandyNaPoslani.push(toCoExportuju);
      });
    }
  });
});

custom.realKomandy = komandy;
custom.realAliasy = aliasy;
komandy["naucse"] = { run: custom.naucse };
komandy["zapomen"] = { run: custom.zapomen };
komandy["kohoje"] = { run: custom.kohoje };
komandy["naucsealias"] = { run: custom.naucsealias };
komandy["zapomenalias"] = { run: custom.zapomenalias };
zpracovatAliasyAKomandy(custom.cAliasy, custom.naSend);

const maKuldan = (id: string, komand: string, kuldan_komandu: number) => {
  if (!kuldan_log[komand]) kuldan_log[komand] = {};

  const cas_ted = Date.now() / 1000;
  const rozdil = cas_ted - kuldan_log[komand][id];
  if (rozdil < kuldan_komandu) return kuldan_komandu - rozdil;

  kuldan_log[komand][id] = cas_ted;
  return 0;
};

async function runKomand(mes: Message, cmd: Komand, cmdName: string, arg: string) {
  if (cmd.cd) {
    const zbyva = Math.round(maKuldan(mes.author.id, cmdName, cmd.cd));
    if (zbyva) return void mes.channel.send(`si kkt vole maz kuldan jeste ${formatCas(zbyva)}`);
  }

  const akce = cmd.run;
  if (typeof akce == "string") return void mes.channel.send(akce);
  try {
    const result = await akce(mes, arg);
    if (result) mes.channel.send(result);
  } catch (e) {
    if (process.env.dieOnError) throw e;
    log("error pri spousteni akce", e as Error);
    const admin = process.env.adminID;
    mes.channel.send(`pri spousteni thohoto komandu nastala chyba ${admin ? `<@${admin}> uz?` : ""}`);
  }
}

client.on("messageCreate", async mes => {
  if (process.env.ignoreMess) return;

  const [mesPrefix, komandSDiakritikou, ...args] = mes.content.split(" ");
  if (oddiakritikovat(mesPrefix.toLowerCase()) != prefix) return void runEvent("messageCreate", [mes]);
  if (!komandSDiakritikou) {
    if (!runEvent("messageCreate", [mes])) mes.channel.send("coe voe");
    return;
  }

  const celArgs = args.join(" ");
  const komandBez = oddiakritikovat(komandSDiakritikou).toLowerCase();
  const cmdName = aliasy[komandBez] ?? customAliasy[komandBez] ?? komandBez;

  if (runEvent("messageCreate", [mes, cmdName])) return;

  const komand = komandy[cmdName] ?? customKomandy[cmdName];
  if (mes.channel.type == ChannelType.DM && komand?.DMUnsafe) return void mes.channel.send("tuten komand bohuzel v sz nefunkuje");

  if (komand) return void runKomand(mes, komand, cmdName, celArgs);

  // neměl jsi na mysli?
  const slova: string[] = [];
  [...Object.keys(komandy), ...Object.keys(customKomandy), ...Object.keys(aliasy), ...Object.keys(customAliasy)].forEach(cmd => {
    const distance = levenshtein(cmd, cmdName);
    if (distance <= Math.ceil(cmdName.length * 0.3)) slova.push(cmd);
  });
  if (!slova.length) return void mes.channel.send("co to znamena ti gadzovko");

  const nabidka = slova.sort().slice(0, 5);
  const radek = new ActionRowBuilder<ButtonBuilder>();
  nabidka.forEach((cmd, i) => {
    const nazev = cmd.length > 80 ? `${cmd.slice(0, 77)}...` : cmd;
    radek.addComponents(new ButtonBuilder({ customId: `${i}`, label: nazev, style: ButtonStyle.Primary }));
  });

  const zprava = await mes.channel.send({ content: "nemnel sy na misli:", components: [radek] });
  const collector = mes.channel.createMessageComponentCollector({ filter: i => i.message.id == zprava.id && mes.author.id == i.user.id, time: 18e4 });
  collector.on("collect", i => {
    const lookupId = Number(i.customId);
    if (isNaN(lookupId)) return void mes.channel.send("neco nefunguje idk");
    const komand = nabidka[lookupId];
    const cmdName = aliasy[komand] ?? customAliasy[komand] ?? komand;
    const cmd = komandy[cmdName] ?? customKomandy[cmdName];
    radek.components.forEach(btn => btn.setDisabled());
    i.update({ content: `ok vole ${emouty.d3k}`, components: [radek] });
    runKomand(mes, cmd, cmdName, celArgs);
  });
  collector.on("end", c => {
    if (c.size) return;
    radek.components.forEach(btn => btn.setDisabled());
    zprava.edit({ content: "pozde", components: [radek] });
  });
});

// Simulation of userPresenceUpdate event
client.on("presenceUpdate", (bef, aft) => {
  const user = aft.user as CUser | null;
  if (!user) return;
  if (!user.presence) user.presence = {};

  if (JSON.stringify(aft.clientStatus) == JSON.stringify(user.presence)) return;
  user.presence = aft.clientStatus || {};

  runEvent("userPresenceUpdate", [bef, aft]);
});

client.login(process.env.token);
