import { existsSync, promises } from 'node:fs';
import { resolve, extname, dirname } from 'pathe';
import * as dotenv from 'dotenv';
import { rm } from 'node:fs/promises';
import { homedir } from 'node:os';
import createJiti from 'jiti';
import * as rc9 from 'rc9';
import { defu } from 'defu';
import { findWorkspaceDir, readPackageJSON } from 'pkg-types';
import { watch } from 'chokidar';
import { debounce } from 'perfect-debounce';
import { diff } from 'ohash';

async function setupDotenv(options) {
  const targetEnvironment = options.env ?? process.env;
  const environment = await loadDotenv({
    cwd: options.cwd,
    fileName: options.fileName ?? ".env",
    env: targetEnvironment,
    interpolate: options.interpolate ?? true
  });
  for (const key in environment) {
    if (!key.startsWith("_") && targetEnvironment[key] === void 0) {
      targetEnvironment[key] = environment[key];
    }
  }
  return environment;
}
async function loadDotenv(options) {
  const environment = /* @__PURE__ */ Object.create(null);
  const dotenvFile = resolve(options.cwd, options.fileName);
  if (existsSync(dotenvFile)) {
    const parsed = dotenv.parse(await promises.readFile(dotenvFile, "utf8"));
    Object.assign(environment, parsed);
  }
  if (!options.env?._applied) {
    Object.assign(environment, options.env);
    environment._applied = true;
  }
  if (options.interpolate) {
    interpolate(environment);
  }
  return environment;
}
function interpolate(target, source = {}, parse = (v) => v) {
  function getValue(key) {
    return source[key] !== void 0 ? source[key] : target[key];
  }
  function interpolate2(value, parents = []) {
    if (typeof value !== "string") {
      return value;
    }
    const matches = value.match(/(.?\${?(?:[\w:]+)?}?)/g) || [];
    return parse(
      // eslint-disable-next-line unicorn/no-array-reduce
      matches.reduce((newValue, match) => {
        const parts = /(.?)\${?([\w:]+)?}?/g.exec(match) || [];
        const prefix = parts[1];
        let value2, replacePart;
        if (prefix === "\\") {
          replacePart = parts[0] || "";
          value2 = replacePart.replace("\\$", "$");
        } else {
          const key = parts[2];
          replacePart = (parts[0] || "").slice(prefix.length);
          if (parents.includes(key)) {
            console.warn(
              `Please avoid recursive environment variables ( loop: ${parents.join(
                " > "
              )} > ${key} )`
            );
            return "";
          }
          value2 = getValue(key);
          value2 = interpolate2(value2, [...parents, key]);
        }
        return value2 !== void 0 ? newValue.replace(replacePart, value2) : newValue;
      }, value)
    );
  }
  for (const key in target) {
    target[key] = interpolate2(getValue(key));
  }
}

