import { ButtonStyle, ChannelType, Client, ClientPresenceStatusData, CommandInteraction, GatewayIntentBits, InteractionReplyOptions, Message, Partials, Presence } from "discord.js";
import { readdirSync } from "fs";
import { CClient, CUser, HelpServer, Komand, KomandNaExport, KomandRaw, ListenerFunkce, Modul, SMessage, SRecord, SuperListenerFunkce } from "./utils/types";
import { adminLog, areStatusesSame, formatCas, log, nabidni, oddiakritikovat, prefix, rand, messageReply } from "./utils/utils.js";
import levenshtein from "js-levenshtein";
import { emouty } from "./utils/emotes";
import { lidiCoMajDenimPremium, setClient } from "./utils/denim-Spravce";

const ints = GatewayIntentBits;
const client = new Client({
  partials: [Partials.Channel],
  intents: [ints.Guilds, ints.GuildVoiceStates, ints.GuildPresences, ints.GuildMessages, ints.DirectMessages, ints.MessageContent]
}) as CClient;
const DJ_PREFIX = "dj:";
const modulFolder = `${__dirname}/modules/`;
const eventy: SRecord<ListenerFunkce<any>[]> = {};
const superEventy: SRecord<SuperListenerFunkce<any>[]> = {};
const kuldan_log: SRecord<SRecord<number>> = {};
const komandyNaPoslani: KomandNaExport[] = [];
const helpServer: HelpServer = require("./utils/helpServer");

client.on("error", err => {
  log("errorek", err);
  adminLog(client, "nejaka chyba na klijentovi", err);
});

client.on("shardError", err => {
  log(err);
  adminLog(client, "šárd ejrr??", err);
});

process.on("unhandledRejection", (reason, promise) => {
  let message: string;

  if (reason instanceof Error) {
    message = `${reason.name}: ${reason.message}\n${reason.stack}`;
  } else {
    message = String(reason);
  }

  if (message.includes("Connect Timeout Error") && message.includes("10000ms")) {
    // Klasika
    log(new Error("Discord timeout..."));
    return;
  }

  log(new Error("Unhandled Rejection in app:"), reason, promise);

  adminLog(client, "Unhandled Rejection", message);
});

process.on("uncaughtException", (err) => {
  log(new Error("Uncaught Exception in app:"), err);

  let message: string;
  if (err instanceof Error) {
    message = `${err.name}: ${err.message}\n${err.stack}`;
  } else {
    message = String(err);
  }

  adminLog(client, "Uncaught Exception", message);
});

client.komandy = {};
client.aliasy = {};
setClient(client);

function getFirstArg(fn: KomandRaw["run"]) {
  if (typeof fn != "function") return;
  return fn.toString()
    .match(/(?:function\s.*?)?\(([^)]*)\)|\w+ =>/)![1]
    ?.split(",")[1]
    ?.trim();
}

const runEvent = async (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 (await 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", e as Error);
    }
  }

  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", e as Error);
    }
  });
};

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

  modul.client = client;
  for (const klic in modul) {
    const prefix = /^(?<s>super_)?on_(?<h>.+)/.exec(klic);
    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));
      }

      // Bohužel typescript je tupá píča a musíme mu vysvětlit, že to vlastně jde
      const n = modul[klic as Exclude<keyof Modul, "client" | "more_komandy">]!;
      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 (klic == "more_komandy") {
      const n = modul[klic]!;
      Object.keys(n).forEach(cmdName => {
        const value = n[cmdName];
        const toCoExportuju: KomandNaExport = { name: cmdName };
        let hide = false;
        if (typeof value !== "object") {
          client.komandy[cmdName] = { run: value };
          toCoExportuju.arg = getFirstArg(value);
        } else {
          client.komandy[cmdName] = { run: value.run, slashRun: value.slashRun, cd: value.cd, DMUnsafe: value.DMUnsafe, premium: value.premium };
          Object.assign(toCoExportuju, { als: value.als, arg: value.arg ?? getFirstArg(value.run) });
          hide = !!value.hidden;
          value.als?.forEach(al => client.aliasy[al] = cmdName);
        }
        if (!hide) komandyNaPoslani.push(toCoExportuju);
      });
    }
  }
});

helpServer.komandy = komandyNaPoslani;

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;
};

