/*eslint no-constant-condition: ["error", { "checkLoops": false }]*/

import { APIEmbed, ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, CacheType, ChannelType, Client, ClientPresenceStatusData, ComponentType, Message, MessageCreateOptions, ReadonlyCollection, SendableChannels, User } from "discord.js";
import { existsSync } from "fs";
import { MysqlError, createPool } from "mysql";
import { SMessage } from "./types";

if (!existsSync("config.json")) throw new Error("config.json neexistuje");
const konfig = require("../../config.json");
if (!konfig.ignoreUnknownKeys)
  for (const klic of Object.keys(konfig)) {
    if (!["adminChannel", "adminID", "DBPwd", "DBUser", "dieOnError", "ignoreMess", "ignorePresence", "ignoreSpink", "ignoreUnknownKeys", "noGeneralSync", "OnePlayToken", "prefix", "sachyDomena", "statPass", "token"].includes(klic)) throw new Error(`config.json obsahuje neznámý klíč: ${klic}`);
  }
process.env = { ...process.env, ...konfig };

export const oddiakritikovat = (a: string) => {
  return a
    .replace(/[àáâãäåæāăą]/g, "a")
    .replace(/[çćĉċč]/g, "c")
    .replace(/[ďđ]/g, "d")
    .replace(/[èéêëðēĕėęě]/g, "e")
    .replace(/[ĝğġģǵ]/g, "g")
    .replace(/[ĥħ]/g, "h")
    .replace(/[ìíîïĩīĭįı]/g, "i")
    .replace(/[ĵ]/g, "j")
    .replace(/[ķĸ]/g, "k")
    .replace(/[ĺļľŀł]/g, "l")
    .replace(/[ñńņňŉŋ]/g, "n")
    .replace(/[òóôõöøōŏőœ]/g, "o")
    .replace(/[þ]/g, "p")
    .replace(/[ŕŗř]/g, "r")
    .replace(/[ßśŝşšſ]/g, "s")
    .replace(/[ţťŧ]/g, "t")
    .replace(/[ùúûüũūŭůűų]/g, "u")
    .replace(/[ŵ]/g, "w")
    .replace(/[ÿýŷ]/g, "y")
    .replace(/[źżž]/g, "z")
    .replace(/[ÀÁÂÃÄÅÆĀĂĄ]/g, "A")
    .replace(/[ÇĆĈĊČ]/g, "C")
    .replace(/[Ď]/g, "D")
    .replace(/[ÈÉÊËÐĒĔĖĘĚ]/g, "E")
    .replace(/[ĜĞĠĢ]/g, "G")
    .replace(/[Ĵ]/g, "J")
    .replace(/[Ķ]/g, "K")
    .replace(/[ĤĦ]/g, "H")
    .replace(/[ÌÍÎÏĨĪĬĮİ]/g, "I")
    .replace(/[ĹĻĽĿŁ]/g, "L")
    .replace(/[ÑŃŅŇŊ]/g, "N")
    .replace(/[ÒÓÔÕÖØŌŎŐŒ]/g, "O")
    .replace(/[Þ]/g, "P")
    .replace(/[ŔŖŘ]/g, "R")
    .replace(/[ŚŜŞŠ]/g, "S")
    .replace(/[ŢŤŦ]/g, "T")
    .replace(/[ÙÚÛÜŨŪŬŮŰŲ]/g, "U")
    .replace(/[Ŵ]/g, "W")
    .replace(/[ÝŶŸ]/g, "Y")
    .replace(/[ŹŻŽ]/g, "Z");
};

/**
 * returns relative time in format `x hodin x minut a x se kund`
 * @param cas Number of seconds to convert
 */
export const formatCas = (cas: number) => {
  const h = Math.floor(cas / 3600);
  const m = Math.floor(cas % 3600 / 60);
  const s = Math.floor(cas % 3600 % 60);

  return `${h} hodin ${m} mynut a ${s} se kund`;
};

/**
 * Returns absolute date in format `d. m. h:mm:ss`
 * @param date Date object to convert
 */
export const formatter = new Intl.DateTimeFormat("cs", { day: "numeric", month: "short", hour: "numeric", minute: "numeric", second: "numeric" }).format;

