import { randomBytes } from "node:crypto";
import {
  readFile as nodeReadFile,
  writeFile as nodeWriteFile,
  rename as nodeRename,
  mkdir as nodeMkdir,
  readdir as nodeReaddir,
  unlink as nodeUnlink,
  stat as nodeStat,
  lstat as nodeLstat
} from "node:fs/promises";
import { dirname, join } from "node:path";
import { createLogger } from "../../../logger.js";
const log = createLogger("cli");
const CONFIG_FILE_MODE = 384;
const BACKUP_DIR_MODE = 448;
const BACKUP_RETENTION = 5;
function defaultWritableFs() {
  return {
    async readFile(p) {
      return nodeReadFile(p, "utf-8");
    },
    async writeFile(p, data) {
      await nodeWriteFile(p, data, { mode: CONFIG_FILE_MODE });
    },
    async rename(from, to) {
      await nodeRename(from, to);
    },
    async mkdir(p, opts) {
      await nodeMkdir(p, { recursive: opts?.recursive ?? true, mode: opts?.mode });
    },
    async readdir(p) {
      return nodeReaddir(p);
    },
    async unlink(p) {
      await nodeUnlink(p);
    },
    async stat(p) {
      const s = await nodeStat(p);
      return {
        isFile: () => s.isFile(),
        isDirectory: () => s.isDirectory(),
        isSymbolicLink: () => s.isSymbolicLink()
      };
    },
    async lstat(p) {
      const s = await nodeLstat(p);
      return {
        isFile: () => s.isFile(),
        isDirectory: () => s.isDirectory(),
        isSymbolicLink: () => s.isSymbolicLink()
      };
    }
  };
}
function isJsonObject(v) {
  return typeof v === "object" && v !== null && !Array.isArray(v);
}
function parseJsonOrEmpty(text) {
  try {
    const parsed = JSON.parse(text);
    return isJsonObject(parsed) ? parsed : {};
  } catch {
    return {};
  }
}
function ensureNested(root, path) {
  let cur = root;
  for (const key of path) {
    const next = cur[key];
    if (isJsonObject(next)) {
      cur = next;
    } else {
      const fresh = {};
      cur[key] = fresh;
      cur = fresh;
    }
  }
  return cur;
}
function navigateParent(root, path) {
  if (path.length === 0) return null;
  let cur = root;
  for (let i = 0; i < path.length - 1; i++) {
    const next = cur[path[i]];
    if (!isJsonObject(next)) return null;
    cur = next;
  }
  return cur;
}
async function atomicWriteJson(fs, path, payload) {
  const dir = dirname(path);
  await fs.mkdir(dir, { recursive: true });
  const tmp = join(dir, `.tmp.${process.pid}.${randomBytes(6).toString("hex")}`);
  const serialized = JSON.stringify(payload, null, 2);
  await fs.writeFile(tmp, serialized);
  try {
    await fs.rename(tmp, path);
  } catch (err) {
    try {
      await fs.unlink(tmp);
    } catch {
    }
    throw err;
  }
}
function buildIndex(catalog) {
  const bySettingsPath = /* @__PURE__ */ new Map();
  for (const cat of catalog) {
    for (const field of cat.fields) {
      bySettingsPath.set(field.settingsPath, field);
    }
  }
  return { bySettingsPath };
}
function errorMessage(err) {
  if (err instanceof Error) return err.message;
  return String(err);
}
async function refuseSymlink(fs, path) {
  let s;
  try {
    s = await fs.lstat(path);
  } catch (err) {
    const code = err.code;
    if (code === "ENOENT") return;
    throw err;
  }
  if (s.isSymbolicLink()) {
    throw new Error(`refused: symlink at ${path}`);
  }
}
function backupFilename(agentId) {
  const iso = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
  return `${agentId}-${iso}.json`;
}
async function writeAgentBackup(fs, target, rawConfig) {
  const dir = target.backupDir();
  await fs.mkdir(dir, { recursive: true, mode: BACKUP_DIR_MODE });
  const file = join(dir, backupFilename(target.id));
  await fs.writeFile(file, rawConfig);
}
async function pruneBackups(fs, target) {
  const dir = target.backupDir();
  let entries;
  try {
    entries = await fs.readdir(dir);
  } catch {
    return;
  }
  const prefix = `${target.id}-`;
  const mine = entries.filter((f) => f.startsWith(prefix)).sort();
  const excess = mine.length - BACKUP_RETENTION;
  if (excess <= 0) return;
  for (const f of mine.slice(0, excess)) {
    try {
      await fs.unlink(join(dir, f));
    } catch {
    }
  }
}
async function save(opts) {
  const fs = opts.fs ?? defaultWritableFs();
  const index = buildIndex(opts.catalog);
  const pending = opts.store.getPending();
  const errors = [];
  for (const [key, value] of Object.entries(pending)) {
    const field = index.bySettingsPath.get(key);
    if (!field?.validate) continue;
    const err = field.validate(value);
    if (err) errors.push({ key, reason: err });
  }
  if (errors.length > 0) {
    return { saved: [], propagated: [], failed: [], errors };
  }
  const secretWrites = [];
  const plainSettings = {};
  const settingsKeys = [];
  for (const [key, value] of Object.entries(pending)) {
    const field = index.bySettingsPath.get(key);
    if (field?.secret === true) {
      if (typeof value === "string" && value.length > 0) {
        secretWrites.push({ key, value, field });
      } else {
        secretWrites.push({ key, value: "", field });
      }
      settingsKeys.push(key);
      continue;
    }
    plainSettings[key] = value;
    settingsKeys.push(key);
  }
  const secretLocations = {};
  const propagationSet = {};
  for (const w of secretWrites) {
    if (w.value === "") {
      try {
        await opts.secretStore.remove(w.key);
      } catch (err) {
        return {
          saved: [],
          propagated: [],
          failed: [{ agentId: "__secret__", reason: errorMessage(err) }]
        };
      }
      secretLocations[w.key] = null;
      continue;
    }
    try {
      const result = await opts.secretStore.set(w.key, w.value);
      secretLocations[w.key] = result.location;
      if (w.field.propagateToAgents !== false) {
        propagationSet[w.field.key] = w.value;
      }
    } catch (err) {
      return {
        saved: [],
        propagated: [],
        failed: [{ agentId: "__secret__", reason: errorMessage(err) }]
      };
    }
  }
  try {
    await refuseSymlink(fs, opts.configPath);
  } catch (err) {
    return {
      saved: [],
      propagated: [],
      failed: [{ agentId: "__config__", reason: errorMessage(err) }],
      errors: [{ key: "__config__", reason: errorMessage(err) }]
    };
  }
  let existingCfg = {};
  try {
    const raw = await fs.readFile(opts.configPath);
    existingCfg = parseJsonOrEmpty(raw);
  } catch {
  }
  const existingSettings = isJsonObject(existingCfg.settings) ? existingCfg.settings : {};
  const mergedSettings = { ...existingSettings, ...plainSettings };
  for (const [key, location] of Object.entries(secretLocations)) {
    if (location === null) {
      delete mergedSettings[`${key}KeyLocation`];
    } else {
      mergedSettings[`${key}KeyLocation`] = location;
    }
  }
  const nextCfg = {
    version: typeof existingCfg.version === "number" ? existingCfg.version : 1,
    settings: mergedSettings
  };
  if (isJsonObject(existingCfg.provider)) nextCfg.provider = existingCfg.provider;
  try {
    await atomicWriteJson(fs, opts.configPath, nextCfg);
  } catch (err) {
    return {
      saved: [],
      propagated: [],
      failed: [{ agentId: "__config__", reason: errorMessage(err) }],
      errors: [{ key: "__config__", reason: errorMessage(err) }]
    };
  }
  for (const [settingsPath, value] of Object.entries(plainSettings)) {
    const field = index.bySettingsPath.get(settingsPath);
    if (!field) continue;
    if (field.propagateToAgents === false) continue;
    propagationSet[field.key] = String(value);
  }
  const propagated = [];
  const failed = [];
  if (Object.keys(propagationSet).length > 0) {
    const results = await Promise.all(
      opts.agents.map(async (target) => {
        try {
          const detected = await target.detect();
          if (!detected) return { id: target.id, ok: false, skipped: true };
          await applyPropagationToAgent(fs, target, propagationSet);
          await pruneBackups(fs, target);
          return { id: target.id, ok: true, skipped: false };
        } catch (err) {
          return { id: target.id, ok: false, skipped: false, reason: errorMessage(err) };
        }
      })
    );
    for (const r of results) {
      if (r.ok) {
        propagated.push(r.id);
      } else if (!r.skipped) {
        failed.push({ agentId: r.id, reason: r.reason ?? "unknown" });
        log.warn("agent propagation failed", { agent: r.id, reason: r.reason });
      }
    }
  }
  opts.store.commit();
  return { saved: settingsKeys, propagated, failed };
}
async function applyPropagationToAgent(fs, target, propagationSet) {
  await refuseSymlink(fs, target.configPath);
  const raw = await fs.readFile(target.configPath);
  await writeAgentBackup(fs, target, raw);
  const root = parseJsonOrEmpty(raw);
  const envBlock = ensureNested(root, target.envPath);
  for (const [k, v] of Object.entries(propagationSet)) {
    envBlock[k] = v;
  }
  await atomicWriteJson(fs, target.configPath, root);
}
async function installAgent(opts) {
  const fs = opts.fs ?? defaultWritableFs();
  try {
    await refuseSymlink(fs, opts.target.configPath);
  } catch (err) {
    return { ok: false, reason: errorMessage(err) };
  }
  let raw = null;
  try {
    raw = await fs.readFile(opts.target.configPath);
  } catch (err) {
    const code = err.code;
    if (code !== "ENOENT") {
      return { ok: false, reason: errorMessage(err) };
    }
  }
  if (raw !== null) {
    try {
      await writeAgentBackup(fs, opts.target, raw);
    } catch (err) {
      return { ok: false, reason: errorMessage(err) };
    }
  }
  const root = raw !== null ? parseJsonOrEmpty(raw) : {};
  const serverEntry = ensureNested(root, opts.target.serverPath);
  serverEntry.command = "npx";
  serverEntry.args = ["-y", "@staticn0va/wigolo"];
  const envBlock = ensureNested(root, opts.target.envPath);
  for (const [k, v] of Object.entries(opts.env)) {
    envBlock[k] = v;
  }
  try {
    await atomicWriteJson(fs, opts.target.configPath, root);
  } catch (err) {
    return { ok: false, reason: errorMessage(err) };
  }
  await pruneBackups(fs, opts.target);
  return { ok: true };
}
async function uninstallAgent(opts) {
  const fs = opts.fs ?? defaultWritableFs();
  try {
    await refuseSymlink(fs, opts.target.configPath);
  } catch (err) {
    return { ok: false, reason: errorMessage(err) };
  }
  let raw;
  try {
    raw = await fs.readFile(opts.target.configPath);
  } catch (err) {
    const code = err.code;
    if (code === "ENOENT") return { ok: true };
    return { ok: false, reason: errorMessage(err) };
  }
  try {
    await writeAgentBackup(fs, opts.target, raw);
  } catch (err) {
    return { ok: false, reason: errorMessage(err) };
  }
  const root = parseJsonOrEmpty(raw);
  const parent = navigateParent(root, opts.target.serverPath);
  if (parent) {
    const leaf = opts.target.serverPath[opts.target.serverPath.length - 1];
    delete parent[leaf];
  } else {
  }
  try {
    await atomicWriteJson(fs, opts.target.configPath, root);
  } catch (err) {
    return { ok: false, reason: errorMessage(err) };
  }
  await pruneBackups(fs, opts.target);
  return { ok: true };
}
export {
  defaultWritableFs,
  installAgent,
  save,
  uninstallAgent
};
//# sourceMappingURL=propagation.js.map