function assume(cond: boolean, msg: string) {
  if (!cond) throw msg;
}

function prase(expression: string, i: number, terminators: string[], args: string[]): [string, number, string?] {
  let ret = "";
  let escaped = false;

  while (i < expression.length) {
    const char = expression[i];
    i++;

    if (escaped) {
      escaped = false;
      ret += char;
      continue;
    }

    switch (char) {
      case "~": // eskejp
        escaped = true;
        break;

      case "[": { // RNG vyber
        let option: string, term: string | undefined;
        const options: string[] = [];

        do {
          [option, i, term] = prase(expression, i, ["|", "]"], args);
          options.push(option);
        } while (term == "|");

        if (options.length > 0)
          ret += options[rand(options.length)];

        break;
      }

      case "<": {  // RNG cislo
        let min: string, max: string;
        [min, i] = prase(expression, i, [","], args);
        [max, i] = prase(expression, i, [">"], args);

        const imin = parseInt(min);
        const imax = parseInt(max);

        assume(!Number.isNaN(min), "RNG cislo: Min neni integer");
        assume(!Number.isNaN(max), "RNG cislo: Max neni integer");

        ret += rand(imax - imin + 1) + imin;

        break;
      }

      case "{": {// ARG
        let argId: string;
        [argId, i] = prase(expression, i, ["}"], args);

        if (argId === "*") { // join args
          ret += args.join(" ");
        } else if (argId === "#") { // number of args
          ret += args.length;
        } else { // arg id #
          const numArgId = parseInt(argId);
          assume(!Number.isNaN(numArgId) && numArgId > 0 && numArgId <= args.length, "ARG: Neplatnej index argumentu");
          ret += args[numArgId - 1];
        }

        break;
      }

      default:
        for (const terminator of terminators) {
          if (terminator == char)
            return [ret, i, char];
        }

        ret += char;
        break;
    }
  }

  assume(terminators.length == 0, "Neocekavanej konec vejrazu");

  return [ret, expression.length - 1];
}

function renderMessage(expression: string, args: string[]) {
  if (!expression.toLowerCase().startsWith(DJ_PREFIX))
    return expression;

  expression = expression.substring(DJ_PREFIX.length);

  try {
    return prase(expression, 0, [], args)[0];
  } catch (ex) {
    return `Chyba: ${ex}`;
  }
}

function handle(e: unknown, mesOrInt: SMessage | CommandInteraction) {
  if (process.env.dieOnError) throw e;
  log("error pri spousteni akce", e as Error);
  const admin = process.env.adminID;

  const txt = `pri spousteni thohoto komandu nastala chyba ${admin ? `<@${admin}> uz?` : ""}`;
  if (mesOrInt instanceof Message) return void messageReply(mesOrInt, txt);
  mesOrInt.reply(txt);
}

