import { access, readFile } from "node:fs/promises";
import { constants } from "node:fs";
import { isIP } from "node:net";
import { resolve } from "node:path";
import { validatePermissionCeiling } from "./permission-ceiling.js";

export const rootDir = resolve(import.meta.dirname, "../../..");
export const currentConfigSchemaVersion = 1;

export async function loadConfig() {
  const explicitPath = process.env.CODE_REMOTE_MANAGER_CONFIG_PATH;
  if (explicitPath) {
    return loadConfigFromPath(resolve(explicitPath));
  }

  const localPath = resolve(rootDir, "config.local.json");
  const examplePath = resolve(rootDir, "config.example.json");
  const configPath = await fileExists(localPath) ? localPath : examplePath;
  return loadConfigFromPath(configPath);
}

export async function loadConfigFromPath(configPath) {
  let rawConfig;
  try {
    rawConfig = await readFile(configPath, "utf8");
  } catch {
    throw new Error("configuration_read_failed");
  }

  let config;
  try {
    config = JSON.parse(rawConfig.replace(/^\uFEFF/, ""));
  } catch {
    throw new Error("configuration_json_invalid");
  }

  validateConfig(config);
  return { config, configPath };
}

export async function fileExists(path) {
  try {
    await access(path, constants.F_OK);
    return true;
  } catch {
    return false;
  }
}

export function validateConfig(config) {
  if (config?.schemaVersion !== undefined && config.schemaVersion !== currentConfigSchemaVersion) {
    throw new Error("configuration_schema_unsupported");
  }

  const requiredStringKeys = ["codexCliPath", "bindHost", "dataDir"];
  for (const key of requiredStringKeys) {
    if (typeof config[key] !== "string" || config[key].length === 0) {
      throw new Error("configuration_invalid");
    }
  }

  if (!isLoopbackBindHost(config.bindHost)) {
    throw new Error("configuration_bind_host_not_loopback");
  }

  if (!Number.isInteger(config.port) || config.port <= 0) {
    throw new Error("configuration_invalid");
  }

  if (!["fake", "codex"].includes(config.engine)) {
    throw new Error("configuration_invalid");
  }

  if (config.projectsRoot !== undefined && (typeof config.projectsRoot !== "string" || config.projectsRoot.length === 0)) {
    throw new Error("configuration_invalid");
  }

  if (config.allowedOrigins !== undefined && (!Array.isArray(config.allowedOrigins) || config.allowedOrigins.some((origin) => typeof origin !== "string" || origin.length === 0))) {
    throw new Error("configuration_invalid");
  }

  if (config.deviceAuthentication !== undefined && (
    !config.deviceAuthentication ||
    typeof config.deviceAuthentication !== "object" ||
    Array.isArray(config.deviceAuthentication) ||
    typeof config.deviceAuthentication.required !== "boolean"
  )) {
    throw new Error("configuration_invalid");
  }

  validatePermissionCeiling(config.permissionCeiling);

  validateList(config.accounts, ["id", "label", "codexHome"], { allowEmpty: true });
  validateList(config.workspaces, ["id", "label", "path"]);
}

function isLoopbackBindHost(host) {
  const family = isIP(host);
  if (family === 4) return host.split(".", 1)[0] === "127";
  if (family !== 6) return false;
  try {
    return new URL(`http://[${host}]`).hostname === "[::1]";
  } catch {
    return false;
  }
}

function validateList(value, keys, { allowEmpty = false } = {}) {
  if (!Array.isArray(value) || (!allowEmpty && value.length === 0)) {
    throw new Error("configuration_invalid");
  }

  const ids = new Set();
  for (const item of value) {
    for (const key of keys) {
      if (typeof item[key] !== "string" || item[key].length === 0) {
        throw new Error("configuration_invalid");
      }
    }

    if (ids.has(item.id)) {
      throw new Error("configuration_duplicate_id");
    }
    ids.add(item.id);
  }
}
