// Modul dedikovaný funkci spinkáček

import { ChannelType, Role, TextBasedChannel, User } from "discord.js";
import { emouty } from "../utils/emotes";
import { Modul, SRecord } from "../utils/types";
import { formatCas, formatter, log, oddiakritikovat, ping, safeQuery, messageReply, sendDM, sendWithoutReply } from "../utils/utils";

const enum Spinky {
  spinkacek,
  vstavacek,
  fakeVstavacek
}

const budouciSpinky: SRecord<NodeJS.Timeout> = {};
const autoSpinky: SRecord<[boolean, TextBasedChannel]> = {};

const contactSpinkServer = async (druh: Spinky, user: User) => {

  handleSpink(druh, user);

  const sql = druh == Spinky.spinkacek
    ? "INSERT INTO spinkacky (discord_id) VALUE (?)"
    : `UPDATE spinkacky SET vstavacek = curtime(), fejk = ${druh == Spinky.fakeVstavacek ? 1 : 0} WHERE discord_id = ? ORDER BY id DESC LIMIT 1`;

  const odpoved = await safeQuery(sql, [user.id]);

  return odpoved.err;
};

const ifUzRemove = (id: string) => {
  const uzPgn = budouciSpinky[id];
  if (uzPgn) {
    clearTimeout(uzPgn);
    delete budouciSpinky[id];
    return true;
  }
  return false;
};

async function getLastEntry(id: string) {
  const odpoved = await safeQuery<{ spinkacek: Date; vstavacek: Date | undefined; }>(`SELECT spinkacek, vstavacek FROM spinkacky WHERE discord_id = ? ORDER BY spinkacek DESC LIMIT 1; `, [id]);

  if (odpoved.err || !odpoved.data.length) return false;
  return odpoved.data[0];
}

/*
                              R.I.P.
  async tovjemamvolepreceroliky(roliky: User, rictCas?: boolean)
                  * 20. 11. 2021  ✝ 19. 11. 2023
                            Vzpomínáme!
*/
const sendGmMessage = async (user: User, jeFake?: boolean) => {
  const odpoved = await contactSpinkServer(jeFake ? Spinky.vstavacek : Spinky.fakeVstavacek, user);
  if (odpoved) return "uz jsi vzhuru ty hajzle";

  const zapis = await getLastEntry(user.id);

  const zacatek = `dobry rano hajzle ${user}`;
  if (!jeFake || zapis === false) return zacatek;

  const cas = (Number(zapis.vstavacek ?? Date.now()) - Number(zapis.spinkacek)) / 1000;
  const formatedCas = formatCas(cas);

  let zpr = "";
  if (cas >= 57600) zpr = "mas dat more vstavacek uz kdyz vstanes retarde";
  else if (cas >= 43200) zpr = "extrémní bídák";
  else if (cas >= 36000) zpr = "fakt bídák";
  else if (cas < 30) zpr = "dobrej mikrospánek debile";
  else if (cas < 10800) zpr = "dobrej fake spink debile";

  return `${zacatek}\nspal sy ${formatedCas}\n${zpr}`;
};

function handleSpink(stav: Spinky, uzivatel: User) {
  for (const [, guild] of uzivatel.client.guilds.cache) {
    const member = guild.members.cache.get(uzivatel.id);
    if (!member) continue;

    const c = guild.roles.cache;
    const spinkRole = c.find(r => r.name == "spink");
    const adminRole = c.find(r => r.name == "admin");

    const make = (akce: "a" | "r", role: Role) => {
      const handle = (e: Error) => {
        if (["Missing Permissions", "Missing Access"].includes(e.message)) return;
        log("chyba pri davani/odebirani role", e);
      };
      if (akce == "a") member.roles.add(role).catch(handle);
      else member.roles.remove(role).catch(handle);
    };

    if (stav == Spinky.spinkacek) {
      if (spinkRole) make("a", spinkRole);
      if (adminRole) make("r", adminRole);
    } else {
      if (spinkRole) make("r", spinkRole);
      if (adminRole) make("a", adminRole);
      continue;
    }

    for (const [, channel] of guild.channels.cache) {
      if (channel.type != ChannelType.GuildVoice) continue;
      for (const [, member] of channel.members) {
        if (member.id != uzivatel.id) continue;
        member.voice.disconnect("spinkacek");
        return false;
      }
    }
  }
}

