/**
 * BrainPort — knowledge archival + retrieval contract (4 memory tiers).
 *
 * Tier 1 — working: get/set with scope='run' + ttlMs auto-expire (A6, sprint-562).
 * Tier 2 — episodic: since/record per agentId (A6, sprint-562).
 * Tier 3 — semantic: query/archive shared across agents (C4, sprint-564).
 * Tier 4 — procedural: resolveSkills for learned/private/shared skills (C4, sprint-564).
 */

export interface BrainEntryInput {
  content: string;
  content_type?: string;
  author?: string;
  category?: string;
  tags?: string[];
  confidence?: number;
  metadata?: Record<string, unknown>;
  brain_id?: string;
}

export interface BrainEntry {
  id: string;
  content: string;
  category?: string;
  tags?: string[];
  metadata?: Record<string, unknown>;
  created_at?: string;
}

export interface BrainQueryFilters {
  category?: string;
  tags?: string[];
  limit?: number;
  [key: string]: unknown;
}

// ─── Tier 1: Working memory ──────────────────────────────────────────────────

export interface WorkingMemoryPort {
  set(
    runId: string,
    key: string,
    value: unknown,
    opts?: { ttlMs?: number }
  ): Promise<void>;
  get(runId: string, key: string): Promise<unknown>;
  delete(runId: string, key: string): Promise<void>;
}

export class InMemoryWorkingMemory implements WorkingMemoryPort {
  private store = new Map<string, { value: unknown; expiresAt: number }>();

  async set(
    runId: string,
    key: string,
    value: unknown,
    opts?: { ttlMs?: number }
  ): Promise<void> {
    this.store.set(`${runId}:${key}`, {
      value,
      expiresAt: Date.now() + (opts?.ttlMs ?? 300_000),
    });
  }

  async get(runId: string, key: string): Promise<unknown> {
    const entry = this.store.get(`${runId}:${key}`);
    if (!entry) return null;
    if (Date.now() > entry.expiresAt) {
      this.store.delete(`${runId}:${key}`);
      return null;
    }
    return entry.value;
  }

  async delete(runId: string, key: string): Promise<void> {
    this.store.delete(`${runId}:${key}`);
  }
}

// ─── Tier 2: Episodic memory ─────────────────────────────────────────────────

export interface EpisodicMemoryEntry {
  agentId: string;
  runId: string;
  event: string;
  metadata?: Record<string, unknown>;
  timestamp: number;
  /**
   * OTel trace ID associated with the run that produced this entry.
   * Optional for backward compatibility — existing entries without traceId
   * are never returned by queryByTrace.
   *
   * DB impact: migration 037_episodic_indexed_by_trace.sql adds
   * `trace_id TEXT` column + `idx_episodic_trace_id` B-tree index
   * on `episodic_memory(trace_id)`.
   */
  traceId?: string;
}

export interface EpisodicMemoryPort {
  /**
   * Record an episodic event.
   *
   * @param agentId - Owning agent.
   * @param runId   - Run (execution) identifier.
   * @param event   - Event name / label.
   * @param metadata - Arbitrary structured payload.
   * @param opts.traceId - OTel trace ID to associate with this entry (optional,
   *   non-breaking — callers that do not pass it produce entries invisible to queryByTrace).
   */
  record(
    agentId: string,
    runId: string,
    event: string,
    metadata?: Record<string, unknown>,
    opts?: { traceId?: string }
  ): Promise<void>;
  since(
    agentId: string,
    sinceMs: number,
    opts?: { limit?: number }
  ): Promise<EpisodicMemoryEntry[]>;
  /**
   * Retrieve all episodic entries associated with a given OTel trace ID,
   * ordered by insertion (ascending timestamp).
   *
   * @param traceId - OTel W3C trace ID (32 hex chars) or any opaque run-correlation key.
   * @param opts.limit - Maximum number of entries to return (default: unbounded).
   * @returns Entries matching traceId, oldest first.
   */
  queryByTrace(
    traceId: string,
    opts?: { limit?: number }
  ): Promise<EpisodicMemoryEntry[]>;
}

export class InMemoryEpisodicMemory implements EpisodicMemoryPort {
  private events: EpisodicMemoryEntry[] = [];

  async record(
    agentId: string,
    runId: string,
    event: string,
    metadata?: Record<string, unknown>,
    opts?: { traceId?: string }
  ): Promise<void> {
    this.events.push({
      agentId,
      runId,
      event,
      metadata,
      timestamp: Date.now(),
      traceId: opts?.traceId,
    });
  }

  async since(
    agentId: string,
    sinceMs: number,
    opts?: { limit?: number }
  ): Promise<EpisodicMemoryEntry[]> {
    const filtered = this.events
      .filter((e) => e.agentId === agentId && e.timestamp >= sinceMs)
      .sort((a, b) => b.timestamp - a.timestamp);
    return opts?.limit ? filtered.slice(0, opts.limit) : filtered;
  }

