import * as DB from "./db";
import * as Notifications from "./elements/notifications";
import { isConfigValueValid } from "./config-validation";
import * as ConfigEvent from "./observables/config-event";
import * as AccountButton from "./elements/account-button";
import { debounce } from "throttle-debounce";
import {
  canSetConfigWithCurrentFunboxes,
  canSetFunboxWithConfig,
} from "./test/funbox/funbox-validation";
import {
  createErrorMessage,
  isObject,
  promiseWithResolvers,
  typedKeys,
} from "./utils/misc";
import * as ConfigSchemas from "@monkeytype/schemas/configs";
import { Config, FunboxName } from "@monkeytype/schemas/configs";
import { LocalStorageWithSchema } from "./utils/local-storage-with-schema";
import { migrateConfig } from "./utils/config";
import { getDefaultConfig } from "./constants/default-config";
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
import { ZodSchema } from "zod";
import * as TestState from "./test/test-state";
import { ConfigMetadataObject, configMetadata } from "./config-metadata";

const configLS = new LocalStorageWithSchema({
  key: "config",
  schema: ConfigSchemas.ConfigSchema,
  fallback: getDefaultConfig(),
  migrate: (value, _issues) => {
    if (!isObject(value)) {
      return getDefaultConfig();
    }
    //todo maybe send a full config to db so that it removes legacy values

    return migrateConfig(value);
  },
});

let config: Config = {
  ...getDefaultConfig(),
};

let configToSend: Partial<Config> = {};
const saveToDatabase = debounce(1000, () => {
  if (Object.keys(configToSend).length > 0) {
    AccountButton.loading(true);
    void DB.saveConfig(configToSend).then(() => {
      AccountButton.loading(false);
    });
  }
  configToSend = {} as Config;
});

function saveToLocalStorage(
  key: keyof Config,
  nosave = false,
  noDbCheck = false,
): void {
  if (nosave) return;
  configLS.set(config);
  if (!noDbCheck) {
    //@ts-expect-error this is fine
    configToSend[key] = config[key];
    saveToDatabase();
  }
}

export function saveFullConfigToLocalStorage(noDbCheck = false): void {
  console.log("saving full config to localStorage");
  configLS.set(config);
  if (!noDbCheck) {
    AccountButton.loading(true);
    void DB.saveConfig(config);
    AccountButton.loading(false);
  }
}

function isConfigChangeBlocked(): boolean {
  if (TestState.isActive && config.funbox.includes("no_quit")) {
    Notifications.add("No quit funbox is active. Please finish the test.", 0, {
      important: true,
    });
    return true;
  }
  return false;
}

// export function batchSetConfig(
//   keyValues: Partial<Config>,
//   nosave: boolean = false,
// ): Partial<Record<keyof Config, boolean>> {
//   let ret: Partial<Record<keyof Config, boolean>> = {};
//   for (const key of typedKeys(keyValues)) {
//     ret[key] = setConfig(key, keyValues[key] as Config[keyof Config], {
//       nosave: true,
//       batched: true,
//     });
//     //@ts-expect-error this is fine
//     configToSend[key] = config[key];
//   }

//   const restartRequired = typedKeys(keyValues).some((key) => {
//     const metadata = configMetadata[key];
//     return metadata.changeRequiresRestart;
//   });

//   const triggerResizeNeeded = typedKeys(keyValues).some((key) => {
//     const metadata = configMetadata[key];
//     return metadata.triggerResize;
//   });

//   if (triggerResizeNeeded) {
//     triggerResize();
//   }

//   if (!nosave) {
//     configLS.set(config);
//     saveToDatabase();
//   }

//   ConfigEvent.dispatch({
//     key: "batchConfigApplied",
//     nosave,
//     fullConfig: config,
//     restartRequired,
//   });

//   return ret;
// }