/**
 * Zpracuje komplexní string pro pgn a vrátí počet milisekund, o kolik v budoucnu autor zamýšlel
 * @param str
 * @returns array, kde první informace je, zda-li byla informace relativní nebo absolutní a druhá je počet milisekund v budoucnoti
 */
function strToDate(str: string): ["rel" | "abs", number] | never[] {
  const cas = new Date();
  const dnes = new Date();
  let neco: RegExpExecArray | null;

  if (str == "") return [];

  // Relativní čas
  neco = /^(?:(?<h>\d+)h)?(?:(?<m>\d+)m)?(?:(?<s>\d+)s?)?$/.exec(str.toLowerCase());
  if (neco) {
    const g = neco.groups!;
    return ["rel", ((Number(g.h) * 3600 || 0) + (Number(g.m) * 60 || 0) + (Number(g.s) || 0)) * 1000] as const;
  }

  // Absolutní čas
  neco = /^((?<d>\d+)\. ?((?<mo>\d+)\.)? ?)?(?<h>\d+)(:(?<m>\d+)(:(?<s>\d+))?)? ?(?<p>am|pm)?$/.exec(str.toLowerCase());
  if (neco) {
    const g = neco.groups!;
    if (typeof g.m == "undefined" && typeof g.p == "undefined") return [];
    if (g.d) cas.setDate(Number(g.d));
    if (g.mo) cas.setMonth(Number(g.mo) - 1);
    const h = Number(g.h);
    cas.setHours(g.p == "pm" && h < 12 ? h + 12 : h);
    cas.setMinutes(Number(g.m) || 0);
    cas.setSeconds(Number(g.s) || 0);
    if (Number(cas) < Number(dnes)) cas.setDate(dnes.getDate() + 1);
    return ["abs", Number(cas) - Number(dnes)] as const;
  }
  return [];
}

