/**
 * Active Learning Loop
 *
 * Auto-extracts memories from agent work sessions.
 * Triggered on harmony_end_agent_session and harmony_update_agent_progress.
 */

import type { HarmonyApiClient } from "./api-client.js";
import { getActiveProjectId, getActiveWorkspaceId } from "./config.js";
import {
  autoExpandGraph,
  findSimilarEntities,
  linkCrossTypeNeighbors,
} from "./graph-expansion.js";

// Entity types that can participate in `contradicts` relations (per schema.ts)
const CONTRADICTION_TYPES = new Set([
  "decision",
  "pattern",
  "preference",
  "lesson",
]);

export interface SessionContext {
  cardId: string;
  cardTitle: string;
  cardLabels: string[];
  agentIdentifier: string;
  agentName: string;
  status: "completed" | "paused";
  progressPercent?: number;
  blockers?: string[];
  currentTask?: string;
  sessionDurationMs?: number;
  cardDescription?: string;
  cardSubtasks?: Array<{ title: string; done: boolean }>;
}

export interface ExtractedLearning {
  title: string;
  content: string;
  type: string;
  tier: string;
  confidence: number;
  tags: string[];
  metadata: Record<string, unknown>;
}

// --- Mid-Session Learning ---

/** Track previous task per session for change detection */
const sessionTaskHistory = new Map<
  string,
  {
    lastTask: string;
    lastExtractionAt: number;
    steps: Array<{ task: string; progress: number; timestamp: number }>;
  }
>();

/** Rate limit: max 1 extraction per 2 minutes per session */
const MID_SESSION_RATE_LIMIT_MS = 120_000;

/**
 * Simple Levenshtein similarity (0-1 range, 1 = identical).
 */
function levenshteinSimilarity(a: string, b: string): number {
  if (a === b) return 1;
  if (a.length === 0 || b.length === 0) return 0;

  // Use shorter strings for performance (truncate to 200 chars)
  const sa = a.slice(0, 200).toLowerCase();
  const sb = b.slice(0, 200).toLowerCase();

  const matrix: number[][] = [];
  for (let i = 0; i <= sa.length; i++) {
    matrix[i] = [i];
  }
  for (let j = 0; j <= sb.length; j++) {
    matrix[0][j] = j;
  }

  for (let i = 1; i <= sa.length; i++) {
    for (let j = 1; j <= sb.length; j++) {
      const cost = sa[i - 1] === sb[j - 1] ? 0 : 1;
      matrix[i][j] = Math.min(
        matrix[i - 1][j] + 1,
        matrix[i][j - 1] + 1,
        matrix[i - 1][j - 1] + cost,
      );
    }
  }

  const maxLen = Math.max(sa.length, sb.length);
  return 1 - matrix[sa.length][sb.length] / maxLen;
}

export interface MidSessionContext {
  cardId: string;
  cardTitle: string;
  agentIdentifier: string;
  agentName: string;
  currentTask?: string;
  status?: string;
  blockers?: string[];
  progressPercent?: number;
}

/**
 * Extract learnings from mid-session progress updates.
 * Called from harmony_update_agent_progress.
 */