export function setConfig<T extends keyof Config>(
  key: T,
  value: Config[T],
  options?: {
    nosave?: boolean;
    noevent?: boolean;
  },
): boolean {
  const metadata = configMetadata[key] as ConfigMetadataObject[T];
  if (metadata === undefined) {
    throw new Error(`Config metadata for key "${key}" is not defined.`);
  }

  if (metadata.overrideValue) {
    value = metadata.overrideValue({
      value,
      currentValue: config[key],
      currentConfig: config,
    });
  }

  const previousValue = config[key];

  if (
    metadata.changeRequiresRestart &&
    TestState.isActive &&
    config.funbox.includes("no_quit")
  ) {
    Notifications.add("No quit funbox is active. Please finish the test.", 0, {
      important: true,
    });
    console.warn(
      `Could not set config key "${key}" with value "${JSON.stringify(
        value,
      )}" - no quit funbox active.`,
    );
    return false;
  }

  if (metadata.isBlocked?.({ value, currentConfig: config })) {
    console.warn(
      `Could not set config key "${key}" with value "${JSON.stringify(
        value,
      )}" - blocked.`,
    );
    return false;
  }

  const schema = ConfigSchemas.ConfigSchema.shape[key] as ZodSchema;

  if (!isConfigValueValid(metadata.displayString ?? key, value, schema)) {
    console.warn(
      `Could not set config key "${key}" with value "${JSON.stringify(
        value,
      )}" - invalid value.`,
    );
    return false;
  }

  if (!canSetConfigWithCurrentFunboxes(key, value, config.funbox)) {
    console.warn(
      `Could not set config key "${key}" with value "${JSON.stringify(
        value,
      )}" - funbox conflict.`,
    );
    return false;
  }

  if (metadata.overrideConfig) {
    const targetConfig = metadata.overrideConfig({
      value,
      currentConfig: config,
    });

    for (const targetKey of typedKeys(targetConfig)) {
      const targetValue = targetConfig[
        targetKey
      ] as ConfigSchemas.Config[keyof typeof configMetadata];

      if (config[targetKey] === targetValue) {
        continue; // no need to set if the value is already the same
      }

      const set = setConfig(targetKey, targetValue, options);
      if (!set) {
        throw new Error(
          `Failed to set config key "${targetKey}" with value "${targetValue}" for ${metadata.displayString} config override.`,
        );
      }
    }
  }

  config[key] = value;
  if (!options?.nosave) saveToLocalStorage(key, options?.nosave);

  if (!options?.noevent) {
    ConfigEvent.dispatch({
      key: key,
      newValue: value,
      nosave: options?.nosave ?? false,
      previousValue: previousValue,
      metadata,
    } as ConfigEvent.ConfigEventOptions);
  }

  metadata.afterSet?.({
    nosave: options?.nosave ?? false,
    currentConfig: config,
  });
  return true;
}

export function toggleFunbox(funbox: FunboxName, nosave?: boolean): boolean {
  if (isConfigChangeBlocked()) return false;

  if (!canSetFunboxWithConfig(funbox, config)) {
    return false;
  }

  const previousValue = config.funbox;

  let newConfig: FunboxName[] = config.funbox;

  if (newConfig.includes(funbox)) {
    newConfig = newConfig.filter((it) => it !== funbox);
  } else {
    newConfig.push(funbox);
    newConfig.sort();
  }

  if (!isConfigValueValid("funbox", newConfig, ConfigSchemas.FunboxSchema)) {
    return false;
  }

  config.funbox = newConfig;
  saveToLocalStorage("funbox", nosave);
  ConfigEvent.dispatch({
    key: "funbox",
    newValue: config.funbox,
    nosave,
    previousValue,
    metadata: configMetadata.funbox,
  });

  return true;
}

export function setQuoteLengthAll(nosave?: boolean): boolean {
  return setConfig("quoteLength", [0, 1, 2, 3], {
    nosave,
  });
}

const lastConfigsToApply: Set<keyof Config> = new Set([
  "keymapMode",
  "minWpm",
  "minAcc",
  "minBurst",
  "paceCaret",
  "quoteLength", //quote length sets mode,
  "words",
  "time",
  "mode", // mode sets punctuation and numbers
  "numbers",
  "punctuation",
  "funbox",
]);