async function runKomand(mes: SMessage, cmd: Komand, cmdName: string, arg: string): Promise<unknown>;
async function runKomand(interaction: CommandInteraction, cmd: Komand, cmdName: string): Promise<unknown>;
async function runKomand(mesOrInt: SMessage | CommandInteraction, cmd: Komand, cmdName: string, arg?: string) {
  const jeMes = mesOrInt instanceof Message;

  if (jeMes && cmd.slashRun) {
    const nazev = `${prefix}${cmdName}`;
    const id = client.slashCommandy[nazev];
    return void messageReply(mesOrInt, `tuto ejenom sleh komand </${nazev}:${id}>`);
  }

  if (cmd.cd && jeMes) {
    const zbyva = Math.round(maKuldan(mesOrInt.author.id, cmdName, cmd.cd));
    if (zbyva) return messageReply(mesOrInt, `si kkt vole maz kuldan jeste ${formatCas(zbyva)}`);
  }

  if (cmd.premium && !lidiCoMajDenimPremium.includes(jeMes ? mesOrInt.author.id : mesOrInt.member?.user.id || "")) {
    return messageReply(mesOrInt as SMessage, "sorka bracho tuto je koamnd jenom pro curaki co platy");
  }

  const akce = cmd.run ?? cmd.slashRun!;

  if (typeof akce == "string") {
    const res = renderMessage(akce, arg?.split(" ") || []);
    if (jeMes) return messageReply(mesOrInt, res);
    return mesOrInt.reply(res);
  }

  try {
    // Je to absolutní fekálie, ale alespoň to je kompilovatelný narozdíl od předchozí verze (commit 08fde76 a předchozí)
    const result = await akce(mesOrInt as never, arg || "");
    if (!result) return;

    if (jeMes) return messageReply(mesOrInt, result)
      .catch(e => handle(e, mesOrInt));
    // další feklo
    mesOrInt.reply(result as InteractionReplyOptions);
  } catch (e) {
    handle(e, mesOrInt);
  }
}

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

  const [mesPrefix, komandSDiakritikou, ...args] = mes.content.split(/\s/);

  const prefixDiff = levenshtein(oddiakritikovat(mesPrefix.toLowerCase()), prefix);
  if (prefixDiff > 0) {
    if (!await runEvent("messageCreate", [mes]) && prefixDiff <= 1 && mes.author.id != client.user!.id) messageReply(mes, `${prefix}*`);
    return;
  }

  if (!komandSDiakritikou) {
    if (!await runEvent("messageCreate", [mes])) messageReply(mes, "coe voe");
    return;
  }

  const komandBez = oddiakritikovat(komandSDiakritikou).toLowerCase();
  let cmdName = komandBez;

  const aliasCelej = client.aliasy[komandBez];
  if (aliasCelej) {
    const [aliasCmdName, ...aliasArgs] = aliasCelej.split(" ");
    cmdName = aliasCmdName;
    args.unshift(...aliasArgs);
  }

  const celArgs = args.join(" ");

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

  const komand = client.komandy[cmdName];
  if (mes.channel.type == ChannelType.DM && komand?.DMUnsafe) return void messageReply(mes, "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(client.komandy), ...Object.keys(client.aliasy)].forEach(cmd => {
    const distance = levenshtein(cmd, cmdName);
    if (distance <= Math.ceil(cmdName.length * 0.3)) slova.push(cmd);
  });
  if (!slova.length) return void messageReply(mes, "co to znamena ti gadzovko");

  const nabidka = slova.sort().slice(0, 5);

  nabidni({
    nabidka,
    channel: mes.channel,
    zpravec: { content: "nemnel sy na misli:" },
    onCollect: (i, lookupId, radek) => {
      if (i.member?.user.id != mes.author.id) return;
      const komand = nabidka[lookupId];
      const cmdName = client.aliasy[komand] ?? komand;
      const cmd = client.komandy[cmdName];
      radek.components.forEach((btn, i) => btn.setDisabled() && (i == lookupId && btn.setStyle(ButtonStyle.Success)));
      i.update({ content: `ok vole ${emouty.d3k}`, components: [radek] });

      if (!cmd) return void messageReply(mes, "bohuzel negdo sy ze mi dela prel");

      if (mes.channel.type == ChannelType.DM && cmd?.DMUnsafe) return void messageReply(mes, "tuten komand bohuzel v sz nefunkuje");

      runKomand(mes, cmd, cmdName, celArgs);
    },
    konecnaZprava: "pozde"
  });
});

const expectedOrSeenStatuses = ["web", "mobile", "desktop"];

function alertPokudNestandardni(client: CClient, aft: Presence) {
  const presence = aft.clientStatus;

  for (const pres in presence) {
    if (expectedOrSeenStatuses.includes(pres)) {
      continue;
    }

    // Máme jinej
    adminLog(client, `zahlidnul jsem zvlastni status "${pres}".\n\`\`\`${JSON.stringify(presence)}\`\`\`\nod uzivatele ${aft.userId} ${aft.user}`);

    expectedOrSeenStatuses.push(pres);
  }
}

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

  if (areStatusesSame(aft.clientStatus, user.presence)) return;

  alertPokudNestandardni(client, aft);

  const naCo = aft.clientStatus || {};
  user.presence = naCo;

  await new Promise(r => setTimeout(r, 1000));

  if (!areStatusesSame(naCo, user.presence)) {
    return;
  }

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

client.on("interactionCreate", async int => {
  if (process.env.ignoreMess || !int.isChatInputCommand()) return;

  const cmdName = int.commandName.slice(prefix.length);

  const command = client.komandy[cmdName];
  if (!command) return void int.reply(`komand nenalezen ${emouty.lukiw}`);

  runKomand(int, command, cmdName);
});

client.login(process.env.token);