export async function extractMidSessionLearnings(
  client: HarmonyApiClient,
  ctx: MidSessionContext,
): Promise<{ count: number; entityIds: string[] }> {
  const workspaceId = getActiveWorkspaceId();
  if (!workspaceId) return { count: 0, entityIds: [] };

  const projectId = getActiveProjectId() || undefined;
  const now = Date.now();
  const entityIds: string[] = [];

  const history = sessionTaskHistory.get(ctx.cardId);

  // Always track step history regardless of rate limit
  if (ctx.currentTask) {
    const previousTask = history?.lastTask || "";
    const similarity = levenshteinSimilarity(previousTask, ctx.currentTask);
    const existingSteps = history?.steps || [];

    if (
      existingSteps.length === 0 ||
      (similarity < 0.6 && previousTask.length > 0)
    ) {
      const newStep = {
        task: ctx.currentTask,
        progress: ctx.progressPercent ?? 0,
        timestamp: now,
      };

      if (existingSteps.length === 0 && previousTask.length > 0) {
        existingSteps.push({
          task: previousTask,
          progress: 0,
          timestamp: history?.lastExtractionAt ?? now,
        });
      }
      existingSteps.push(newStep);

      sessionTaskHistory.set(ctx.cardId, {
        lastTask: ctx.currentTask,
        lastExtractionAt: history?.lastExtractionAt ?? 0,
        steps: existingSteps,
      });
    }
  }

  // Rate limit check (only gates memory entity creation, not step tracking)
  if (history && now - history.lastExtractionAt < MID_SESSION_RATE_LIMIT_MS) {
    // Still within rate limit, but always extract blockers immediately
    if (ctx.status !== "blocked" || !ctx.blockers?.length) {
      return { count: 0, entityIds: [] };
    }
  }

  // Rule 1: Status transitions to "blocked" → create error entity immediately
  if (ctx.status === "blocked" && ctx.blockers?.length) {
    for (const blocker of ctx.blockers) {
      try {
        const result = await client.createMemoryEntity({
          workspace_id: workspaceId,
          project_id: projectId,
          type: "error",
          scope: "project",
          memory_tier: "draft",
          title: `Blocker (mid-session): ${blocker.slice(0, 100)}`,
          content: `Encountered while working on "${ctx.cardTitle}":\n\n${blocker}\n\nAgent: ${ctx.agentName}\nProgress: ${ctx.progressPercent ?? "unknown"}%`,
          confidence: 0.5,
          tags: ["auto-extracted", "blocker", "mid-session"],
          metadata: {
            source: "mid_session",
            card_id: ctx.cardId,
          },
          agent_identifier: ctx.agentIdentifier,
        });
        const entity = result.entity as { id: string };
        if (entity?.id) entityIds.push(entity.id);
      } catch {
        // Non-fatal
      }
    }
    sessionTaskHistory.set(ctx.cardId, {
      lastTask: ctx.currentTask || "",
      lastExtractionAt: now,
      steps: history?.steps || [],
    });
    return { count: entityIds.length, entityIds };
  }

  // Rule 2: Task changed significantly → capture context entity
  if (ctx.currentTask) {
    const previousTask = history?.lastTask || "";
    const similarity = levenshteinSimilarity(previousTask, ctx.currentTask);

    if (similarity < 0.6 && previousTask.length > 0) {
      try {
        const result = await client.createMemoryEntity({
          workspace_id: workspaceId,
          project_id: projectId,
          type: "context",
          scope: "project",
          memory_tier: "draft",
          title: `Task transition: ${ctx.cardTitle}`,
          content: `Agent transitioned tasks on "${ctx.cardTitle}".\n\nPrevious: ${previousTask}\nCurrent: ${ctx.currentTask}\nProgress: ${ctx.progressPercent ?? "unknown"}%`,
          confidence: 0.5,
          tags: ["auto-extracted", "task-transition", "mid-session"],
          metadata: {
            source: "mid_session",
            card_id: ctx.cardId,
            previous_task: previousTask,
            current_task: ctx.currentTask,
          },
          agent_identifier: ctx.agentIdentifier,
        });
        const entity = result.entity as { id: string };
        if (entity?.id) entityIds.push(entity.id);
      } catch {
        // Non-fatal
      }
    }

    // Update lastExtractionAt only when entities were created
    const currentHistory = sessionTaskHistory.get(ctx.cardId);
    sessionTaskHistory.set(ctx.cardId, {
      lastTask: ctx.currentTask,
      lastExtractionAt:
        entityIds.length > 0 ? now : (currentHistory?.lastExtractionAt ?? 0),
      steps: currentHistory?.steps || [],
    });
  }

  return { count: entityIds.length, entityIds };
}

/**
 * Clean up mid-session tracking for a card (call on session end).
 */
export function clearMidSessionTracking(cardId: string): void {
  sessionTaskHistory.delete(cardId);
}

// --- Procedure Extraction ---

interface StepEntry {
  task: string;
  progress: number;
  timestamp: number;
}

interface EnrichedStep {
  task: string;
  progress: number;
  progressDelta: number;
  durationMs: number;
  isKeyDecision: boolean;
}

/**
 * Enrich raw steps with progress deltas and key decision detection.
 * A step is a "key decision" if it represents a >20% progress jump.
 */
function enrichSteps(steps: StepEntry[]): EnrichedStep[] {
  return steps.map((step, i) => {
    const prevProgress = i > 0 ? steps[i - 1].progress : 0;
    const prevTimestamp = i > 0 ? steps[i - 1].timestamp : step.timestamp;
    const progressDelta = step.progress - prevProgress;
    return {
      task: step.task,
      progress: step.progress,
      progressDelta,
      durationMs: step.timestamp - prevTimestamp,
      isKeyDecision: progressDelta >= 20,
    };
  });
}

/**
 * Build structured procedure content from session data and enriched steps.
 */
