'use strict';

const node_fs = require('node:fs');
const pathe = require('pathe');
const dotenv = require('dotenv');
const promises = require('node:fs/promises');
const node_os = require('node:os');
const createJiti = require('jiti');
const rc9 = require('rc9');
const defu = require('defu');
const pkgTypes = require('pkg-types');
const chokidar = require('chokidar');
const perfectDebounce = require('perfect-debounce');
const ohash = require('ohash');

function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }

function _interopNamespaceCompat(e) {
  if (e && typeof e === 'object' && 'default' in e) return e;
  const n = Object.create(null);
  if (e) {
    for (const k in e) {
      n[k] = e[k];
    }
  }
  n.default = e;
  return n;
}

const dotenv__namespace = /*#__PURE__*/_interopNamespaceCompat(dotenv);
const createJiti__default = /*#__PURE__*/_interopDefaultCompat(createJiti);
const rc9__namespace = /*#__PURE__*/_interopNamespaceCompat(rc9);

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 = pathe.resolve(options.cwd, options.fileName);
  if (node_fs.existsSync(dotenvFile)) {
    const parsed = dotenv__namespace.parse(await node_fs.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 = pathe.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__default(void 0, {
    interopDefault: true,
    requireCache: false,
    esmResolve: true,
    ...options.jitiOptions
  });
  const r = {
    config: {},
    cwd: options.cwd,
    configFile: pathe.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__namespace.readUser({ name: options.rcFile, dir: options.cwd })
      );
      const workspaceDir = await pkgTypes.findWorkspaceDir(options.cwd).catch(() => {
      });
      if (workspaceDir) {
        Object.assign(
          configRC,
          rc9__namespace.read({ name: options.rcFile, dir: workspaceDir })
        );
      }
    }
    Object.assign(
      configRC,
      rc9__namespace.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 pkgTypes.readPackageJSON(options.cwd).catch(() => {
    });
    const values = keys.map((key) => pkgJsonFile?.[key]);
    Object.assign(pkgJson, defu.defu({}, ...values));
  }
  r.config = defu.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.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.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 ? pathe.resolve(process.env.XDG_CACHE_HOME, "c12", name) : pathe.resolve(node_os.homedir(), ".cache/c12", name);
    if (node_fs.existsSync(tmpDir)) {
      await promises.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 = !pathe.extname(source);
  const cwd = pathe.resolve(options.cwd, isDir ? source : pathe.dirname(source));
  if (isDir) {
    source = options.configFile;
  }
  const res = {
    config: void 0,
    cwd,
    source,
    sourceOptions
  };
  try {
    res.configFile = options.jiti.resolve(pathe.resolve(cwd, source), {
      paths: [cwd]
    });
  } catch {
  }
  if (!node_fs.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.defu(envConfig, res.config);
    }
  }
  res.meta = defu.defu(res.sourceOptions.meta, res.config.$meta);
  delete res.config.$meta;
  if (res.sourceOptions.overrides) {
    res.config = defu.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) => pathe.resolve(l.cwd, configFileName + "." + ext)
        ),
        l.source && pathe.resolve(l.cwd, l.source),
        // TODO: Support watching rc from home and workspace
        options.rcFile && pathe.resolve(
          l.cwd,
          typeof options.rcFile === "string" ? options.rcFile : `.${configName}rc`
        ),
        options.packageJson && pathe.resolve(l.cwd, "package.json")
      ]).filter(Boolean)
    )
  ];
  const _fswatcher = chokidar.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: () => ohash.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", perfectDebounce.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];
    }
  });
}

exports.createDefineConfig = createDefineConfig;
exports.loadConfig = loadConfig;
exports.loadDotenv = loadDotenv;
exports.setupDotenv = setupDotenv;
exports.watchConfig = watchConfig;