  async queryByTrace(
    traceId: string,
    opts?: { limit?: number }
  ): Promise<EpisodicMemoryEntry[]> {
    // Insertion order is preserved (Array.filter is stable; events are pushed in order).
    const filtered = this.events.filter((e) => e.traceId === traceId);
    return opts?.limit ? filtered.slice(0, opts.limit) : filtered;
  }
}

// ─── Tier 3: Semantic memory (C4 — sprint-564) ───────────────────────────────

export interface SemanticMemoryPort {
  /** Query shared knowledge across agents. */
  query(q: string, filters?: BrainQueryFilters): Promise<BrainEntry[]>;
  /** Archive shared knowledge. */
  archive(entry: BrainEntryInput): Promise<BrainEntry | null>;
}

export class InMemorySemanticMemory implements SemanticMemoryPort {
  private entries: BrainEntry[] = [];

  async query(q: string, filters?: BrainQueryFilters): Promise<BrainEntry[]> {
    let results = this.entries.filter((e) =>
      e.content.toLowerCase().includes(q.toLowerCase())
    );
    if (filters?.category)
      results = results.filter((e) => e.category === filters.category);
    if (filters?.tags && filters.tags.length > 0) {
      results = results.filter((e) =>
        e.tags?.some((t) => filters.tags?.includes(t))
      );
    }
    return filters?.limit ? results.slice(0, filters.limit) : results;
  }

  async archive(entry: BrainEntryInput): Promise<BrainEntry | null> {
    const created: BrainEntry = {
      id: `semantic-${this.entries.length + 1}`,
      content: entry.content,
      category: entry.category,
      tags: entry.tags,
      metadata: entry.metadata as Record<string, unknown> | undefined,
      created_at: new Date().toISOString(),
    };
    this.entries.push(created);
    return created;
  }
}

// ─── Tier 4: Procedural memory (C4 — sprint-564) ─────────────────────────────

export interface ProceduralSkill {
  name: string;
  description: string;
  source: "learning-loop" | "manual" | "shared";
  confidence: number;
  chainOfThought?: string;
}

export interface ProceduralMemoryPort {
  /** Resolve all skills available to an agent (learned + shared + private). */
  resolveSkills(agentId: string): Promise<ProceduralSkill[]>;
  /** Register a learned skill for an agent. */
  registerSkill(agentId: string, skill: ProceduralSkill): Promise<void>;
  /** Share a skill with other agents. */
  shareSkill(
    skillName: string,
    fromAgentId: string,
    toAgentIds: string[]
  ): Promise<void>;
}

export class InMemoryProceduralMemory implements ProceduralMemoryPort {
  private skills = new Map<string, ProceduralSkill[]>();
  private shared = new Map<string, string[]>();

  async resolveSkills(agentId: string): Promise<ProceduralSkill[]> {
    const own = this.skills.get(agentId) ?? [];
    const sharedSkills: ProceduralSkill[] = [];
    for (const [fromAgent, skillNames] of this.shared) {
      const fromSkills = this.skills.get(fromAgent) ?? [];
      for (const name of skillNames) {
        const skill = fromSkills.find((s) => s.name === name);
        if (skill) sharedSkills.push({ ...skill, source: "shared" as const });
      }
    }
    return [...own, ...sharedSkills];
  }

  async registerSkill(agentId: string, skill: ProceduralSkill): Promise<void> {
    const existing = this.skills.get(agentId) ?? [];
    this.skills.set(agentId, [...existing, skill]);
  }

  async shareSkill(
    skillName: string,
    fromAgentId: string,
    toAgentIds: string[]
  ): Promise<void> {
    for (const _toAgent of toAgentIds) {
      const shared = this.shared.get(fromAgentId) ?? [];
      if (!shared.includes(skillName)) {
        this.shared.set(fromAgentId, [...shared, skillName]);
      }
    }
  }
}

// ─── Typed errors ─────────────────────────────────────────────────────────────

/**
 * Thrown when the Brain API is unreachable (connection refused, DNS failure,
 * timeout on connect). Callers can retry with backoff or fall back to a local cache.
 */
export class BrainUnavailableError extends Error {
  constructor(message: string, public readonly cause?: unknown) {
    super(message);
    this.name = "BrainUnavailableError";
    Object.setPrototypeOf(this, new.target.prototype);
  }
}

/**
 * Thrown when the Brain API returns 429 (rate limit exceeded).
 * Callers should retry after the delay indicated in the `retryAfterMs` field
 * (extracted from the Retry-After header) or use exponential backoff.
 */
export class BrainRateLimitError extends Error {
  /** Retry-After delay in milliseconds. Derived from the HTTP Retry-After header. */
  readonly retryAfterMs: number;

  constructor(
    message: string,
    retryAfterMs: number,
    public readonly cause?: unknown
  ) {
    super(message);
    this.name = "BrainRateLimitError";
    this.retryAfterMs = retryAfterMs;
    Object.setPrototypeOf(this, new.target.prototype);
  }
}