function buildProcedureContent(
  session: SessionContext,
  enrichedSteps: EnrichedStep[],
  wikiLinksLine: string,
): string {
  const triggerLabels =
    session.cardLabels.length > 0
      ? `Labels: ${session.cardLabels.join(", ")}`
      : "";

  const stepsMarkdown = enrichedSteps
    .map((s, i) => {
      const marker = s.isKeyDecision ? " **[key step]**" : "";
      const duration =
        s.durationMs > 0 ? ` (~${Math.round(s.durationMs / 60000)}min)` : "";
      return `${i + 1}. ${s.task} (${s.progress}%, +${s.progressDelta}%)${marker}${duration}`;
    })
    .join("\n");

  const subtaskSection =
    session.cardSubtasks && session.cardSubtasks.length > 0
      ? [
          "",
          "## Subtasks Completed",
          ...session.cardSubtasks.map(
            (s) => `- [${s.done ? "x" : " "}] ${s.title}`,
          ),
        ].join("\n")
      : "";

  const durationInfo = session.sessionDurationMs
    ? `\nDuration: ${Math.round(session.sessionDurationMs / 60000)} minutes`
    : "";

  return [
    "## Trigger",
    `When working on: "${session.cardTitle}"`,
    triggerLabels,
    "",
    "## Steps",
    stepsMarkdown,
    subtaskSection,
    "",
    "## Outcome",
    `Completed at ${session.progressPercent ?? "unknown"}%`,
    session.currentTask ? `Final state: ${session.currentTask}` : "",
    `Agent: ${session.agentName}`,
    durationInfo,
    wikiLinksLine,
  ]
    .filter((line) => line !== undefined)
    .join("\n");
}

/**
 * Extract a new procedure or reinforce an existing similar one.
 *
 * Deduplication: searches for existing procedure entities with similar titles.
 * If found, merges new steps and bumps confidence. Otherwise creates a new one.
 */
async function extractOrReinforceProcedure(
  client: HarmonyApiClient,
  session: SessionContext,
  steps: StepEntry[],
  workspaceId: string,
  projectId?: string,
  wikiLinksLine = "",
): Promise<
  | { mode: "created"; learning: ExtractedLearning }
  | { mode: "reinforced"; entityId: string }
  | null
> {
  const enrichedSteps = enrichSteps(steps);
  const newContent = buildProcedureContent(
    session,
    enrichedSteps,
    wikiLinksLine,
  );
  const tags = [
    "auto-extracted",
    "procedure",
    ...session.cardLabels.slice(0, 3),
  ];

  // Try to find an existing similar procedure to reinforce
  try {
    const similar = await findSimilarEntities(
      client,
      `Procedure: ${session.cardTitle}`,
      newContent,
      workspaceId,
      { projectId, limit: 5, minRrfScore: 0.03 },
    );

    const matchingProcedure = similar.find(
      (e) => e.type === "procedure" && (e.rrf_score ?? 0) >= 0.05,
    );

    if (matchingProcedure) {
      // Fetch the full entity to get metadata and memory_tier
      const { entity: rawEntity } = await client.getMemoryEntity(
        matchingProcedure.id,
      );
      const fullEntity = rawEntity as {
        id: string;
        content?: string;
        confidence?: number;
        memory_tier?: string;
        metadata?: Record<string, unknown>;
      };

      const currentMeta = fullEntity.metadata || {};
      const sourceCards = (
        (currentMeta.source_cards as string[]) || []
      ).slice();
      if (!sourceCards.includes(session.cardId)) {
        sourceCards.push(session.cardId);
      }
      const reuseCount = ((currentMeta.reuse_count as number) || 0) + 1;
      const currentConfidence = fullEntity.confidence ?? 0.7;
      // Bump confidence by 0.05 per reinforcement, max 0.95
      const newConfidence = Math.min(0.95, currentConfidence + 0.05);

      // Append new steps as an additional execution record
      const stepsAppendix = enrichedSteps
        .map((s, i) => `${i + 1}. ${s.task} (${s.progress}%)`)
        .join("\n");
      const appendix = `\n\n---\n### Execution ${reuseCount + 1}: ${session.cardTitle}\n${stepsAppendix}\nAgent: ${session.agentName} | ${new Date().toISOString().split("T")[0]}`;

      const updatedMeta = {
        ...currentMeta,
        reuse_count: reuseCount,
        source_cards: sourceCards,
        last_reinforced_at: new Date().toISOString(),
        step_count: Math.max(
          (currentMeta.step_count as number) || 0,
          steps.length,
        ),
      };

      // Auto-promote to reference tier if reinforced enough (≥3 executions)
      const shouldPromote =
        reuseCount >= 2 && fullEntity.memory_tier !== "reference";

      await client.updateMemoryEntity(fullEntity.id, {
        content: (fullEntity.content || "") + appendix,
        confidence: newConfidence,
        metadata: {
          ...updatedMeta,
          ...(shouldPromote
            ? {
                promoted_reason: `Reinforced by ${reuseCount + 1} successful sessions`,
                promoted_at: new Date().toISOString(),
              }
            : {}),
        },
        ...(shouldPromote ? { memory_tier: "reference" } : {}),
      });

      return { mode: "reinforced", entityId: fullEntity.id };
    }
  } catch {
    // Similarity search failed, fall through to create new
  }

  // No existing procedure found — create a new one
  return {
    mode: "created",
    learning: {
      title: `Procedure: ${session.cardTitle}`,
      content: newContent,
      type: "procedure",
      tier: "episode",
      confidence: 0.7,
      tags,
      metadata: {
        source: "active_learning",
        card_id: session.cardId,
        source_cards: [session.cardId],
        step_count: steps.length,
        key_step_count: enrichedSteps.filter((s) => s.isKeyDecision).length,
        reuse_count: 0,
      },
    },
  };
}

