/** Doctor repair for HEARTBEAT.md files that accidentally contain docs template wrappers. */
import fs from "node:fs/promises";
import path from "node:path";
import { note } from "../../packages/terminal-core/src/note.js";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { resolveWorkspaceTemplateDir } from "../agents/workspace-templates.js";
import { DEFAULT_HEARTBEAT_FILENAME } from "../agents/workspace.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { formatErrorMessage } from "../infra/errors.js";
import { writeTextAtomic } from "../infra/json-files.js";
import { shortenHomePath } from "../utils.js";

const LEGACY_HEARTBEAT_PROSE_TEMPLATE = [
  "# HEARTBEAT.md",
  "Keep this file empty unless you want a tiny checklist. Keep it small.",
] as const;

const LEGACY_HEARTBEAT_HEADING_FENCED_TEMPLATE = [
  "# HEARTBEAT.md Template",
  "```markdown",
  "# Keep this file empty (or with only comments) to skip heartbeat API calls.",
  "# Add tasks below when you want the agent to check something periodically.",
  "```",
] as const;

const LEGACY_HEARTBEAT_FENCED_TEMPLATE = [
  "```markdown",
  "# Keep this file empty (or with only comments) to skip heartbeat API calls.",
  "# Add tasks below when you want the agent to check something periodically.",
  "```",
] as const;

const LEGACY_HEARTBEAT_FENCED_RELATED_TEMPLATE = [
  "```markdown",
  "# Keep this file empty (or with only comments) to skip heartbeat API calls.",
  "# Add tasks below when you want the agent to check something periodically.",
  "```",
  "## Related",
  "- [Heartbeat config](/gateway/config-agents)",
] as const;

const DOCS_HEARTBEAT_TEMPLATE_PAGE_AS_TEMPLATE = [
  "# HEARTBEAT.md template",
  "`HEARTBEAT.md` lives in the agent workspace. Keep the file empty, or with only Markdown comments and headings, when you want OpenClaw to skip heartbeat model calls.",
  "The default runtime template is:",
  "```markdown",
  "# Keep this file empty (or with only comments) to skip heartbeat API calls.",
  "# Add tasks below when you want the agent to check something periodically.",
  "```",
  "Add short tasks below the comments only when you want the agent to check something periodically. Keep heartbeat instructions small because they are read during recurring wakes.",
  "## Related",
  "- [Heartbeat config](/gateway/config-agents)",
] as const;

const HEARTBEAT_DEFAULT_BODY_LINES = [
  "# Keep this file empty (or with only comments) to skip heartbeat API calls.",
  "# Add tasks below when you want the agent to check something periodically.",
] as const;

const DIRTY_HEARTBEAT_DOC_WRAPPER_LINES = new Set([
  "```markdown",
  "# HEARTBEAT.md Template",
  "# HEARTBEAT.md template",
  "- [Heartbeat config](/gateway/config-agents)",
]);

const KNOWN_DIRTY_HEARTBEAT_TEMPLATE_LINES = new Set([
  "```markdown",
  "```",
  "# HEARTBEAT.md Template",
  "# HEARTBEAT.md template",
  "`HEARTBEAT.md` lives in the agent workspace. Keep the file empty, or with only Markdown comments and headings, when you want OpenClaw to skip heartbeat model calls.",
  "The default runtime template is:",
  "Add short tasks below the comments only when you want the agent to check something periodically. Keep heartbeat instructions small because they are read during recurring wakes.",
  ...LEGACY_HEARTBEAT_PROSE_TEMPLATE,
  "# Keep this file empty (or with only comments) to skip heartbeat API calls.",
  "# Add tasks below when you want the agent to check something periodically.",
  "## Related",
  "- [Heartbeat config](/gateway/config-agents)",
]);

const KNOWN_REPAIRABLE_DIRTY_HEARTBEAT_TEMPLATES = [
  LEGACY_HEARTBEAT_PROSE_TEMPLATE,
  LEGACY_HEARTBEAT_HEADING_FENCED_TEMPLATE,
  LEGACY_HEARTBEAT_FENCED_TEMPLATE,
  LEGACY_HEARTBEAT_FENCED_RELATED_TEMPLATE,
  DOCS_HEARTBEAT_TEMPLATE_PAGE_AS_TEMPLATE,
] as const;

