import path from "node:path";
import { cosmiconfig } from "cosmiconfig";
import {
  type ParserServices,
  TSESLint,
  TSESTree,
} from "@typescript-eslint/utils";
import resolve from "resolve";
import type { Connector } from "@hi18n/tools-core";
import * as jsonMfConnector from "./json-mf-connector.ts";

const explorer = cosmiconfig("hi18n");

export type ParserSpec = string | TSESLint.Parser.LooseParserModule;
export type ParserDefinition = ESLintParser | GenericParser;
export type ESLintParser = {
  parseForESLint: (
    source: string,
    options: TSESLint.ParserOptions,
  ) => ESLintParserResult<TSESTree.Program>;
};
export type GenericParser = {
  parse: (source: string, options: TSESLint.ParserOptions) => TSESTree.Program;
};
export type ESLintParserResult<T> = {
  ast: T;
  services?: ParserServices | undefined;
  visitorKeys?: Record<string, string[]> | undefined;
  scopeManager?: TSESLint.Scope.ScopeManager | undefined;
};

export type ConnectorSpec = string | ConnectorDependency;
export type ConnectorDependency = {
  connector: Connector;
};

const DEFAULT_EXTENSIONS = [
  ".js",
  ".cjs",
  ".mjs",
  ".ts",
  ".cts",
  ".mts",
  ".jsx",
  ".tsx",
];

const DEFAULT_EXTENSIONS_TO_REMOVE = [".js", ".cjs", ".mjs"];

const DEFAULT_PARSER_OPTIONS: TSESLint.ParserOptions = {
  ecmaVersion: "latest",
  sourceType: "module",
};

const configKeys = [
  "include",
  "exclude",
  "parser",
  "parserOptions",
  "extensions",
  "extensionsToRemove",
  "baseUrl",
  "paths",
  "connector",
  "connectorOptions",
];

export type Config = {
  configPath: string;
  include?: string[] | undefined;
  exclude?: string[] | undefined;
  parser: ParserSpec;
  parserOptions: TSESLint.ParserOptions;
  extensions: string[];
  extensionsToRemove: string[];
  baseUrl?: string | undefined;
  paths?: Record<string, string[]> | undefined;
  connector: ConnectorDependency | undefined;
  connectorOptions: unknown;
};

export async function loadConfig(cwd: string): Promise<Config> {
  const cosmiconfigResult = (await explorer.search(cwd)) ?? {
    config: {},
    filepath: path.join(cwd, ".hi18nrc.json"),
    isEmpty: true,
  };
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
  const { config, filepath } = cosmiconfigResult;
  if (!isObject(config)) {
    throw new Error("config: not an object");
  }
  if (!optional(isArrayOf(isString))(config["include"])) {
    throw new Error("config.include: not an array of strings");
  }
  if (!optional(isArrayOf(isString))(config["exclude"])) {
    throw new Error("config.exclude: not an array of strings");
  }
  if (!optional(oneof(isString, isLooseParserModule))(config["parser"])) {
    throw new Error("config.parser: not a string nor a parser object");
  }
  if (!optional(isObject)(config["parserOptions"])) {
    throw new Error("config.parserOptions: not an object");
  }
  if (!optional(isArrayOf(isString))(config["extensions"])) {
    throw new Error("config.extensions: not an array of strings");
  }
  if (!optional(isArrayOf(isString))(config["extensionsToRemove"])) {
    throw new Error("config.extensionsToRemove: not an array of strings");
  }
  if (!optional(isString)(config["baseUrl"])) {
    throw new Error("config.baseUrl: not a string");
  }
  if (!optional(isRecordOf(isArrayOf(isString)))(config["paths"])) {
    throw new Error("config.paths: not a record of arrays of strings");
  }
  if (!optional(oneof(isString, isConnectorDependency))(config["connector"])) {
    throw new Error("config.connector: not a string nor a parser object");
  }
  if (!optional(isObject)(config["connectorOptions"])) {
    throw new Error("config.connectorOptions: not an object");
  }
  for (const key of Object.keys(config)) {
    if (!configKeys.includes(key)) {
      throw new Error(`Unrecognized config: ${key}`);
    }
  }

  const include = config["include"];
  const exclude = config["exclude"];
  const parser = resolveParser(config["parser"], filepath);
  const parserOptions =
    (config["parserOptions"] as TSESLint.ParserOptions | undefined) ??
    DEFAULT_PARSER_OPTIONS;
  const extensions = expandExtensions(config["extensions"]);
  const extensionsToRemove = expandExtensionsToRemove(
    config["extensionsToRemove"],
  );
  const baseUrl = expandBaseUrl(config["baseUrl"], filepath);
  const paths = config["paths"];

  if (paths && !baseUrl) {
    throw new Error("baseUrl must be specified");
  }

  const connector = resolveConnector(config["connector"], filepath);
  const connectorOptions = config["connectorOptions"] ?? {};

  return {
    configPath: filepath,
    include,
    exclude,
    parser,
    parserOptions,
    extensions,
    extensionsToRemove,
    baseUrl,
    paths,
    connector,
    connectorOptions,
  };
}

