/**
 * markdown.ts — Publish preserved memories as personal memory-compatible markdown files.
 * Generates markdown files with YAML frontmatter.
 * and supporting evidence excerpts.
 */

import type postgres from "postgres";
import { createHash } from "crypto";
import { writeFile, mkdir, unlink } from "fs/promises";
import { existsSync } from "fs";
import { isAbsolute, join, relative, resolve, sep } from "path";
import { config } from "../config";
import { redactSecrets } from "../security/secret-scanner";

const PUBLISH_DIR = process.env.BRAINCORE_PUBLISH_DIR || "./data/memory/PRESERVED";

export interface PublishResult {
  published: number;
  path: string;
}

/**
 * Publish memories that are in 'published' lifecycle state.
 * Only publishes memories that are new or have changed since last publish.
 * Tracks publish state in preserve.publish_note.
 */
export async function publishNotes(
  sql: postgres.Sql,
  scope?: string,
): Promise<PublishResult> {
  if (!existsSync(PUBLISH_DIR)) {
    await mkdir(PUBLISH_DIR, { recursive: true });
  }

  const scopeFilter = scope ? sql`AND m.scope_path LIKE ${scope + "%"}` : sql``;

  const memories = await sql`
    SELECT
      m.memory_id,
      m.memory_type::text AS memory_type,
      m.title,
      m.narrative,
      m.support_count,
      m.contradiction_count,
      m.confidence::float AS confidence,
      m.scope_path,
      m.fingerprint,
      m.created_at,
      m.updated_at,
      e.canonical_name AS scope_entity_name
    FROM preserve.memory m
    LEFT JOIN preserve.entity e ON m.scope_entity_id = e.entity_id
    WHERE m.lifecycle_state = 'published'
      AND m.tenant = ${config.tenant}
    ${scopeFilter}
    ORDER BY m.updated_at DESC
  `;

  let published = 0;

  for (const mem of memories) {
    const markdown = buildMarkdown(mem);
    const contentHash = createHash("sha256")
      .update(markdown, "utf-8")
      .digest("hex");

    const [existing] = await sql`
      SELECT publish_id, content_hash, file_path
      FROM preserve.publish_note
      WHERE memory_id = ${mem.memory_id}::uuid
      LIMIT 1
    `.catch(() => [undefined]);

    const filenameBase = slugify(redactText(mem.title || mem.memory_id)) || "memory";
    const filename = `${filenameBase}-${filenameId(mem.memory_id)}.md`;
    const filePath = join(PUBLISH_DIR, filename);

    if (existing && existing.content_hash === contentHash && existing.file_path === filePath) {
      continue;
    }

    await writeFile(filePath, markdown, "utf-8");

    if (existing) {
      await sql`
        UPDATE preserve.publish_note
        SET content_hash = ${contentHash},
            file_path = ${filePath},
            published_at = now()
        WHERE publish_id = ${existing.publish_id}
      `;
    } else {
      await sql`
        INSERT INTO preserve.publish_note (memory_id, file_path, content_hash)
        VALUES (${mem.memory_id}::uuid, ${filePath}, ${contentHash})
      `;
    }

    if (existing) {
      await removeStalePublishedFile(existing.file_path, filePath);
    }

    published++;
  }

  return { published, path: PUBLISH_DIR };
}

function buildMarkdown(mem: any): string {
  const lines: string[] = [];
  const title = redactText(mem.title || "Untitled Memory") || "Untitled Memory";
  const narrative = redactText(mem.narrative || "");
  const memoryType = redactText(mem.memory_type || "pattern") || "pattern";
  const scopePath = mem.scope_path ? redactText(mem.scope_path) : "";
  const scopeEntityName = mem.scope_entity_name ? redactText(mem.scope_entity_name) : "";

  lines.push("---");
  lines.push(`name: ${yamlSafe(title)}`);
  lines.push(`description: ${yamlSafe(summarize(narrative, 120))}`);
  lines.push("type: reference");
  lines.push(`memory_type: ${yamlSafe(memoryType)}`);
  lines.push(`confidence: ${mem.confidence || 0}`);
  lines.push(`support_count: ${mem.support_count || 0}`);
  if (scopePath) lines.push(`scope_path: ${yamlSafe(scopePath)}`);
  if (scopeEntityName) lines.push(`entity: ${yamlSafe(scopeEntityName)}`);
  lines.push(`braincore_id: ${mem.memory_id}`);
  lines.push(`generated: ${new Date().toISOString().split("T")[0]}`);
  lines.push("---");
  lines.push("");

  lines.push(`# ${title}`);
  lines.push("");

  if (narrative) {
    lines.push(narrative);
    lines.push("");
  }

  lines.push("---");
  lines.push("");
  lines.push(`*Confidence: ${((mem.confidence || 0) * 100).toFixed(0)}% | Support: ${mem.support_count || 0} episodes | Contradictions: ${mem.contradiction_count || 0}*`);
  lines.push("");
  lines.push(`*Generated by BrainCore pattern compiler on ${new Date().toISOString().split("T")[0]}*`);

  return lines.join("\n");
}

function redactText(value: unknown): string {
  return redactSecrets(String(value ?? "")).redacted;
}

function slugify(text: string): string {
  return text
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "")
    .slice(0, 80);
}

function filenameId(memoryId: unknown): string {
  return String(memoryId || "memory")
    .replace(/[^a-zA-Z0-9-]/g, "")
    .slice(0, 80) || "memory";
}

async function removeStalePublishedFile(oldFilePath: unknown, newFilePath: string): Promise<void> {
  if (!oldFilePath) return;

  const oldPath = resolveInsideDir(PUBLISH_DIR, String(oldFilePath));
  if (!oldPath) return;
  if (oldPath === resolve(newFilePath)) return;

  await unlink(oldPath).catch((error: any) => {
    if (error?.code !== "ENOENT") {
      console.warn(`  [publish] Could not remove stale publish file: ${error.message}`);
    }
  });
}

function resolveInsideDir(rootDir: string, filePath: string): string | null {
  const target = resolve(filePath);
  const relation = relative(resolve(rootDir), target);
  if (relation === "" || relation === ".." || relation.startsWith(`..${sep}`) || isAbsolute(relation)) {
    return null;
  }
  return target;
}

function yamlSafe(text: string): string {
  return JSON.stringify(text.replace(/[\r\n]+/g, " "));
}

function summarize(text: string, maxLen: number): string {
  const firstLine = text.split("\n").find((l) => l.trim())?.trim() || text;
  if (firstLine.length <= maxLen) return firstLine;
  return firstLine.slice(0, maxLen - 3) + "...";
}