// --- Same-Session Causal Linking ---

/** Causal pairing rules for entities created within the same session */
const SESSION_CAUSAL_RULES: Array<{
  sourceType: string;
  targetType: string;
  relation: string;
  confidence: number;
}> = [
  {
    sourceType: "error",
    targetType: "solution",
    relation: "resolved_by",
    confidence: 0.8,
  },
  {
    sourceType: "lesson",
    targetType: "error",
    relation: "learned_from",
    confidence: 0.75,
  },
];

/**
 * Link entities created within the same session using causal relations.
 *
 * For example, if a session produces both an `error` and a `solution`,
 * creates an `error -[resolved_by]-> solution` relation.
 *
 * Non-fatal: all errors are caught silently.
 */
export async function linkSessionEntities(
  client: HarmonyApiClient,
  createdPairs: Array<{ id: string; learning: ExtractedLearning }>,
  _workspaceId: string,
  _projectId?: string,
): Promise<{ relationsCreated: number }> {
  let relationsCreated = 0;

  for (const rule of SESSION_CAUSAL_RULES) {
    const sources = createdPairs.filter(
      (p) => p.learning.type === rule.sourceType,
    );
    const targets = createdPairs.filter(
      (p) => p.learning.type === rule.targetType,
    );

    for (const source of sources) {
      for (const target of targets) {
        try {
          await client.createMemoryRelation({
            source_id: source.id,
            target_id: target.id,
            relation_type: rule.relation,
            confidence: rule.confidence,
          });
          relationsCreated++;
        } catch {
          // Skip duplicate/failed relations silently
        }
      }
    }
  }

  return { relationsCreated };
}

// --- Session-End Learning ---

/**
 * Extract learnings from a completed agent session.
 * Returns the number of memories created.
 */