function resolveParser(
  parser: ParserSpec | undefined,
  filepath: string,
): TSESLint.Parser.LooseParserModule {
  if (typeof parser === "string") {
    const parserPath = resolve.sync(parser, {
      basedir: path.dirname(filepath),
    });
    // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-return
    return require(parserPath);
  } else if (typeof parser === "object" && parser != null) {
    return parser;
  }
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-return
  return require("@typescript-eslint/parser");
}

function resolveConnector(
  connector: ConnectorSpec | undefined,
  filepath: string,
): ConnectorDependency | undefined {
  if (connector === "@hi18n/cli/json-mf-connector") {
    return jsonMfConnector;
  } else if (typeof connector === "string") {
    const connectorPath = resolve.sync(connector, {
      basedir: path.dirname(filepath),
    });
    // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-return
    return require(connectorPath);
  } else if (typeof connector === "object") {
    return connector;
  }
  return undefined;
}

function expandExtensions(extensions: string[] | undefined): string[] {
  if (extensions === undefined) return DEFAULT_EXTENSIONS;
  return extensions.flatMap((ext) =>
    ext === "..." ? DEFAULT_EXTENSIONS : [ext],
  );
}

function expandExtensionsToRemove(
  extensionsToRemove: string[] | undefined,
): string[] {
  if (extensionsToRemove === undefined) return DEFAULT_EXTENSIONS_TO_REMOVE;
  return extensionsToRemove.flatMap((ext) =>
    ext === "..." ? DEFAULT_EXTENSIONS_TO_REMOVE : [ext],
  );
}

function expandBaseUrl(
  baseUrl: string | undefined,
  filepath: string,
): string | undefined {
  if (baseUrl === undefined) return undefined;
  return path.resolve(path.dirname(filepath), baseUrl);
}

function isObject(x: unknown): x is object & Record<string, unknown> {
  return typeof x === "object";
}

function isString(x: unknown): x is string {
  return typeof x === "string";
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
function isFunction(x: unknown): x is Function {
  return typeof x === "function";
}

function isArrayOf<T>(pred: (x: unknown) => x is T) {
  return function isArrayOf(x: unknown): x is T[] {
    return Array.isArray(x) && x.every((value) => pred(value));
  };
}

function isRecordOf<T>(pred: (x: unknown) => x is T) {
  return function isRecordOf(x: unknown): x is Record<string, T> {
    return isObject(x) && Object.values(x).every((value) => pred(value));
  };
}

function optional<T>(pred: (x: unknown) => x is T) {
  return function optional(x: unknown): x is T | undefined {
    return x === undefined || pred(x);
  };
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function oneof<Types extends any[]>(
  ...preds: { [K in keyof Types]: (x: unknown) => x is Types[K] }
) {
  return function oneof(x: unknown): x is Types[number] {
    return preds.some((pred) => pred(x));
  };
}

function isLooseParserModule(
  x: unknown,
): x is TSESLint.Parser.LooseParserModule {
  const xx = x as Record<string, unknown>;
  return (
    typeof xx.parseForESLint === "function" || typeof xx.parse === "function"
  );
}

function isConnectorDependency(x: unknown): x is ConnectorDependency {
  return isObject(x) && isFunction(x["connector"]);
}