export async function applyConfig(
  partialConfig: Partial<Config>,
): Promise<void> {
  if (partialConfig === undefined || partialConfig === null) return;

  //migrate old values if needed, remove additional keys and merge with default config
  const fullConfig: Config = migrateConfig(partialConfig);

  ConfigEvent.dispatch({ key: "fullConfigChange" });

  const defaultConfig = getDefaultConfig();
  for (const key of typedKeys(fullConfig)) {
    //@ts-expect-error this is fine, both are of type config
    config[key] = defaultConfig[key];
  }

  const configKeysToReset: (keyof Config)[] = [];

  const firstKeys = typedKeys(fullConfig).filter(
    (key) => !lastConfigsToApply.has(key),
  );

  // const keys = [...firstKeys, ...lastConfigsToApply];

  // const batchSetResult = batchSetConfig(
  //   keys.reduce((obj, key) => {
  //     //@ts-expect-error this is fine
  //     obj[key] = fullConfig[key];
  //     return obj;
  //   }, {}),
  //   true,
  // );
  // const failedKeys = typedKeys(batchSetResult).filter(
  //   (key) => batchSetResult[key] === false,
  // );

  // if (failedKeys.length > 0) {
  //   configKeysToReset.push(...failedKeys);
  // }

  for (const configKey of [...firstKeys, ...lastConfigsToApply]) {
    const configValue = fullConfig[configKey];

    const set = setConfig(configKey, configValue, {
      nosave: true,
      noevent: true,
    });

    if (!set) {
      configKeysToReset.push(configKey);
    }
  }

  for (const key of configKeysToReset) {
    saveToLocalStorage(key);
  }

  ConfigEvent.dispatch({ key: "configApplied", fullConfig: config });

  ConfigEvent.dispatch({ key: "fullConfigChangeFinished" });
}

export async function resetConfig(): Promise<void> {
  await applyConfig(getDefaultConfig());
  await DB.resetConfig();
  saveFullConfigToLocalStorage(true);
}

export async function loadFromLocalStorage(): Promise<void> {
  console.log("loading localStorage config");
  const newConfig = configLS.get();
  if (newConfig === undefined) {
    await resetConfig();
  } else {
    await applyConfig(newConfig);
    saveFullConfigToLocalStorage(true);
  }
  loadDone();
}

export function getConfigChanges(): Partial<Config> {
  const configChanges: Partial<Config> = {};
  typedKeys(config)
    .filter((key) => {
      return config[key] !== getDefaultConfig()[key];
    })
    .forEach((key) => {
      //@ts-expect-error this is fine
      configChanges[key] = config[key];
    });
  return configChanges;
}

export async function applyConfigFromJson(json: string): Promise<void> {
  try {
    const parsedConfig = parseJsonWithSchema(
      json,
      ConfigSchemas.PartialConfigSchema.strip(),
      {
        migrate: (value) => {
          if (Array.isArray(value)) {
            throw new Error("Invalid config");
          }
          return migrateConfig(value);
        },
      },
    );
    await applyConfig(parsedConfig);
    saveFullConfigToLocalStorage();
    Notifications.add("Done", 1);
  } catch (e) {
    const msg = createErrorMessage(e, "Failed to import settings");
    console.error(msg);
    Notifications.add(msg, -1);
  }
}

const { promise: configLoadPromise, resolve: loadDone } =
  promiseWithResolvers();

export { configLoadPromise };
export default config;
export const __testing = {
  configMetadata,
  replaceConfig: (setConfig: Partial<Config>): void => {
    const newConfig = { ...getDefaultConfig(), ...setConfig };
    for (const key of Object.keys(config)) {
      Reflect.deleteProperty(config, key);
    }
    Object.assign(config, newConfig);
    configToSend = {} as Config;
  },
  getConfig: () => config,
};