export const ping = /^<@!?\d+>$/;

export function adminLog(client: Client, text: string, err?: string | Error) {
  if (process.env.adminChannel) {
    const adminChannel = client.channels.cache.get(process.env.adminChannel);
    if (adminChannel?.type == ChannelType.GuildText)
      sendWithoutReply(adminChannel, err ? `${text}\n\`\`\`${err}\`\`\`` : text);
  }
  if (process.env.adminID) {
    const user = client.users.cache.get(process.env.adminID);
    if (user) sendDM(user, err ? `${text}\n\`\`\`${err}\`\`\`` : text);
  }
}

export function log(...content: unknown[]) {
  let hasError = false;

  const colored = content.map(entry => {
    if (entry instanceof Error) {
      hasError = true;
      return `\x1b[31m${entry.stack || entry}\x1b[0m`;
    }

    return entry;
  });

  const d = new Date();

  const params = [`[${d.getDate()}.${d.getMonth() + 1}. ${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}.${d.getMilliseconds()}]:`, ...colored];

  if (hasError) {
    console.error(...params);
  } else {
    console.log(...params);
  }
}

export async function sendDM(user: User, txt: string) {
  try {
    const channel = await user.createDM();
    await sendWithoutReply(channel, txt);
  } catch (e) {
    log(new Error(`dementovi ${user} nejde poslat DM ${txt}`));
  }
}

export const rand = (max: number) => Math.floor(Math.random() * max);

export const prefix = process.env.prefix || "more";

type Nastaveni = {
  nabidka: (string | (({ val: string; } | { emoji: string; }) & { disabled?: boolean; }))[];
  channel: SendableChannels;
  zpravec: MessageCreateOptions;
  onCollect: (i: ButtonInteraction, id: number, radek: ActionRowBuilder<ButtonBuilder>) => void;
  timeout?: number;
} & ({
  onEnd: (c: ReadonlyCollection<string, ButtonInteraction<CacheType>>) => void;
} | {
  konecnaZprava: string;
  stejne?: boolean;
});

export async function nabidni(nastaveni: Nastaveni) {
  const radek = new ActionRowBuilder<ButtonBuilder>();
  nastaveni.nabidka.forEach((moznost, i) => {
    const component = new ButtonBuilder({ customId: `${i}`, style: ButtonStyle.Primary });

    const ejtoString = typeof moznost == "string";

    if (ejtoString || "val" in moznost) {
      const value = ejtoString ? moznost : moznost.val;
      const nazev = value.length > 80 ? `${value.slice(0, 77)}...` : value;
      component.setLabel(nazev);
    } else {
      component.setEmoji(moznost.emoji);
    }
    if (!ejtoString && moznost.disabled) component.setDisabled();
    radek.addComponents(component);
  });

  const zprava = await sendWithoutReply(nastaveni.channel, { ...nastaveni.zpravec, components: [radek] });
  const collector = nastaveni.channel.createMessageComponentCollector<ComponentType.Button>({ filter: i => i.message.id == zprava.id, time: nastaveni.timeout ?? 18e4 });

  collector.on("collect", i => {
    const lookupId = Number(i.customId);
    if (isNaN(lookupId)) return void sendWithoutReply(nastaveni.channel, "neco nefunguje idk");

    nastaveni.onCollect(i, lookupId, radek);
  });

  collector.on("end", c => {
    if ("onEnd" in nastaveni)
      nastaveni.onEnd(c);
    else if (!c.size || nastaveni.stejne) {
      radek.components.forEach(btn => btn.setDisabled());
      zprava.edit({ content: nastaveni.konecnaZprava, components: [radek] });
    }
  });
}