export type HeartbeatTemplateRepairAnalysis =
  | { status: "clean" }
  | { status: "dirty-template" }
  | { status: "dirty-template-with-custom-content"; customLines: string[] };

function linesEqual(left: readonly string[], right: readonly string[]): boolean {
  return left.length === right.length && left.every((line, index) => line === right[index]);
}

/** Classifies heartbeat template content as clean, repairable, or risky because it has user text. */
export function analyzeHeartbeatTemplateForRepair(
  content: string,
): HeartbeatTemplateRepairAnalysis {
  const lines = content
    .split(/\r?\n/)
    .map((line) => line.trim())
    .filter((line) => line.length > 0);
  if (KNOWN_REPAIRABLE_DIRTY_HEARTBEAT_TEMPLATES.some((template) => linesEqual(lines, template))) {
    return { status: "dirty-template" };
  }

  const hasDefaultTemplateBody = HEARTBEAT_DEFAULT_BODY_LINES.every((line) => lines.includes(line));
  const hasDirtyDocWrapper = lines.some((line) => DIRTY_HEARTBEAT_DOC_WRAPPER_LINES.has(line));
  const hasLegacyProseTemplate = LEGACY_HEARTBEAT_PROSE_TEMPLATE.every((line) =>
    lines.includes(line),
  );
  if ((!hasDefaultTemplateBody || !hasDirtyDocWrapper) && !hasLegacyProseTemplate) {
    return { status: "clean" };
  }
  const customLines = lines.filter((line) => !KNOWN_DIRTY_HEARTBEAT_TEMPLATE_LINES.has(line));
  return { status: "dirty-template-with-custom-content", customLines };
}

async function readCleanHeartbeatTemplate(): Promise<string> {
  const templateDir = await resolveWorkspaceTemplateDir();
  const templatePath = path.join(templateDir, DEFAULT_HEARTBEAT_FILENAME);
  return await fs.readFile(templatePath, "utf-8");
}

/** Replaces known dirty heartbeat templates with the clean runtime template when repair is enabled. */
export async function maybeRepairHeartbeatTemplate(params: {
  cfg: OpenClawConfig;
  shouldRepair: boolean;
}): Promise<void> {
  const workspaceDir = resolveAgentWorkspaceDir(params.cfg, resolveDefaultAgentId(params.cfg));
  const heartbeatPath = path.join(workspaceDir, DEFAULT_HEARTBEAT_FILENAME);
  let content: string;
  try {
    content = await fs.readFile(heartbeatPath, "utf-8");
  } catch (error) {
    if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") {
      return;
    }
    note(
      `Could not inspect ${shortenHomePath(heartbeatPath)}: ${formatErrorMessage(error)}`,
      "Heartbeat template",
    );
    return;
  }

  const analysis = analyzeHeartbeatTemplateForRepair(content);
  if (analysis.status === "clean") {
    return;
  }
  if (analysis.status === "dirty-template-with-custom-content") {
    note(
      [
        `${shortenHomePath(heartbeatPath)} contains an older heartbeat template wrapper plus custom or unrecognized content.`,
        "Doctor left it unchanged so it does not delete user tasks. Remove the fenced template and Related lines manually if they are not intentional.",
      ].join("\n"),
      "Heartbeat template",
    );
    return;
  }
  if (!params.shouldRepair) {
    note(
      [
        `${shortenHomePath(heartbeatPath)} contains an older heartbeat documentation template.`,
        'Run "openclaw doctor --fix" to replace it with the clean heartbeat template.',
      ].join("\n"),
      "Heartbeat template",
    );
    return;
  }

  try {
    const cleanTemplate = await readCleanHeartbeatTemplate();
    await writeTextAtomic(heartbeatPath, cleanTemplate, { mode: 0o600 });
    note(
      `Replaced ${shortenHomePath(heartbeatPath)} with the clean heartbeat template.`,
      "Doctor changes",
    );
  } catch (error) {
    note(
      `Could not repair ${shortenHomePath(heartbeatPath)}: ${formatErrorMessage(error)}`,
      "Heartbeat template",
    );
  }
}