export async function extractLearnings(
  client: HarmonyApiClient,
  session: SessionContext,
): Promise<{ count: number; entityIds: string[] }> {
  const workspaceId = getActiveWorkspaceId();
  if (!workspaceId) {
    return { count: 0, entityIds: [] };
  }

  const projectId = getActiveProjectId() || undefined;
  const learnings: ExtractedLearning[] = [];

  // Search for related entities to enrich summaries with wiki-links
  let relatedEntityTitles: string[] = [];
  if (workspaceId) {
    try {
      const searchQuery = [
        session.cardTitle,
        session.currentTask || "",
        ...session.cardLabels,
      ]
        .filter(Boolean)
        .join(" ");
      const similar = await findSimilarEntities(
        client,
        searchQuery,
        session.currentTask || session.cardTitle,
        workspaceId,
        { projectId, limit: 5, minRrfScore: 0.01 },
      );
      relatedEntityTitles = similar.slice(0, 3).map((e) => e.title);
    } catch {
      /* non-fatal */
    }
  }

  const wikiLinksLine =
    relatedEntityTitles.length > 0
      ? `\nRelated: ${relatedEntityTitles.map((t) => `[[${t}]]`).join(", ")}`
      : "";

  // Rule 1: Session had blockers → create error entities
  if (session.blockers && session.blockers.length > 0) {
    for (const blocker of session.blockers) {
      learnings.push({
        title: `Blocker: ${blocker.slice(0, 100)}`,
        content: `Encountered while working on "${session.cardTitle}":\n\n${blocker}\n\nAgent: ${session.agentName}\nSession status: ${session.status}`,
        type: "error",
        tier: "reference",
        confidence: 0.7,
        tags: ["auto-extracted", "blocker", ...session.cardLabels.slice(0, 3)],
        metadata: {
          source: "active_learning",
          card_id: session.cardId,
        },
      });
    }
  }

  // Rule 2: Session completed → create lesson entity summarizing work
  // Only create when there's meaningful content beyond "completed X at 100%"
  const hasMeaningfulContent =
    (session.blockers?.length ?? 0) > 0 ||
    session.status === "paused" ||
    ((session.cardSubtasks?.length ?? 0) > 0 &&
      session.cardSubtasks?.some((s) => !s.done));

  if (session.status === "completed" && hasMeaningfulContent) {
    const durationInfo = session.sessionDurationMs
      ? `\nDuration: ${Math.round(session.sessionDurationMs / 60000)} minutes`
      : "";

    learnings.push({
      title: `Session: ${session.cardTitle}`,
      content: [
        `Completed work on "${session.cardTitle}".`,
        session.currentTask ? `Final task: ${session.currentTask}` : "",
        session.progressPercent !== undefined
          ? `Progress: ${session.progressPercent}%`
          : "",
        durationInfo,
        session.cardLabels.length > 0
          ? `Labels: ${session.cardLabels.join(", ")}`
          : "",
        session.blockers?.length
          ? `Blockers encountered: ${session.blockers.join("; ")}`
          : "",
        `\nAgent: ${session.agentName}`,
        wikiLinksLine,
      ]
        .filter(Boolean)
        .join("\n"),
      type: "lesson",
      tier: "episode",
      confidence: 0.7,
      tags: [
        "auto-extracted",
        "session-summary",
        ...session.cardLabels.slice(0, 3),
      ],
      metadata: {
        source: "active_learning",
        card_id: session.cardId,
      },
    });
  }

  // Rule 3: Card had "bug" label + completed → create solution entity
  const hasBugLabel = session.cardLabels.some((l) =>
    ["bug", "fix", "hotfix", "defect", "error"].includes(l.toLowerCase()),
  );
  if (hasBugLabel && session.status === "completed") {
    learnings.push({
      title: `Solution: ${session.cardTitle}`,
      content: [
        `Resolved bug: "${session.cardTitle}"`,
        session.currentTask ? `\nApproach: ${session.currentTask}` : "",
        `\nAgent: ${session.agentName}`,
        wikiLinksLine,
      ]
        .filter(Boolean)
        .join("\n"),
      type: "solution",
      tier: "reference",
      confidence: 0.8,
      tags: ["auto-extracted", "bug-fix", ...session.cardLabels.slice(0, 3)],
      metadata: {
        source: "active_learning",
        card_id: session.cardId,
        auto_confidence: true,
      },
    });
  }

  // Store learnings, tracking entity ID → learning for graph expansion
  const entityIds: string[] = [];

  // Rule 4: Successful session with tracked steps → create or reinforce procedure entity
  const stepHistory = sessionTaskHistory.get(session.cardId);
  const hasEnoughSteps = stepHistory && stepHistory.steps.length >= 2;
  const isSuccessful =
    session.status === "completed" &&
    (session.progressPercent === undefined || session.progressPercent >= 85) &&
    !session.blockers?.length;

  if (isSuccessful && hasEnoughSteps) {
    const procedureResult = await extractOrReinforceProcedure(
      client,
      session,
      stepHistory.steps,
      workspaceId,
      projectId,
      wikiLinksLine,
    );
    if (procedureResult) {
      if (procedureResult.mode === "created") {
        learnings.push(procedureResult.learning);
      } else {
        // Reinforced existing procedure — already saved via API, just track the ID
        entityIds.push(procedureResult.entityId);
      }
    }
  }
  const createdPairs: Array<{ id: string; learning: ExtractedLearning }> = [];
  for (const learning of learnings) {
    try {
      const metadata = {
        ...learning.metadata,
      };

      const result = await client.createMemoryEntity({
        workspace_id: workspaceId,
        project_id: projectId,
        type: learning.type,
        scope: "project",
        memory_tier: learning.tier,
        title: learning.title,
        content: learning.content,
        confidence: learning.confidence,
        tags: learning.tags,
        metadata,
        agent_identifier: session.agentIdentifier,
      });

      const entity = result.entity as { id: string };
      if (entity?.id) {
        entityIds.push(entity.id);
        createdPairs.push({ id: entity.id, learning });
      }
    } catch {
      // Non-fatal: individual learning extraction failure shouldn't block others
    }
  }

  // Auto-expand the knowledge graph and detect contradictions for each newly created entity (fire-and-forget)
  for (const { id, learning } of createdPairs) {
    autoExpandGraph(
      client,
      id,
      learning.title,
      learning.content,
      learning.tags,
      workspaceId,
      projectId,
    ).catch(() => {});

    detectContradictions(
      client,
      id,
      learning.type,
      learning.title,
      learning.content,
      learning.tags,
      workspaceId,
      projectId,
    ).catch(() => {});

    // Cross-type causal linking (e.g., new error → existing solutions)
    linkCrossTypeNeighbors(
      client,
      id,
      learning.type,
      learning.title,
      learning.content,
      workspaceId,
      projectId,
    ).catch(() => {});
  }

  // Same-session causal linking (e.g., error + solution in same session → resolved_by)
  if (createdPairs.length >= 2) {
    linkSessionEntities(client, createdPairs, workspaceId, projectId).catch(
      () => {},
    );
  }

  // Detect recurring patterns across sessions (fire-and-forget)
  if (entityIds.length > 0) {
    detectAndCreatePatterns(
      client,
      entityIds,
      session,
      workspaceId,
      projectId,
    ).catch(() => {});
  }

  // Detect recurring causal patterns (error→solution chains across sessions)
  if (createdPairs.length > 0) {
    detectCausalPatterns(
      client,
      createdPairs,
      session,
      workspaceId,
      projectId,
    ).catch(() => {});
  }

  // Clean up mid-session tracking
  clearMidSessionTracking(session.cardId);

  return { count: entityIds.length, entityIds };
}