// ─── Campaign post-mortem + lesson types (Phase 9 — campaign orchestrator) ───

/**
 * Input for archiving a campaign post-mortem.
 * Stored under category `vauban_postmortem` with tags
 * `["postmortem", "campaign:<slug>", "outcome:<outcome>"]`.
 */
export interface PostmortemInput {
  readonly campaign_slug: string;
  readonly outcome: "success" | "partial" | "failure";
  readonly metrics: Readonly<Record<string, number>>;
  readonly what_worked: readonly string[];
  readonly what_failed: readonly string[];
  readonly lessons: readonly string[];
}

/**
 * Input for archiving a reusable lesson learned.
 * Stored under category `vauban_lesson` with tags
 * `["lesson", ...applies_to]`.
 */
export interface LessonInput {
  readonly title: string;
  readonly context: string;
  readonly insight: string;
  readonly applies_to: readonly string[];
  readonly evidence: Readonly<Record<string, unknown>>;
}

// ─── Full BrainPort (4 tiers, additive non-breaking) ─────────────────────────

export interface BrainPort {
  archiveKnowledge(entry: BrainEntryInput): Promise<BrainEntry | null>;
  queryKnowledge?(
    query: string,
    filters?: BrainQueryFilters
  ): Promise<BrainEntry[]>;

  /**
   * Archive a campaign post-mortem. Optional — implementations that do not
   * override this fall back to the default in `HttpBrainAdapter`.
   */
  archivePostmortem?(input: PostmortemInput): Promise<void>;

  /**
   * Archive a reusable lesson learned. Optional — same fallback as above.
   */
  archiveLesson?(input: LessonInput): Promise<void>;

  working?: WorkingMemoryPort;
  episodic?: EpisodicMemoryPort;
  semantic?: SemanticMemoryPort;
  procedural?: ProceduralMemoryPort;
}

// ─── OTel-traced wrapper ──────────────────────────────────────────────────────

import type { Span } from "@opentelemetry/api";
import { SpanStatusCode, trace } from "@opentelemetry/api";

const PORT_TRACER = trace.getTracer("vauban-agent-sdk.ports", "0.1.0");

/**
 * Wrap any BrainPort implementation with OTel spans.
 * archiveKnowledge and queryKnowledge calls each get a span with the category
 * and content preview. Gracefully degrades to noop spans when no OTel SDK
 * is installed.
 *
 * Usage:
 *   const raw: BrainPort = buildHttpBrain(...);
 *   const traced = createTracedBrainPort(raw);
 *   await traced.archiveKnowledge({ content: "..." }) // emits "brain.archiveKnowledge" span
 */
export function createTracedBrainPort(impl: BrainPort): BrainPort {
  return {
    working: impl.working,
    episodic: impl.episodic,
    semantic: impl.semantic,
    procedural: impl.procedural,

    archivePostmortem: impl.archivePostmortem
      ? impl.archivePostmortem.bind(impl)
      : undefined,

    archiveLesson: impl.archiveLesson
      ? impl.archiveLesson.bind(impl)
      : undefined,

    async archiveKnowledge(entry: BrainEntryInput): Promise<BrainEntry | null> {
      return PORT_TRACER.startActiveSpan(
        "brain.archiveKnowledge",
        {
          attributes: {
            "brain.entry.category": entry.category ?? "unknown",
            "brain.entry.content_preview": entry.content.slice(0, 200),
            "brain.entry.tags": entry.tags?.join(",") ?? "",
            "vauban.port.name": "brain",
          },
        },
        async (span: Span) => {
          try {
            const result = await impl.archiveKnowledge(entry);
            if (result) {
              span.setAttribute("brain.entry.id", result.id);
            }
            span.setStatus({ code: SpanStatusCode.OK });
            return result;
          } catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            span.setStatus({ code: SpanStatusCode.ERROR, message });
            if (err instanceof Error) span.recordException(err);
            throw err;
          } finally {
            span.end();
          }
        }
      );
    },

    async queryKnowledge(
      query: string,
      filters?: BrainQueryFilters
    ): Promise<BrainEntry[]> {
      if (!impl.queryKnowledge) return [];
      const qk = impl.queryKnowledge.bind(impl);
      return PORT_TRACER.startActiveSpan(
        "brain.queryKnowledge",
        {
          attributes: {
            "brain.query.preview": query.slice(0, 200),
            "brain.query.category": filters?.category ?? "none",
            "brain.query.limit": filters?.limit ?? -1,
            "vauban.port.name": "brain",
          },
        },
        async (span: Span) => {
          try {
            const result = await qk(query, filters);
            span.setAttribute("brain.query.result_count", result.length);
            span.setStatus({ code: SpanStatusCode.OK });
            return result;
          } catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            span.setStatus({ code: SpanStatusCode.ERROR, message });
            if (err instanceof Error) span.recordException(err);
            throw err;
          } finally {
            span.end();
          }
        }
      );
    },
  };
}
