import {
  CustomBackgroundFilter,
  CustomBackgroundFilterSchema,
  CustomBackgroundSize,
  CustomBackgroundSizeSchema,
  CustomThemeColors,
  CustomThemeColorsSchema,
  FunboxSchema,
  FunboxName,
} from "@monkeytype/schemas/configs";
import { Language } from "@monkeytype/schemas/languages";
import { CustomTextSettingsSchema } from "@monkeytype/schemas/results";
import {
  DifficultySchema,
  Mode2Schema,
  ModeSchema,
} from "@monkeytype/schemas/shared";
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
import { tryCatchSync } from "@monkeytype/util/trycatch";
import { decompressFromURI } from "lz-ts";
import { z } from "zod";

import Ape from "../ape";
import { setConfig } from "../config/setters";
import { Config } from "../config/store";
import * as DB from "../db";
import { authEvent } from "../events/auth";
import { showLoaderBar, hideLoaderBar } from "../states/loader-bar";
import {
  showNoticeNotification,
  showErrorNotification,
  showSuccessNotification,
} from "../states/notifications";
import * as CustomText from "../test/custom-text";
import { restart as restartTest } from "../test/test-logic";
import * as TestState from "../test/test-state";
import * as Misc from "../utils/misc";
import * as ChallengeController from "./challenge-controller";

export async function linkDiscord(hashOverride: string): Promise<void> {
  if (!hashOverride) return;
  const fragment = new URLSearchParams(hashOverride.slice(1));
  if (fragment.has("access_token")) {
    history.replaceState(null, "", "/");
    const accessToken = fragment.get("access_token") as string;
    const tokenType = fragment.get("token_type") as string;
    const state = fragment.get("state") as string;

    showLoaderBar();
    const response = await Ape.users.linkDiscord({
      body: { tokenType, accessToken, state },
    });
    hideLoaderBar();

    if (response.status !== 200) {
      showErrorNotification("Failed to link Discord", { response });
      return;
    }

    if (response.body.data === null) {
      showErrorNotification("Failed to link Discord: data returned was null");
      return;
    }

    showSuccessNotification(response.body.message);

    const snapshot = DB.getSnapshot();
    if (!snapshot) return;

    const { discordId, discordAvatar } = response.body.data;
    if (discordId !== undefined) {
      snapshot.discordId = discordId;
    }
    if (discordAvatar !== undefined) {
      snapshot.discordAvatar = discordAvatar;
    }

    DB.setSnapshot(snapshot);
  }
}

const customThemeUrlDataSchema = z.object({
  c: CustomThemeColorsSchema,
  i: z.string().optional(),
  s: CustomBackgroundSizeSchema.optional(),
  f: CustomBackgroundFilterSchema.optional(),
});

export function loadCustomThemeFromUrl(getOverride?: string): void {
  const getValue = Misc.findGetParameter("customTheme", getOverride);
  if (getValue === null) return;

  const { data: decoded, error } = tryCatchSync(() =>
    parseJsonWithSchema(atob(getValue), customThemeUrlDataSchema),
  );
  if (error) {
    console.log("Custom theme URL decoding failed", error);
    showNoticeNotification("Failed to load theme from URL: " + error.message);
    return;
  }

  let colorArray: CustomThemeColors | undefined;
  let image: string | undefined;
  let size: CustomBackgroundSize | undefined;
  let filter: CustomBackgroundFilter | undefined;
  if (Array.isArray(decoded.c) && decoded.c.length === 10) {
    colorArray = decoded.c;
    image = decoded.i;
    size = decoded.s;
    filter = decoded.f;
  } else if (Array.isArray(decoded) && decoded.length === 10) {
    // This is for backward compatibility with old format
    colorArray = decoded as unknown as CustomThemeColors;
  }

  if (colorArray === undefined || colorArray.length !== 10) {
    showNoticeNotification("Failed to load theme from URL: no colors found");
    return;
  }

  const oldCustomTheme = Config.customTheme;
  const oldCustomThemeColors = Config.customThemeColors;
  try {
    setConfig("customThemeColors", colorArray);
    showSuccessNotification("Custom theme applied");

    if (image !== undefined && size !== undefined && filter !== undefined) {
      setConfig("customBackground", image);
      setConfig("customBackgroundSize", size);
      setConfig("customBackgroundFilter", filter);
    }

    if (!Config.customTheme) setConfig("customTheme", true);
  } catch (e) {
    showNoticeNotification(
      "Something went wrong. Reverting to previous state.",
    );
    console.error(e);
    setConfig("customTheme", oldCustomTheme);
    setConfig("customThemeColors", oldCustomThemeColors);
  }
}

const TestSettingsSchema = z.tuple([
  ModeSchema.nullable(),
  Mode2Schema.nullable(),
  CustomTextSettingsSchema.partial({
    pipeDelimiter: true,
    limit: true,
    mode: true,
  })
    //legacy values
    .extend({
      isTimeRandom: z.boolean().optional(),
      isWordRandom: z.boolean().optional(),
      word: z.number().int().optional(),
      time: z.number().int().optional(),
      delimiter: z.string().optional(),
    })
    .nullable(),
  z.boolean().nullable(), //punctuation
  z.boolean().nullable(), //numbers
  z.string().nullable(), //language
  DifficultySchema.nullable(),
  FunboxSchema.or(z.string().nullable()), //funbox as array or legacy string as hash separated values
]);