const PATTERN_THRESHOLD = 3;

/**
 * Detect recurring patterns across sessions.
 *
 * Uses embedding-based similarity (via hybrid search) to find related entities
 * instead of naive tag matching. When ≥3 similar entities cluster, creates or
 * updates a `pattern` entity with `part_of` relations from members.
 */
export async function detectAndCreatePatterns(
  client: HarmonyApiClient,
  newEntityIds: string[],
  session: SessionContext,
  workspaceId: string,
  projectId?: string,
): Promise<string[]> {
  const patternEntityIds: string[] = [];

  for (const newEntityId of newEntityIds) {
    try {
      // Fetch the newly created entity to get its type, title, and content
      const { entity: rawEntity } = await client.getMemoryEntity(newEntityId);
      const entity = rawEntity as {
        id: string;
        type: string;
        title: string;
        content: string;
        tags?: string[];
      };
      if (!entity?.type) continue;

      // Use embedding-based search with title + content snippet as query
      const similar = await findSimilarEntities(
        client,
        entity.title,
        entity.content,
        workspaceId,
        { projectId, limit: 30, minRrfScore: 0.01 },
      );

      // Exclude newly created entities
      const existing = similar.filter(
        (c) => !newEntityIds.includes(c.id) && c.type === entity.type,
      );

      if (existing.length < PATTERN_THRESHOLD) continue;

      // Build a descriptive pattern title from member entity titles
      const memberTitles = [
        entity.title,
        ...existing.slice(0, 4).map((e) => e.title),
      ];
      const patternTitle = `Pattern: recurring ${entity.type} (${existing.length + 1} instances)`;

      // Check if a pattern entity for this type already exists
      const { entities: existingPatterns } = await client.listMemoryEntities({
        workspace_id: workspaceId,
        project_id: projectId,
        type: "pattern",
        limit: 10,
      });

      // Find a pattern that covers this entity type
      const matchingPattern = (
        existingPatterns as Array<{
          id: string;
          metadata?: Record<string, unknown>;
        }>
      ).find((p) => p.metadata?.pattern_type === entity.type);

      let patternId: string | null = null;

      if (matchingPattern) {
        patternId = matchingPattern.id;
        await client.updateMemoryEntity(patternId, {
          content: `Recurring pattern: ${entity.type} entities appearing ${existing.length + 1} times.\n\nMembers:\n${memberTitles.map((t) => `- ${t}`).join("\n")}\n\nLast updated: ${new Date().toISOString()}`,
          metadata: {
            pattern_count: existing.length + 1,
            pattern_type: entity.type,
            last_updated: new Date().toISOString(),
          },
        });
      } else {
        const result = await client.createMemoryEntity({
          workspace_id: workspaceId,
          project_id: projectId,
          type: "pattern",
          scope: "project",
          memory_tier: "reference",
          title: patternTitle,
          content: `Recurring pattern: ${entity.type} entities detected ${existing.length + 1} times.\n\nMembers:\n${memberTitles.map((t) => `- ${t}`).join("\n")}`,
          confidence: 0.75,
          tags: ["auto-extracted", "pattern", entity.type],
          metadata: {
            source: "pattern_detection",
            pattern_type: entity.type,
            pattern_count: existing.length + 1,
          },
          agent_identifier: session.agentIdentifier,
        });

        const created = result.entity as { id: string };
        if (created?.id) {
          patternId = created.id;
          patternEntityIds.push(patternId);
        }
      }

      if (!patternId) continue;

      // Link members → pattern using part_of relations
      const toLink = [newEntityId, ...existing.slice(0, 4).map((e) => e.id)];
      for (const sourceId of toLink) {
        try {
          await client.createMemoryRelation({
            source_id: sourceId,
            target_id: patternId,
            relation_type: "part_of",
            confidence: 0.75,
          });
        } catch {
          // Skip duplicate/failed relations silently
        }
      }
    } catch {
      // Non-fatal: pattern detection failure should not block anything
    }
  }

  return patternEntityIds;
}