export const strankovani = (channel: SendableChannels, embed: APIEmbed, zacatekNazvu: string, stranky: string[][]) => {
  let aktualniStranka = 0;
  nabidni({
    channel: channel,
    nabidka: [{ emoji: "⬅️", disabled: true }, { emoji: "➡️" }],
    zpravec: { embeds: [embed] },
    onCollect: (i, id, radek) => {
      // Výpočet nový stánky
      aktualniStranka += (!id) ? -1 : 1;

      // Úprava aktivnosti tlačítek
      radek.components[0].setDisabled(!aktualniStranka);
      radek.components[1].setDisabled(aktualniStranka + 1 == stranky.length);

      // Úprava embedu
      embed.fields![0] = {
        name: `${zacatekNazvu} (${aktualniStranka + 1}/${stranky.length})`,
        value: `• ${stranky[aktualniStranka].join("\n• ")}`
      };

      // Odeslání
      i.update({ embeds: [embed], components: [radek] });
    },
    konecnaZprava: "",
    stejne: true
  });
};

// Připojení do databáze
export const bazenek = createPool({
  host: "127.0.0.1",
  user: process.env.DBUser,
  password: process.env.DBPwd,
  database: "denim",
  charset: "utf8mb4_unicode_ci"
});

export const safeQuery = <T>(query: string, parametry?: string[]) => new Promise<{ data: T[]; err: MysqlError | null; }>(res => {
  bazenek.query({ sql: query, values: parametry }, (err, odpoved) => {
    res({ data: odpoved, err });
  });
});

export const messageLinks = new Map<string, string>();

/**
 * Resolvne až zpráva s daným ID bude zařazena do messageLinks
 * @param mes Zpráva na kterou se má počkat
 */
export async function wait(mes: Message) {
  const waitingFrom = Date.now();

  while (true) {
    if (messageLinks.has(mes.id))
      return;

    if (Date.now() - waitingFrom > 30_000) {
      const err = new Error(`Zpráva ${mes.id} nebyla zařazena do messageLinks do 30 sekund`);
      adminLog(mes.client, "při čekání na zprávu jsem se nedočkal", err);
      log(err);
      return;
    }

    await new Promise(r => setTimeout(r, 0));
  }
}

async function baseSend(channel: SendableChannels, co: string | MessageCreateOptions, orgId: string) {
  const nova = await channel.send(co);

  messageLinks.set(nova.id, orgId);

  return nova;
}

/**
  * Slouží k odeslání zprávy jako odpověď na jinou zprávu a zajistí, anti-spam mechanismus bude fungovat.
  * @param mes Zpráva na kterou se má odpovědět
  * @param co Obsah zprávy, který se má odeslat
  */
export async function messageReply(mes: SMessage, co: string | MessageCreateOptions) {
  return await baseSend(mes.channel, co, mes.id);
}

/**
  * Slouží k odeslání zprávy bez odpovědi na jinou zprávu a zajistí, že anti-spam mechanismus bude fungovat.
  * @param channel Kanál, kam se má zpráva odeslat
  * @param co Obsah zprávy, který se má odeslat
  */
export async function sendWithoutReply(channel: SendableChannels, co: string | MessageCreateOptions) {
  return await baseSend(channel, co, "<nic>");
}

export const areStatusesSame = (object1?: ClientPresenceStatusData | null, object2?: ClientPresenceStatusData | null) => {

  if ((object1 === null || object1 === undefined) && (object2 === null || object2 === undefined)) return true;

  if (object1 === null || object2 === null || object1 === undefined || object2 === undefined) return false;

  const objKeys1 = Object.keys(object1) as (keyof ClientPresenceStatusData)[];
  const objKeys2 = Object.keys(object2);

  if (objKeys1.length !== objKeys2.length) return false;

  for (const key of objKeys1) {
    const value1 = object1[key];
    const value2 = object2[key];

    if (value1 !== value2)
      return false;
  }
  return true;
};

export async function betterFetch(url: string, options?: RequestInit) {
  const maxRetry = 5;
  const retryWait = 3_000;

  for (let currentAttempt = 1; currentAttempt < maxRetry; currentAttempt++) {
    try {
      return await fetch(url, options);
    } catch (err) {
      log(`Chyba při stahování ${url}, pokus číslo ${currentAttempt}, čekám ${retryWait / 1000} sekund`);
      await new Promise(r => setTimeout(r, retryWait));
    }
  }

  throw new Error(`Nepodařilo se stáhnout ${url} ani na ${maxRetry} pokusů`);
}
