/**
 * Aider adapter — managed-block in `~/.aider.conf.yml` + sidecar
 * context file in the vault.
 *
 * Aider has no native MCP client at the time this adapter was written
 * (see `docs/plans/2026-05-20-multi-runtime-install-design.md` §3.2,
 * §13). The install path is therefore:
 *
 *   1. Generate `<vault>/.open-second-brain/aider-context.md` from
 *      `templates/install/aider-context.md.tmpl` — placeholders are
 *      `{{VAULT}}` and `{{AGENT_NAME}}`.
 *   2. Write a marker-fenced managed block to `~/.aider.conf.yml`
 *      that injects the sidecar into Aider's `read:` list. If the
 *      user already has their own `read:` elsewhere in the same
 *      file, Aider's YAML loader treats it as a duplicate key —
 *      we surface this caveat in the plan's `postNotes` so the
 *      operator can merge by hand.
 *
 * If a future Aider release ships native MCP, switch this adapter to
 * the JSON-merge body. The interface stays the same.
 */

import { existsSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

import { atomicWriteFileSync } from "../../fs-atomic.ts";
import {
  insertManagedBlock,
  removeManagedBlock,
  hasManagedBlock,
  extractManagedBlock,
} from "../managed-block.ts";
import { recordEntry, readManifest, removeEntry } from "../manifest.ts";
import {
  InstallError,
  type ApplyOpts,
  type ApplyResult,
  type DetectResult,
  type InstallAdapter,
  type InstallEnv,
  type InstallPlan,
  type ManifestEntry,
  type McpPayload,
  type UninstallResult,
  type VerifyResult,
} from "../types.ts";
import { defaultRegistry } from "../registry.ts";

const TARGET = "aider";
const LABEL = "Aider";

function repoRoot(): string {
  return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
}

function templatePath(): string {
  return join(repoRoot(), "templates", "install", "aider-context.md.tmpl");
}

function resolveConfPath(env: InstallEnv): string {
  if (env.env["AIDER_CONFIG"]) return env.env["AIDER_CONFIG"];
  const local = join(env.cwd, ".aider.conf.yml");
  if (existsSync(local)) return local;
  return join(env.home, ".aider.conf.yml");
}

function resolveSidecarPath(env: InstallEnv, opts: { aiderContextPath?: string }): string {
  if (opts.aiderContextPath) return opts.aiderContextPath;
  return join(env.vault, ".open-second-brain", "aider-context.md");
}

function readSafe(path: string): string {
  try {
    return readFileSync(path, "utf8");
  } catch {
    return "";
  }
}

function fileMtimeMs(path: string): number | null {
  try {
    return statSync(path).mtimeMs;
  } catch {
    return null;
  }
}

function resolveAgentName(env: InstallEnv, payload: McpPayload): string {
  return payload.full.env?.["VAULT_AGENT_NAME"] ?? env.env["VAULT_AGENT_NAME"] ?? "agent";
}

function renderTemplate(env: InstallEnv, payload: McpPayload): string {
  const tpl = readFileSync(templatePath(), "utf8");
  return tpl
    .replace(/\{\{VAULT\}\}/g, env.vault)
    .replace(/\{\{AGENT_NAME\}\}/g, resolveAgentName(env, payload));
}

function renderManagedBody(sidecarPath: string): string {
  return [
    "# Open Second Brain — agent context.",
    "# Aider auto-includes this file as read-only context.",
    "# Regenerated by `o2b install --target aider --apply` — do not hand-edit.",
    "read:",
    `  - ${sidecarPath}`,
    "",
  ].join("\n");
}

function ensureParent(path: string): void {
  const parent = dirname(path);
  if (!existsSync(parent)) mkdirSync(parent, { recursive: true });
}

export const aiderAdapter: InstallAdapter = {
  target: TARGET,
  label: LABEL,

  detect(env: InstallEnv): DetectResult {
    const path = resolveConfPath(env);
    if (!existsSync(path)) {
      return {
        target: TARGET,
        status: "not-installed",
        configPath: path,
        notes: ["config file missing"],
      };
    }
    const raw = readSafe(path);
    if (hasManagedBlock(raw)) {
      return {
        target: TARGET,
        status: "installed",
        configPath: path,
        notes: [],
      };
    }
    return {
      target: TARGET,
      status: "not-installed",
      configPath: path,
      notes: [],
    };
  },

  plan(_payload: McpPayload, env: InstallEnv): InstallPlan {
    const conf = resolveConfPath(env);
    const sidecar = resolveSidecarPath(env, {});
    return {
      target: TARGET,
      steps: [
        {
          kind: "file-copy",
          path: sidecar,
          preview: `render templates/install/aider-context.md.tmpl → ${sidecar}`,
        },
        {
          kind: "managed-block",
          path: conf,
          preview: `add OSB managed block to ${conf} (read: → ${sidecar})`,
        },
      ],
      postNotes: [
        "If your existing ~/.aider.conf.yml already sets `read:`, YAML treats",
        "duplicate top-level keys as ambiguous. Merge the two `read:` lists",
        "by hand if you keep your own.",
      ],
    };
  },

  apply(_plan: InstallPlan, payload: McpPayload, env: InstallEnv, opts: ApplyOpts): ApplyResult {
    const conf = resolveConfPath(env);
    const sidecar = resolveSidecarPath(env, opts);
    const stored = readManifest(env.vault).installs[TARGET];

    // user-modified-block safety net
    if (stored && existsSync(conf) && !opts.force) {
      const raw = readSafe(conf);
      const current = extractManagedBlock(raw);
      const expected = renderManagedBody(sidecar);
      if (current !== null && current.trim() !== expected.trim()) {
        const appliedAt = Date.parse(stored.applied_at);
        const mtime = fileMtimeMs(conf) ?? 0;
        if (Number.isFinite(appliedAt) && mtime > appliedAt + 1000) {
          throw new InstallError(
            `aider: ${conf} appears hand-edited after the last install ` +
              `(managed block diverges from canonical content).`,
            TARGET,
            "user-modified-block",
            "re-run with --force to overwrite, or run `o2b uninstall --target aider --apply` first",
          );
        }
      }
    }

    // Render sidecar
    const sidecarContent = renderTemplate(env, payload);
    if (!opts.dryRun) {
      ensureParent(sidecar);
      atomicWriteFileSync(sidecar, sidecarContent);
    }

    // Insert managed block
    const confRaw = existsSync(conf) ? readFileSync(conf, "utf8") : "";
    const nextConf = insertManagedBlock(confRaw, renderManagedBody(sidecar));
    if (!opts.dryRun) {
      ensureParent(conf);
      atomicWriteFileSync(conf, nextConf);
    }

    const manifest: ManifestEntry = {
      target: TARGET,
      applied_at: env.now.toISOString(),
      operation: "managed-block",
      config_path: conf,
      owned_paths: [sidecar],
      owned_block_marker: "# >>> open-second-brain managed >>>",
    };
    if (!opts.dryRun) recordEntry(env.vault, manifest);

    return { target: TARGET, manifest, steps_executed: opts.dryRun ? 0 : 2 };
  },

  uninstall(env: InstallEnv, opts: ApplyOpts & { fromSnippet?: boolean }): UninstallResult {
    const stored = readManifest(env.vault).installs[TARGET];
    // Prefer the config path recorded at install time. `AIDER_CONFIG`
    // or `cwd` can change between install and uninstall, and
    // `resolveConfPath(env)` would then return a different file.
    const conf = stored?.config_path ?? resolveConfPath(env);
    const skipped: Array<readonly [string, string]> = [];
    const removed_paths: string[] = [];

    if (!stored && !opts.fromSnippet) {
      throw new InstallError(
        "aider: no install manifest entry found",
        TARGET,
        "manifest-missing",
        "o2b uninstall --target aider --apply --force-from-snippet",
      );
    }

    // Remove managed block from conf file
    if (existsSync(conf)) {
      const raw = readFileSync(conf, "utf8");
      if (hasManagedBlock(raw)) {
        const next = removeManagedBlock(raw);
        if (!opts.dryRun) atomicWriteFileSync(conf, next);
      } else {
        skipped.push([conf, "no managed block present"]);
      }
    } else {
      skipped.push([conf, "config file no longer present"]);
    }

    // Remove sidecar file
    const sidecarTargets = stored?.owned_paths ?? [resolveSidecarPath(env, {})];
    for (const t of sidecarTargets) {
      if (existsSync(t)) {
        if (!opts.dryRun) rmSync(t, { force: true });
        removed_paths.push(t);
      } else {
        skipped.push([t, "sidecar file no longer present"]);
      }
    }

    if (!opts.dryRun) removeEntry(env.vault, TARGET);
    return { target: TARGET, removed_keys: [], removed_paths, skipped };
  },

  verify(env: InstallEnv): VerifyResult {
    const stored = readManifest(env.vault).installs[TARGET];
    if (!stored) {
      return {
        target: TARGET,
        status: "not-installed",
        details: ["no install manifest entry"],
        fix_hint: null,
      };
    }
    const conf = stored.config_path ?? resolveConfPath(env);
    if (!existsSync(conf)) {
      return {
        target: TARGET,
        status: "drift",
        details: [`config file missing: ${conf}`],
        fix_hint: "o2b install --target aider --apply",
      };
    }
    const raw = readSafe(conf);
    if (!hasManagedBlock(raw)) {
      return {
        target: TARGET,
        status: "drift",
        details: [`managed block absent from ${conf}`],
        fix_hint: "o2b install --target aider --apply",
      };
    }
    const sidecarPaths = stored.owned_paths ?? [];
    for (const p of sidecarPaths) {
      if (!existsSync(p)) {
        return {
          target: TARGET,
          status: "drift",
          details: [`sidecar context file missing: ${p}`],
          fix_hint: "o2b install --target aider --apply",
        };
      }
      // Refuse to report ok if the sidecar is suspiciously empty.
      if (readSafe(p).trim().length === 0) {
        return {
          target: TARGET,
          status: "drift",
          details: [`sidecar context file is empty: ${p}`],
          fix_hint: "o2b install --target aider --apply",
        };
      }
    }
    return {
      target: TARGET,
      status: "ok",
      details: [`${conf}: managed block present; sidecar ok`],
      fix_hint: null,
    };
  },
};

defaultRegistry.register(aiderAdapter);