// --- Cross-Session Causal Pattern Detection ---

const CAUSAL_PATTERN_THRESHOLD = 3;

/**
 * Detect recurring error→solution chains across sessions.
 *
 * When a session produces both error and solution entities, searches for
 * similar errors from other sessions that also have `resolved_by` relations.
 * If ≥3 such pairs exist, creates a `pattern` entity to capture the
 * recurring causal chain.
 */
export async function detectCausalPatterns(
  client: HarmonyApiClient,
  createdPairs: Array<{ id: string; learning: ExtractedLearning }>,
  session: SessionContext,
  workspaceId: string,
  projectId?: string,
): Promise<string[]> {
  const patternIds: string[] = [];

  // Find error+solution pairs from this session
  const errors = createdPairs.filter((p) => p.learning.type === "error");
  const solutions = createdPairs.filter((p) => p.learning.type === "solution");
  if (errors.length === 0 || solutions.length === 0) return patternIds;

  for (const errorPair of errors) {
    try {
      // Search for similar errors from other sessions
      const similarErrors = await findSimilarEntities(
        client,
        errorPair.learning.title,
        errorPair.learning.content,
        workspaceId,
        {
          projectId,
          limit: 20,
          minRrfScore: 0.03,
          excludeIds: createdPairs.map((p) => p.id),
          type: "error",
        },
      );

      // Filter to errors that have resolved_by relations
      const resolvedErrors: Array<{
        errorId: string;
        errorTitle: string;
        solutionTitle: string;
      }> = [];
      for (const similar of similarErrors.slice(0, 10)) {
        try {
          const { outgoing } = await client.getRelatedEntities(similar.id);
          const resolvedByRel = (
            outgoing as Array<{ relation_type: string; target_title?: string }>
          ).find((r) => r.relation_type === "resolved_by");
          if (resolvedByRel) {
            resolvedErrors.push({
              errorId: similar.id,
              errorTitle: similar.title,
              solutionTitle:
                (resolvedByRel as { target_title?: string }).target_title ||
                "unknown",
            });
          }
        } catch {
          // Non-fatal: skip entities where relation lookup fails
        }
      }

      // Need at least CAUSAL_PATTERN_THRESHOLD similar resolved errors (including current)
      if (resolvedErrors.length + 1 < CAUSAL_PATTERN_THRESHOLD) continue;

      // Check if a causal pattern already exists for this domain
      const { entities: existingPatterns } = await client.listMemoryEntities({
        workspace_id: workspaceId,
        project_id: projectId,
        type: "pattern",
        limit: 10,
      });

      const matchingPattern = (
        existingPatterns as Array<{
          id: string;
          metadata?: Record<string, unknown>;
        }>
      ).find(
        (p) => p.metadata?.pattern_chain_type === "error_resolved_by_solution",
      );

      if (matchingPattern) {
        // Update existing causal pattern
        await client.updateMemoryEntity(matchingPattern.id, {
          content: [
            `Recurring error→solution chain detected (${resolvedErrors.length + 1} instances).`,
            "",
            "## Error→Solution Pairs",
            `- ${errorPair.learning.title} → ${solutions[0].learning.title}`,
            ...resolvedErrors
              .slice(0, 5)
              .map((r) => `- ${r.errorTitle} → ${r.solutionTitle}`),
            "",
            `Last updated: ${new Date().toISOString()}`,
          ].join("\n"),
          metadata: {
            pattern_chain_type: "error_resolved_by_solution",
            pattern_count: resolvedErrors.length + 1,
            last_updated: new Date().toISOString(),
          },
        });

        // Link current error+solution to the pattern
        for (const pair of [errorPair, solutions[0]]) {
          try {
            await client.createMemoryRelation({
              source_id: pair.id,
              target_id: matchingPattern.id,
              relation_type: "part_of",
              confidence: 0.75,
            });
          } catch {
            // Skip duplicate relations
          }
        }
      } else {
        // Create new causal pattern
        const result = await client.createMemoryEntity({
          workspace_id: workspaceId,
          project_id: projectId,
          type: "pattern",
          scope: "project",
          memory_tier: "reference",
          title: `Pattern: recurring error→solution chain (${resolvedErrors.length + 1} instances)`,
          content: [
            `Recurring error→solution chain detected across ${resolvedErrors.length + 1} sessions.`,
            "",
            "## Error→Solution Pairs",
            `- ${errorPair.learning.title} → ${solutions[0].learning.title}`,
            ...resolvedErrors
              .slice(0, 5)
              .map((r) => `- ${r.errorTitle} → ${r.solutionTitle}`),
          ].join("\n"),
          confidence: 0.8,
          tags: ["auto-extracted", "pattern", "causal-chain"],
          metadata: {
            source: "causal_pattern_detection",
            pattern_chain_type: "error_resolved_by_solution",
            pattern_count: resolvedErrors.length + 1,
          },
          agent_identifier: session.agentIdentifier,
        });

        const created = result.entity as { id: string };
        if (created?.id) {
          patternIds.push(created.id);

          // Link members to the pattern
          const memberIds = [
            errorPair.id,
            solutions[0].id,
            ...resolvedErrors.slice(0, 4).map((r) => r.errorId),
          ];
          for (const memberId of memberIds) {
            try {
              await client.createMemoryRelation({
                source_id: memberId,
                target_id: created.id,
                relation_type: "part_of",
                confidence: 0.75,
              });
            } catch {
              // Skip duplicate relations
            }
          }
        }
      }
    } catch {
      // Non-fatal: causal pattern detection failure should not block anything
    }
  }

  return patternIds;
}