export function loadTestSettingsFromUrl(getOverride?: string): void {
  const getValue = Misc.findGetParameter("testSettings", getOverride);
  if (getValue === null) return;

  // if the encoding structure or method ever changes, make sure to support the old data format
  // otherwise eiko will be sad

  const { data: de, error } = tryCatchSync(() =>
    parseJsonWithSchema(decompressFromURI(getValue) ?? "", TestSettingsSchema),
  );
  if (error) {
    console.error("Failed to parse test settings:", error);
    showNoticeNotification(
      "Failed to load test settings from URL: " + error.message,
    );
    return;
  }

  const applied: Record<string, string> = {};

  if (de[0] !== null) {
    setConfig("mode", de[0], {
      nosave: true,
    });
    applied["mode"] = de[0];
  }

  const mode = de[0] ?? Config.mode;
  if (de[1] !== null) {
    if (mode === "time") {
      setConfig("time", parseInt(de[1], 10), {
        nosave: true,
      });
    } else if (mode === "words") {
      setConfig("words", parseInt(de[1], 10), {
        nosave: true,
      });
    } else if (mode === "quote") {
      setConfig("quoteLength", [-2]);
      TestState.setSelectedQuoteId(parseInt(de[1], 10));
    }
    applied["mode2"] = de[1];
  }

  if (de[2] !== null) {
    const customTextSettings = de[2];
    CustomText.setText(customTextSettings.text);

    //make sure to set mode before the limit as mode also sets the limit
    CustomText.setMode(customTextSettings.mode ?? "repeat");

    if (customTextSettings.limit !== undefined) {
      CustomText.setLimitMode(customTextSettings.limit.mode);
      CustomText.setLimitValue(customTextSettings.limit.value);
    }
    //convert legacy values
    else {
      if (customTextSettings.isWordRandom) {
        CustomText.setLimitMode("word");
      } else if (customTextSettings.isTimeRandom) {
        CustomText.setLimitMode("time");
      }
      if (customTextSettings.word !== undefined) {
        CustomText.setLimitValue(customTextSettings.word);
      } else if (customTextSettings.time !== undefined) {
        CustomText.setLimitValue(customTextSettings.time);
      }
    }

    if (customTextSettings.pipeDelimiter) {
      CustomText.setPipeDelimiter(customTextSettings.pipeDelimiter);
    }
    //convert legacy values
    else if (customTextSettings.delimiter === "|") {
      CustomText.setPipeDelimiter(true);
    }

    applied["custom text settings"] = "";
  }

  if (de[3] !== null) {
    setConfig("punctuation", de[3], {
      nosave: true,
    });
    applied["punctuation"] = de[3] ? "on" : "off";
  }

  if (de[4] !== null) {
    setConfig("numbers", de[4], {
      nosave: true,
    });
    applied["numbers"] = de[4] ? "on" : "off";
  }

  if (de[5] !== null) {
    setConfig("language", de[5] as Language, {
      nosave: true,
    });
    applied["language"] = de[5];
  }

  if (de[6] !== null) {
    setConfig("difficulty", de[6], {
      nosave: true,
    });
    applied["difficulty"] = de[6];
  }

  if (de[7] !== null) {
    let val: FunboxName[] = [];
    //convert legacy values
    if (typeof de[7] === "string") {
      val = de[7].split("#") as FunboxName[];
    } else {
      val = de[7];
    }
    setConfig("funbox", val, {
      nosave: true,
    });
    applied["funbox"] = val.join(", ");
  }

  restartTest({
    nosave: true,
  });

  const appliedEntries = Object.entries(applied).filter(
    ([, v]) => v !== undefined,
  );

  if (appliedEntries.length > 0) {
    const lines = appliedEntries
      .map(
        ([key, val]) =>
          Misc.escapeHTML(key) + (val ? ": " + Misc.escapeHTML(val) : ""),
      )
      .join("<br />");
    showSuccessNotification(`Settings applied from URL:<br /><br />${lines}`, {
      durationMs: 10000,
      useInnerHtml: true,
    });
  }
}

export async function loadChallengeFromUrl(
  getOverride?: string,
): Promise<void> {
  const getValue = (
    Misc.findGetParameter("challenge", getOverride) ?? ""
  ).toLowerCase();
  if (getValue === "") return;

  ChallengeController.setup(getValue)
    .then((result) => {
      if (result) {
        restartTest({
          nosave: true,
        });
      }
    })
    .catch((e: unknown) => {
      showErrorNotification("Failed to load challenge");
      console.error(e);
    });
}

authEvent.subscribe(async (event) => {
  if (event.type === "authStateChanged") {
    const search = window.location.search;
    const hash = window.location.hash;

    await event.data.loadPromise;

    loadCustomThemeFromUrl(search);
    loadTestSettingsFromUrl(search);
    void loadChallengeFromUrl(search);
    void linkDiscord(hash);
  }
});