async function loadConfig(options) {
  options.cwd = resolve(process.cwd(), options.cwd || ".");
  options.name = options.name || "config";
  options.envName = options.envName ?? process.env.NODE_ENV;
  options.configFile = options.configFile ?? (options.name !== "config" ? `${options.name}.config` : "config");
  options.rcFile = options.rcFile ?? `.${options.name}rc`;
  if (options.extend !== false) {
    options.extend = {
      extendKey: "extends",
      ...options.extend
    };
  }
  options.jiti = options.jiti || createJiti(void 0, {
    interopDefault: true,
    requireCache: false,
    esmResolve: true,
    ...options.jitiOptions
  });
  const r = {
    config: {},
    cwd: options.cwd,
    configFile: resolve(options.cwd, options.configFile),
    layers: []
  };
  if (options.dotenv) {
    await setupDotenv({
      cwd: options.cwd,
      ...options.dotenv === true ? {} : options.dotenv
    });
  }
  const { config, configFile } = await resolveConfig(".", options);
  if (configFile) {
    r.configFile = configFile;
  }
  const configRC = {};
  if (options.rcFile) {
    if (options.globalRc) {
      Object.assign(
        configRC,
        rc9.readUser({ name: options.rcFile, dir: options.cwd })
      );
      const workspaceDir = await findWorkspaceDir(options.cwd).catch(() => {
      });
      if (workspaceDir) {
        Object.assign(
          configRC,
          rc9.read({ name: options.rcFile, dir: workspaceDir })
        );
      }
    }
    Object.assign(
      configRC,
      rc9.read({ name: options.rcFile, dir: options.cwd })
    );
  }
  const pkgJson = {};
  if (options.packageJson) {
    const keys = (Array.isArray(options.packageJson) ? options.packageJson : [
      typeof options.packageJson === "string" ? options.packageJson : options.name
    ]).filter((t) => t && typeof t === "string");
    const pkgJsonFile = await readPackageJSON(options.cwd).catch(() => {
    });
    const values = keys.map((key) => pkgJsonFile?.[key]);
    Object.assign(pkgJson, defu({}, ...values));
  }
  r.config = defu(
    options.overrides,
    config,
    configRC,
    pkgJson,
    options.defaultConfig
  );
  if (options.extend) {
    await extendConfig(r.config, options);
    r.layers = r.config._layers;
    delete r.config._layers;
    r.config = defu(r.config, ...r.layers.map((e) => e.config));
  }
  const baseLayers = [
    options.overrides && {
      config: options.overrides,
      configFile: void 0,
      cwd: void 0
    },
    { config, configFile: options.configFile, cwd: options.cwd },
    options.rcFile && { config: configRC, configFile: options.rcFile },
    options.packageJson && { config: pkgJson, configFile: "package.json" }
  ].filter((l) => l && l.config);
  r.layers = [...baseLayers, ...r.layers];
  if (options.defaults) {
    r.config = defu(r.config, options.defaults);
  }
  return r;
}
async function extendConfig(config, options) {
  config._layers = config._layers || [];
  if (!options.extend) {
    return;
  }
  let keys = options.extend.extendKey;
  if (typeof keys === "string") {
    keys = [keys];
  }
  const extendSources = [];
  for (const key of keys) {
    extendSources.push(
      ...(Array.isArray(config[key]) ? config[key] : [config[key]]).filter(
        Boolean
      )
    );
    delete config[key];
  }
  for (let extendSource of extendSources) {
    const originalExtendSource = extendSource;
    let sourceOptions = {};
    if (extendSource.source) {
      sourceOptions = extendSource.options || {};
      extendSource = extendSource.source;
    }
    if (Array.isArray(extendSource)) {
      sourceOptions = extendSource[1] || {};
      extendSource = extendSource[0];
    }
    if (typeof extendSource !== "string") {
      console.warn(
        `Cannot extend config from \`${JSON.stringify(
          originalExtendSource
        )}\` in ${options.cwd}`
      );
      continue;
    }
    const _config = await resolveConfig(extendSource, options, sourceOptions);
    if (!_config.config) {
      console.warn(
        `Cannot extend config from \`${extendSource}\` in ${options.cwd}`
      );
      continue;
    }
    await extendConfig(_config.config, { ...options, cwd: _config.cwd });
    config._layers.push(_config);
    if (_config.config._layers) {
      config._layers.push(..._config.config._layers);
      delete _config.config._layers;
    }
  }
}
const GIT_PREFIXES = ["github:", "gitlab:", "bitbucket:", "https://"];
const NPM_PACKAGE_RE = /^(@[\da-z~-][\d._a-z~-]*\/)?[\da-z~-][\d._a-z~-]*($|\/.*)/;
async function resolveConfig(source, options, sourceOptions = {}) {
  if (options.resolve) {
    const res2 = await options.resolve(source, options);
    if (res2) {
      return res2;
    }
  }
  if (GIT_PREFIXES.some((prefix) => source.startsWith(prefix))) {
    const { downloadTemplate } = await import('giget');
    const url = new URL(source);
    const gitRepo = url.protocol + url.pathname.split("/").slice(0, 2).join("/");
    const name = gitRepo.replace(/[#/:@\\]/g, "_");
    const tmpDir = process.env.XDG_CACHE_HOME ? resolve(process.env.XDG_CACHE_HOME, "c12", name) : resolve(homedir(), ".cache/c12", name);
    if (existsSync(tmpDir)) {
      await rm(tmpDir, { recursive: true });
    }
    const cloned = await downloadTemplate(source, { dir: tmpDir });
    source = cloned.dir;
  }
  if (NPM_PACKAGE_RE.test(source)) {
    try {
      source = options.jiti.resolve(source, { paths: [options.cwd] });
    } catch {
    }
  }
  const isDir = !extname(source);
  const cwd = resolve(options.cwd, isDir ? source : dirname(source));
  if (isDir) {
    source = options.configFile;
  }
  const res = {
    config: void 0,
    cwd,
    source,
    sourceOptions
  };
  try {
    res.configFile = options.jiti.resolve(resolve(cwd, source), {
      paths: [cwd]
    });
  } catch {
  }
  if (!existsSync(res.configFile)) {
    return res;
  }
  res.config = options.jiti(res.configFile);
  if (res.config instanceof Function) {
    res.config = await res.config();
  }
  if (options.envName) {
    const envConfig = {
      ...res.config["$" + options.envName],
      ...res.config.$env?.[options.envName]
    };
    if (Object.keys(envConfig).length > 0) {
      res.config = defu(envConfig, res.config);
    }
  }
  res.meta = defu(res.sourceOptions.meta, res.config.$meta);
  delete res.config.$meta;
  if (res.sourceOptions.overrides) {
    res.config = defu(res.sourceOptions.overrides, res.config);
  }
  return res;
}

function createDefineConfig() {
  return (input) => input;
}

const eventMap = {
  add: "created",
  change: "updated",
  unlink: "removed"
};
async function watchConfig(options) {
  let config = await loadConfig(options);
  const configName = options.name || "config";
  const configFileName = options.configFile ?? (options.name !== "config" ? `${options.name}.config` : "config");
  const watchingFiles = [
    ...new Set(
      (config.layers || []).filter((l) => l.cwd).flatMap((l) => [
        ...["ts", "js", "mjs", "cjs", "cts", "mts", "json"].map(
          (ext) => resolve(l.cwd, configFileName + "." + ext)
        ),
        l.source && resolve(l.cwd, l.source),
        // TODO: Support watching rc from home and workspace
        options.rcFile && resolve(
          l.cwd,
          typeof options.rcFile === "string" ? options.rcFile : `.${configName}rc`
        ),
        options.packageJson && resolve(l.cwd, "package.json")
      ]).filter(Boolean)
    )
  ];
  const _fswatcher = watch(watchingFiles, {
    ignoreInitial: true,
    ...options.chokidarOptions
  });
  const onChange = async (event, path) => {
    const type = eventMap[event];
    if (!type) {
      return;
    }
    if (options.onWatch) {
      await options.onWatch({
        type,
        path
      });
    }
    const oldConfig = config;
    const newConfig = await loadConfig(options);
    config = newConfig;
    const changeCtx = {
      newConfig,
      oldConfig,
      getDiff: () => diff(oldConfig.config, config.config)
    };
    if (options.acceptHMR) {
      const changeHandled = await options.acceptHMR(changeCtx);
      if (changeHandled) {
        return;
      }
    }
    if (options.onUpdate) {
      await options.onUpdate(changeCtx);
    }
  };
  if (options.debounce !== false) {
    _fswatcher.on("all", debounce(onChange, options.debounce ?? 100));
  } else {
    _fswatcher.on("all", onChange);
  }
  const utils = {
    watchingFiles,
    unwatch: async () => {
      await _fswatcher.close();
    }
  };
  return new Proxy(utils, {
    get(_, prop) {
      if (prop in utils) {
        return utils[prop];
      }
      return config[prop];
    }
  });
}

export { createDefineConfig, loadConfig, loadDotenv, setupDotenv, watchConfig };