// --- Contradiction Detection ---

export interface ContradictionResult {
  entityId: string;
  title: string;
  type: string;
  tags: string[];
}

/**
 * Detect potential contradictions for a newly created entity using semantic search.
 *
 * Uses hybrid FTS+vector search to find same-type entities with similar content,
 * then creates `contradicts` relations. Only applies to entity types allowed by
 * the schema: decision, pattern, preference, lesson.
 *
 * Returns the list of contradicting entities found (for inclusion in tool response).
 */
export async function detectContradictions(
  client: HarmonyApiClient,
  entityId: string,
  entityType: string,
  title: string,
  content: string,
  tags: string[],
  workspaceId: string,
  projectId?: string,
): Promise<ContradictionResult[]> {
  // Only types that can participate in contradicts relations
  if (!CONTRADICTION_TYPES.has(entityType)) return [];

  // Need at least a title to search
  if (!title.trim()) return [];

  try {
    const similar = await findSimilarEntities(
      client,
      title,
      content,
      workspaceId,
      {
        projectId,
        limit: 10,
        excludeIds: [entityId],
      },
    );

    // Filter to same type only (contradictions are within the same type)
    const candidates = similar.filter(
      (e) => e.type === entityType && CONTRADICTION_TYPES.has(e.type),
    );

    if (candidates.length === 0) return [];

    // Check for actual content divergence: similar topic but different stance.
    // Entities that are very similar (high RRF) are likely supporting, not contradicting.
    // We want entities that share topic keywords but have meaningful differences.
    const contradictions: ContradictionResult[] = [];

    for (const candidate of candidates) {
      // Skip entities with very high similarity — they're duplicates/supporting, not contradictions
      if ((candidate.rrf_score ?? 0) > 0.8) continue;

      // Skip entities with very low similarity — they're unrelated
      if ((candidate.rrf_score ?? 0) < 0.02) continue;

      // Check tag overlap: need at least 1 shared tag to indicate same domain
      const sharedTags = tags.filter((t) => candidate.tags?.includes(t));
      if (tags.length > 0 && sharedTags.length === 0) continue;

      contradictions.push({
        entityId: candidate.id,
        title: candidate.title,
        type: candidate.type,
        tags: candidate.tags || [],
      });
    }

    // Create contradicts relations (fire-and-forget style, but await for response)
    for (const c of contradictions) {
      try {
        await client.createMemoryRelation({
          source_id: entityId,
          target_id: c.entityId,
          relation_type: "contradicts",
          confidence: 0.5,
        });
      } catch {
        // Skip duplicate/failed relations silently (409 Conflict is expected)
      }
    }

    return contradictions;
  } catch {
    // Non-fatal: contradiction detection should never block entity creation
    return [];
  }
}
