import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, readdirSync, rmdirSync } from "node:fs";
import { join, dirname, basename, resolve, extname } from "node:path";
import { TEMPLATES, MCP_CONFIGS } from "./templates.js";
import { VERSION } from "../version.js";
function getSentinelMarkers(filename) {
  const ext = extname(filename).toLowerCase();
  const base = basename(filename).toLowerCase();
  if ([".yaml", ".yml", ".sh", ".bash", ".py", ".toml", ".conf"].includes(ext) || base === ".clinerules") {
    return {
      open: (v) => `# mapx v${v}`,
      close: "# /mapx",
      regex: /#\s*mapx\s+v([\d.]+)\s*\n([\s\S]*?)#\s*\/mapx/i
    };
  }
  return {
    open: (v) => `<!-- mapx v${v} -->`,
    close: "<!-- /mapx -->",
    regex: /<!--\s*mapx\s+v([\d.]+)\s*-->([\s\S]*?)<!--\s*\/mapx\s*-->/i
  };
}
class AgentGenerator {
  version;
  constructor() {
    this.version = VERSION;
  }
  getVersion() {
    return this.version;
  }
  listProviders() {
    return Object.keys(TEMPLATES).filter((p) => p !== "instructions");
  }
  getTemplate(provider) {
    return TEMPLATES[provider];
  }
  substitute(content, dir, mcpPort = 3456) {
    const projectDir = resolve(dir);
    const projectName = basename(projectDir);
    return content.replaceAll("{{PROJECT_NAME}}", projectName).replaceAll("{{PROJECT_DIR}}", projectDir).replaceAll("{{MAPX_VERSION}}", this.version).replaceAll("{{MCP_PORT}}", mcpPort.toString());
  }
  plan(providers, options) {
    const actions = [];
    const dir = options.dir;
    const mcpPort = options.mcpPort || 3456;
    const targetProviders = [...providers];
    if (!targetProviders.includes("instructions")) {
      targetProviders.push("instructions");
    }
    for (const provider of targetProviders) {
      const template = TEMPLATES[provider];
      if (!template) continue;
      const filepath = join(dir, template.filename);
      const rawContent = this.substitute(template.content, dir, mcpPort);
      const markers = getSentinelMarkers(template.filename);
      const wrappedNewContent = `${markers.open(this.version)}
${rawContent}
${markers.close}`;
      if (!existsSync(filepath)) {
        actions.push({
          provider,
          filename: template.filename,
          filepath,
          status: template.isAppend ? "append" : "create",
          newContent: wrappedNewContent
        });
        continue;
      }
      const existingFileContent = readFileSync(filepath, "utf-8");
      const match = existingFileContent.match(markers.regex);
      if (!match) {
        if (template.isAppend) {
          actions.push({
            provider,
            filename: template.filename,
            filepath,
            status: "append",
            oldContent: existingFileContent,
            newContent: existingFileContent.endsWith("\n") ? `${existingFileContent}
${wrappedNewContent}` : `${existingFileContent}

${wrappedNewContent}`
          });
        } else {
          actions.push({
            provider,
            filename: template.filename,
            filepath,
            status: "no_sentinel",
            oldContent: existingFileContent,
            newContent: wrappedNewContent,
            diff: this.diff(existingFileContent, wrappedNewContent)
          });
        }
        continue;
      }
      const fileVersion = match[1];
      const fileContentInside = match[2];
      const expectedContentOld = fileContentInside;
      const expectedContentNew = rawContent;
      if (expectedContentOld.trim() === expectedContentNew.trim() && fileVersion === this.version) {
        actions.push({
          provider,
          filename: template.filename,
          filepath,
          status: "up_to_date",
          oldContent: existingFileContent,
          newContent: existingFileContent
        });
        continue;
      }
      const newFileContent = existingFileContent.replace(markers.regex, wrappedNewContent);
      actions.push({
        provider,
        filename: template.filename,
        filepath,
        status: fileVersion === this.version ? "update_conflict" : "update_clean",
        oldContent: existingFileContent,
        newContent: newFileContent,
        diff: this.diff(fileContentInside, rawContent)
      });
    }
    return actions;
  }
  execute(action) {
    if (action.status === "up_to_date") return;
    const parentDir = dirname(action.filepath);
    if (!existsSync(parentDir)) {
      mkdirSync(parentDir, { recursive: true });
    }
    writeFileSync(action.filepath, action.newContent, "utf-8");
  }
  revert(options) {
    const dir = options.dir;
    for (const provider of Object.keys(TEMPLATES)) {
      const template = TEMPLATES[provider];
      if (!template) continue;
      const filepath = join(dir, template.filename);
      if (!existsSync(filepath)) continue;
      try {
        const content = readFileSync(filepath, "utf-8");
        const markers = getSentinelMarkers(template.filename);
        const match = content.match(markers.regex);
        if (match) {
          const cleaned = content.replace(markers.regex, "").trim();
          if (cleaned.length === 0) {
            unlinkSync(filepath);
            console.log(`  \u2713 Removed ${template.filename}`);
            let parentDir = dirname(filepath);
            const resolvedDir = resolve(dir);
            while (parentDir !== resolvedDir && parentDir !== "/" && parentDir !== ".") {
              if (existsSync(parentDir) && readdirSync(parentDir).length === 0) {
                rmdirSync(parentDir);
                parentDir = dirname(parentDir);
              } else {
                break;
              }
            }
          } else {
            writeFileSync(filepath, cleaned, "utf-8");
            console.log(`  \u2713 Cleaned mapx integration from ${template.filename}`);
          }
        }
      } catch (err) {
        console.error(`  \u2717 Failed to revert agent file ${template.filename}: ${err.message}`);
      }
    }
  }
  /**
   * Detect which agent tools are present in the project directory.
   * Returns the list of MCP config entries that should be generated.
   */
  detectAgentTools(dir) {
    return MCP_CONFIGS.filter((entry) => entry.detect(dir));
  }
  /**
   * List all available MCP config targets.
   */
  listMcpConfigs() {
    return MCP_CONFIGS;
  }
  /**
   * Plan and generate MCP config files for detected (or specified) agent tools.
   * For JSON config files that already exist, we merge the mapx entry into them
   * rather than overwriting the entire file.
   */
  generateMcpConfigs(tools, options) {
    const dir = resolve(options.dir);
    const actions = [];
    for (const entry of tools) {
      const filepath = join(dir, entry.filename);
      const newContent = entry.generate(dir);
      if (!existsSync(filepath)) {
        actions.push({
          tool: entry.name,
          filename: entry.filename,
          filepath,
          status: "create",
          content: newContent
        });
        continue;
      }
      try {
        const existingRaw = readFileSync(filepath, "utf-8");
        const existing = JSON.parse(existingRaw);
        const newObj = JSON.parse(newContent);
        const merged = this.mergeMapxIntoConfig(existing, newObj, entry.name);
        const mergedStr = JSON.stringify(merged, null, 2);
        if (existingRaw.trim() === mergedStr.trim()) {
          actions.push({
            tool: entry.name,
            filename: entry.filename,
            filepath,
            status: "up_to_date",
            content: mergedStr
          });
        } else {
          actions.push({
            tool: entry.name,
            filename: entry.filename,
            filepath,
            status: "merge",
            content: mergedStr
          });
        }
      } catch {
        actions.push({
          tool: entry.name,
          filename: entry.filename,
          filepath,
          status: "update",
          content: newContent
        });
      }
    }
    return actions;
  }
  /**
   * Execute MCP config actions (write files to disk).
   */
  executeMcpConfig(action) {
    if (action.status === "up_to_date") return;
    const parentDir = dirname(action.filepath);
    if (!existsSync(parentDir)) {
      mkdirSync(parentDir, { recursive: true });
    }
    writeFileSync(action.filepath, action.content + "\n", "utf-8");
  }
  /**
   * Merge the mapx MCP entry into an existing config object without
   * destroying other entries. Handles the different config shapes:
   * - opencode: { mcp: { mapx: ... } }
   * - gemini-cli: { mcpServers: { mapx: ... } }
   * - cursor/vscode: { mcpServers/servers: { mapx: ... } }
   */
  mergeMapxIntoConfig(existing, newObj, toolName) {
    const merged = { ...existing };
    for (const key of Object.keys(newObj)) {
      if (key === "$schema") {
        if (!merged.$schema) merged.$schema = newObj.$schema;
        continue;
      }
      const val = newObj[key];
      if (typeof val === "object" && val !== null && "mapx" in val) {
        if (!merged[key]) merged[key] = {};
        merged[key] = { ...merged[key], mapx: val.mapx };
      }
    }
    return merged;
  }
  /**
   * Remove mapx entries from MCP config files during uninit.
   * If the config file has only mapx entries, delete the file entirely.
   */
  revertMcpConfigs(options) {
    const dir = resolve(options.dir);
    for (const entry of MCP_CONFIGS) {
      const filepath = join(dir, entry.filename);
      if (!existsSync(filepath)) continue;
      try {
        const raw = readFileSync(filepath, "utf-8");
        const obj = JSON.parse(raw);
        let modified = false;
        for (const key of ["mcp", "mcpServers", "servers"]) {
          if (obj[key] && typeof obj[key] === "object" && "mapx" in obj[key]) {
            delete obj[key].mapx;
            modified = true;
            if (Object.keys(obj[key]).length === 0) {
              delete obj[key];
            }
          }
        }
        if (!modified) continue;
        const remaining = Object.keys(obj).filter((k) => k !== "$schema");
        if (remaining.length === 0) {
          unlinkSync(filepath);
          console.log(`  \u2713 Removed ${entry.filename}`);
          let parentDir = dirname(filepath);
          const resolvedDir = resolve(dir);
          while (parentDir !== resolvedDir && parentDir !== "/" && parentDir !== ".") {
            if (existsSync(parentDir) && readdirSync(parentDir).length === 0) {
              rmdirSync(parentDir);
              parentDir = dirname(parentDir);
            } else {
              break;
            }
          }
        } else {
          writeFileSync(filepath, JSON.stringify(obj, null, 2) + "\n", "utf-8");
          console.log(`  \u2713 Removed mapx entry from ${entry.filename}`);
        }
      } catch (err) {
        console.error(`  \u2717 Failed to revert MCP config ${entry.filename}: ${err.message}`);
      }
    }
  }
  diff(oldStr, newStr) {
    const oldLines = oldStr.split("\n");
    const newLines = newStr.split("\n");
    const diff = [];
    let i = 0, j = 0;
    while (i < oldLines.length || j < newLines.length) {
      if (i < oldLines.length && j < newLines.length) {
        if (oldLines[i] === newLines[j]) {
          diff.push(`  ${oldLines[i]}`);
          i++;
          j++;
        } else {
          let found = false;
          for (let k = 1; k < 5; k++) {
            if (i + k < oldLines.length && oldLines[i + k] === newLines[j]) {
              for (let m = 0; m < k; m++) {
                diff.push(`- ${oldLines[i + m]}`);
              }
              i += k;
              found = true;
              break;
            }
            if (j + k < newLines.length && oldLines[i] === newLines[j + k]) {
              for (let m = 0; m < k; m++) {
                diff.push(`+ ${newLines[j + m]}`);
              }
              j += k;
              found = true;
              break;
            }
          }
          if (!found) {
            diff.push(`- ${oldLines[i]}`);
            diff.push(`+ ${newLines[j]}`);
            i++;
            j++;
          }
        }
      } else if (i < oldLines.length) {
        diff.push(`- ${oldLines[i]}`);
        i++;
      } else if (j < newLines.length) {
        diff.push(`+ ${newLines[j]}`);
        j++;
      }
    }
    return diff.join("\n");
  }
}
export {
  AgentGenerator
};