const exp: Modul = {
  more_komandy: {

    spinkacek: {
      als: ["spink", "spoink", "spinkake", "spoinkacek", "gn", emouty.spinkacek, emouty.gn],
      run: async mes => {
        if (mes.author.bot) return `až někdy${emouty.kapp}`;
        const odpoved = await contactSpinkServer(Spinky.spinkacek, mes.author);

        if (!odpoved) {
          mes.react(emouty.spinkacek);
          ifUzRemove(mes.author.id);
        }
        else return 'nespis uz?????';
      }
    },

    vstavacek: {
      als: ["vstavcacek", "gm", "unspinkacek"],
      arg: "fejk (nepovinné)",
      run: async (mes, arg) => {
        if (mes.author.bot) return emouty.sjeta;

        return sendGmMessage(mes.author, arg.toLowerCase() != "fejk");
      }
    },

    pgn: {
      als: [`p${emouty.gn}`],
      run: (mes, kdy) => {
        if (mes.author.bot) return emouty.sjeta;

        // Random výplod GitHub Copilota
        if (kdy == "fejk") {
          mes.react(emouty.lukiw);
          return "fejk spinkacek";
        }

        let druh = "";
        let spinkZa: number;
        let random = false;

        if (kdy.startsWith("random")) {
          random = true;
          let rozdil = 540_000;
          let min = 60_000;
          if (kdy.length > 7) {
            const str = kdy.toLowerCase().slice(7).replace(/ /g, "").split("-");
            if (str.length != 2) return "cos to napsal kkte";
            const [from, to] = [strToDate(str[0]), strToDate(str[1])];
            if (!from.length || !to.length) return "cos to napsal kkte";
            if (to[1] <= from[1]) return "ja teda nevim jestli ti vys jak funguje cas ael vym ze tam mas chibu";

            rozdil = to[1] - from[1];
            min = from[1];
          }
          spinkZa = (Math.random() * rozdil + min);
          log(`random spink pro ${oddiakritikovat(mes.member?.displayName || "neznamejmeno")} byl nastaven na ${formatter(Date.now() + spinkZa)}`);
        } else {
          if (kdy == "")
            [druh, spinkZa] = ["rel", 2 * 60 * 60 * 1000];
          else {
            const back = strToDate(kdy);
            if (typeof back[1] != "number") return "cos to napsal kkte";
            [druh, spinkZa] = back;
          }
        }

        ifUzRemove(mes.author.id);

        budouciSpinky[mes.author.id] = setTimeout(() => {
          contactSpinkServer(Spinky.spinkacek, mes.author);
          mes.react(emouty.spinkacek);
        }, spinkZa);
        if (!random) {
          if (druh == "abs") return `tvuj spinkacek byl naplanovan na za ${formatCas(spinkZa / 1000)}`;
          else return `tvuj spinkacek byl naplanovan na ${formatter(Date.now() + spinkZa)}`;
        }
        mes.react(emouty.lajk);
      }
    },

    ps: {
      als: ["poslednispink"],
      arg: "kdo (nepovinné)",
      run: async (mes, arg) => {
        const prazdno = arg == "";
        if (!ping.test(arg) && !prazdno) return "sikkt";

        const id = prazdno ? mes.author.id : mes.mentions.members?.first()?.id ?? "";
        const entry = await getLastEntry(id);
        if (entry === false) return `tuten kkt jeste nespal ${emouty.lukiw}`;

        if (!entry.vstavacek) return "ten kkt jiz ickon spi";
        const cas = Date.now() - Number(entry.vstavacek);
        return `uz ${prazdno ? "jsi" : "je"} vzhuru ${formatCas(cas / 1000)}`;
      }
    },

    zruspgn: {
      als: ["zpgn", "cancelpgn", "cpgn", "unpgn", `unp${emouty.gn}`],
      run: mes => {
        if (!ifUzRemove(mes.author.id)) return `spinkacek ale nemas naplanovanej ty kkte ${mes.author}`;
        return "to bylo teda trapny debile";
      }
    },

    autospink: mes =>
      `autospink je ${autoSpinky[mes.author.id] ? (delete autoSpinky[mes.author.id], "vypnut") : (autoSpinky[mes.author.id] = [true, mes.channel], "zapnut")}`,

    autoautospink: {
      als: ["🚗🚗spink"],
      run: mes =>
        `autoautospink je ${autoSpinky[mes.author.id] ? (delete autoSpinky[mes.author.id], "vypnut") : (autoSpinky[mes.author.id] = [false, mes.channel], "zapnut")}`
    },

    kdysemselspat: async mes => {
      const entry = await getLastEntry(mes.author.id);
      if (entry === false) return "este sy nigdy nespal";

      return `to bilo ${formatter(entry.spinkacek)}`;
    }
  },

  on_voiceStateUpdate: async (bef, aft) => {
    if (!aft.channel || bef.channel) return;

    const entry = await getLastEntry(aft.id);

    if (entry && !entry.vstavacek) aft.disconnect("spinkacek")
      .catch(err => log("spinkacek odpojit se nepovedlo nebo co:", err));
  },

  super_on_messageCreate: async (mes, cmd?: string) => {
    const entry = await getLastEntry(mes.author.id);
    if ((entry && !entry.vstavacek) && cmd != "vstavacek") {
      if (mes.channel.type == ChannelType.DM) {
        messageReply(mes, "drz hubu");
        return true;
      }

      mes.delete().catch((e: Error) => {
        if (e.name == "DiscordAPIError[5OO13]") return;
        log("chyba pri mazani", e);
      });
      sendDM(mes.author, "spis tak nepis");
      return true;
    }

    return false;
  },

  // autoSpink handeler
  on_userPresenceUpdate: async (bef, aft) => {
    if (!bef) return;
    const befoff = bef.status == "offline";
    const aftoff = aft.status == "offline";
    const prop = autoSpinky[aft.userId];
    if (!prop) return;

    if (!befoff && aftoff) {
      return contactSpinkServer(Spinky.spinkacek, aft.member!.user);
    }

    if (befoff && !aftoff) {
      // autospink, ne autoautospink
      if (prop[0]) delete autoSpinky[aft.userId];
      const kanel = prop[1];
      if (kanel?.type == ChannelType.GuildText) sendWithoutReply(kanel, await sendGmMessage(aft.member!.user));
    }
  }
};

if (!process.env.ignoreSpink)
  module.exports = exp;
