/*
 * Priority Forge — pgvector memory-hub adapter
 *
 * Second adapter alongside `xmem`, intentionally vendor-free. Runs against
 * any Postgres 14+ with the `vector` extension installed. Purpose: prove the
 * MemoryHub abstraction holds against a non-xtrace backend (the "high surface
 * area" promise) and provide a deterministic fallback when xmem is degraded.
 *
 * Embedding strategy
 * ------------------
 * The default `embedSummary` is a 384-dim normalized bag-of-words hash. It is
 * deliberately tiny (zero npm deps) and good enough to make the contract test
 * for search() pass: searching for a substring of an event's summary returns
 * that event with non-zero score. Users wanting real semantic quality pass
 * their own embedder (`embed: (text) => Promise<number[]>`) at construction
 * — e.g. wrapping @xenova/transformers, OpenAI embeddings, or a hosted model.
 *
 * Schema bootstrap: see `setup-pgvector.sql` in this directory.
 */

import { createHash, randomUUID } from 'crypto';
import { Pool, PoolConfig } from 'pg';
import {
  BlockerEdge,
  ClaimInfo,
  ClaimOutcome,
  ClaimResult,
  CreateSharedTaskInput,
  MemoryHub,
  MemoryHubHealth,
  OrchestrationHub,
  ReleaseResult,
  CancelResult,
  RenewResult,
  SharedTask,
  SharedTaskFilter,
  SharedTaskStatus,
  TeamActivityFilter,
  TeamMemoryEvent,
  TeamMemoryEventType,
} from '../types';
import { loadTeamHubConfig } from '../team-hub-config';
import {
  defaultSinceTs,
  listDecayCandidates as filterDecayCandidates,
  rankMemoryResults,
} from '../salience';

const EMBED_DIM = 384;

export type Embedder = (text: string) => Promise<number[]>;

export interface PgvectorAdapterOptions {
  connectionString: string;
  /** Custom embedder. Defaults to the built-in bag-of-words hash. */
  embed?: Embedder;
  /** Optional pg.Pool config overrides (max, idleTimeoutMillis, etc.). */
  poolConfig?: PoolConfig;
  /**
   * Identity hardening (optional). When all three are provided AND there
   * is a matching row in `agent_identities`, every write / claim / release
   * / renewLease verifies the configured token before proceeding. When
   * any is missing, verification is off and the adapter behaves as a
   * standard MemoryHub-only client. Reads are never gated.
   */
  teamId?: string;
  agentId?: string;
  agentToken?: string;
}

// ──────────────────────────────────────────────────────────────────────────
// Built-in bag-of-words hash embedder. Zero deps. Deterministic. Bad at
// semantic similarity but good enough to satisfy the contract: searching for
// any token that appears in a summary returns events containing that token.
// ──────────────────────────────────────────────────────────────────────────
function fnv1a(s: string): number {
  let h = 0x811c9dc5;
  for (let i = 0; i < s.length; i++) {
    h ^= s.charCodeAt(i);
    h = Math.imul(h, 0x01000193);
  }
  return h >>> 0;
}

async function defaultEmbedder(text: string): Promise<number[]> {
  const vec = new Array<number>(EMBED_DIM).fill(0);
  const tokens = text.toLowerCase().match(/[a-z0-9]+/g) ?? [];
  for (const tok of tokens) {
    vec[fnv1a(tok) % EMBED_DIM] += 1;
  }
  let norm = 0;
  for (const v of vec) norm += v * v;
  norm = Math.sqrt(norm);
  if (norm === 0) return vec;
  return vec.map((v) => v / norm);
}

function vectorToPgLiteral(vec: number[]): string {
  // pgvector accepts '[1,2,3]' style string literals.
  return `[${vec.join(',')}]`;
}

interface PgRow {
  event_id: string;
  team_id: string;
  agent_id: string;
  workspace_id: string;
  event_type: string;
  ts: Date;
  payload: Record<string, unknown>;
  summary: string;
  metadata: Record<string, unknown>;
  score?: number;
}

function rowToEvent(row: PgRow): TeamMemoryEvent {
  return {
    eventId: row.event_id,
    teamId: row.team_id,
    agentId: row.agent_id,
    workspaceId: row.workspace_id,
    eventType: row.event_type as TeamMemoryEventType,
    timestamp: row.ts.toISOString(),
    payload: row.payload ?? {},
    summary: row.summary ?? '',
    metadata: (row.metadata ?? {}) as Record<string, string | number | boolean | string[] | null>,
  };
}

export class PgvectorMemoryHub implements MemoryHub, OrchestrationHub {
  readonly backend = 'pgvector' as const;
  readonly supportsOrchestration = true;

  private pool: Pool;
  private embed: Embedder;
  // Identity hardening — all three must be non-empty for verification to
  // engage. If any is missing, verifyAgent() short-circuits to true and
  // the adapter accepts writes from any caller (back-compat for setups
  // that don't yet use agent_identities).
  private hardenedTeamId?: string;
  private hardenedAgentId?: string;
  private hardenedTokenHash?: Buffer;

  constructor(opts: PgvectorAdapterOptions) {
    this.pool = new Pool({ connectionString: opts.connectionString, ...opts.poolConfig });
    this.embed = opts.embed ?? defaultEmbedder;
    if (opts.teamId && opts.agentId && opts.agentToken) {
      this.hardenedTeamId = opts.teamId;
      this.hardenedAgentId = opts.agentId;
      this.hardenedTokenHash = createHash('sha256').update(opts.agentToken).digest();
    }
  }

  async close(): Promise<void> {
    await this.pool.end().catch(() => undefined);
  }

  /**
   * Verify the configured agent token matches the registered hash for
   * (teamId, claimedAgentId). Returns true (skip verification) when the
   * adapter wasn't constructed with hardening fields. Returns false when
   * the caller's claimed agentId doesn't match our configured agentId
   * (a local PF instance can ONLY act as its own configured agent), or
   * when the token hash doesn't match the registered row.
   */
  private async verifyAgent(claimedAgentId: string): Promise<boolean> {
    if (!this.hardenedTeamId || !this.hardenedAgentId || !this.hardenedTokenHash) {
      return true;
    }
    if (claimedAgentId !== this.hardenedAgentId) return false;
    try {
      const result = await this.pool.query(
        `SELECT 1 FROM agent_identities
         WHERE team_id = $1 AND agent_id = $2 AND token_hash = $3`,
        [this.hardenedTeamId, this.hardenedAgentId, this.hardenedTokenHash],
      );
      return result.rows.length > 0;
    } catch (err) {
      console.error(
        '[memory-hub:pgvector] verifyAgent failed:',
        err instanceof Error ? err.message : err,
      );
      return false;
    }
  }

  /**
   * Bootstrap method (admin-level). Registers an agent's token hash so
   * future writes by that agent (configured with the same token) pass
   * verifyAgent(). Idempotent — re-registering with a new token rotates
   * the credential. Uses the existing pool's connection, so requires
   * INSERT privilege on agent_identities.
   *
   * Not part of the OrchestrationHub interface; consumed via duck-typed
   * cast from the MCP tool. Other adapters don't expose this method.
   */
  async registerAgent(
    teamId: string,
    agentId: string,
    token: string,
  ): Promise<{ ok: boolean; reason?: string }> {
    try {
      const hash = createHash('sha256').update(token).digest();
      await this.pool.query(
        `INSERT INTO agent_identities (team_id, agent_id, token_hash)
         VALUES ($1, $2, $3)
         ON CONFLICT (team_id, agent_id) DO UPDATE
           SET token_hash = EXCLUDED.token_hash, registered_at = now()`,
        [teamId, agentId, hash],
      );
      return { ok: true };
    } catch (err) {
      return { ok: false, reason: err instanceof Error ? err.message : String(err) };
    }
  }

  // ──────────────────────────────────────────────────────────────────────
  // write — UPSERT on event_id (idempotent against the reader's retries and
  // the subscriber's at-least-once delivery).
  // ──────────────────────────────────────────────────────────────────────
  async write(event: TeamMemoryEvent): Promise<{ accepted: boolean; remoteId?: string }> {
    if (!(await this.verifyAgent(event.agentId))) {
      console.warn(
        '[memory-hub:pgvector] write rejected: unauthorized agent',
        event.agentId,
      );
      return { accepted: false };
    }
    try {
      const embedding = await this.embed(event.summary || JSON.stringify(event.payload));
      const result = await this.pool.query(
        `INSERT INTO team_memory_events
          (event_id, team_id, agent_id, workspace_id, event_type, ts, payload, summary, metadata, embedding)
         VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8, $9::jsonb, $10::vector)
         ON CONFLICT (event_id) DO UPDATE SET
           team_id      = EXCLUDED.team_id,
           agent_id     = EXCLUDED.agent_id,
           workspace_id = EXCLUDED.workspace_id,
           event_type   = EXCLUDED.event_type,
           ts           = EXCLUDED.ts,
           payload      = EXCLUDED.payload,
           summary      = EXCLUDED.summary,
           metadata     = EXCLUDED.metadata,
           embedding    = EXCLUDED.embedding
         RETURNING event_id`,
        [
          event.eventId,
          event.teamId,
          event.agentId,
          event.workspaceId,
          event.eventType,
          event.timestamp,
          JSON.stringify(event.payload ?? {}),
          event.summary ?? '',
          JSON.stringify(event.metadata ?? {}),
          vectorToPgLiteral(embedding),
        ],
      );
      return { accepted: true, remoteId: result.rows[0]?.event_id };
    } catch (err) {
      console.error('[memory-hub:pgvector] write failed:', err instanceof Error ? err.message : err);
      return { accepted: false };
    }
  }

  // ──────────────────────────────────────────────────────────────────────
  // read — SELECT with full filter pushdown into SQL (much cleaner than
  // client-side filtering). Returns events sorted by ts DESC.
  // ──────────────────────────────────────────────────────────────────────
  async read(filter: TeamActivityFilter): Promise<TeamMemoryEvent[]> {
    try {
      const thresholds = loadTeamHubConfig().salience;
      const sinceTs = filter.sinceTs ?? defaultSinceTs(thresholds);
      const conds: string[] = ['team_id = $1'];
      const params: unknown[] = [filter.teamId];
      const p = (v: unknown) => {
        params.push(v);
        return `$${params.length}`;
      };
      conds.push(`ts >= ${p(sinceTs)}`);
      if (filter.agentIds && filter.agentIds.length > 0) conds.push(`agent_id = ANY(${p(filter.agentIds)})`);
      if (filter.workspaceIds && filter.workspaceIds.length > 0) conds.push(`workspace_id = ANY(${p(filter.workspaceIds)})`);
      if (filter.eventTypes && filter.eventTypes.length > 0) conds.push(`event_type = ANY(${p(filter.eventTypes)})`);
      if (!filter.includeTombstoned) {
        conds.push(`(metadata->>'tombstonedAt' IS NULL)`);
      }
      const limit = filter.limit && filter.limit > 0 ? Math.min(filter.limit, 1000) : 200;
      const fetchLimit = Math.min(limit * 4, 1000);
      const sql = `
        SELECT event_id, team_id, agent_id, workspace_id, event_type, ts, payload, summary, metadata
        FROM team_memory_events
        WHERE ${conds.join(' AND ')}
        ORDER BY ts DESC
        LIMIT ${fetchLimit}
      `;
      const result = await this.pool.query<PgRow>(sql, params);
      const events = result.rows.map(rowToEvent);
      const ranked = rankMemoryResults(events, thresholds);
      return ranked.slice(0, limit);
    } catch (err) {
      console.error('[memory-hub:pgvector] read failed:', err instanceof Error ? err.message : err);
      return [];
    }
  }

  // ──────────────────────────────────────────────────────────────────────
  // search — cosine similarity on the embedding column. Returns events with
  // a `score` (1 - cosine_distance) stuffed into metadata for callers that
  // want to surface relevance.
  // ──────────────────────────────────────────────────────────────────────
  async search(teamId: string, query: string, limit = 20): Promise<TeamMemoryEvent[]> {
    try {
      const thresholds = loadTeamHubConfig().salience;
      const qEmbed = await this.embed(query);
      const fetchN = Math.min(Math.max(limit, 1) * 4, 200);
      const result = await this.pool.query<PgRow>(
        `SELECT event_id, team_id, agent_id, workspace_id, event_type, ts, payload, summary, metadata,
                (1 - (embedding <=> $2::vector)) AS score
         FROM team_memory_events
         WHERE team_id = $1
           AND embedding IS NOT NULL
           AND (metadata->>'tombstonedAt' IS NULL)
           AND ts >= $4
         ORDER BY embedding <=> $2::vector ASC
         LIMIT $3`,
        [
          teamId,
          vectorToPgLiteral(qEmbed),
          fetchN,
          defaultSinceTs(thresholds),
        ],
      );
      const semanticScores = new Map<string, number>();
      const events: TeamMemoryEvent[] = [];
      for (const row of result.rows) {
        const ev = rowToEvent(row);
        if (typeof row.score === 'number') semanticScores.set(ev.eventId, row.score);
        events.push(ev);
      }
      const ranked = rankMemoryResults(events, thresholds, semanticScores);
      for (const ev of ranked.slice(0, limit)) {
        void this.touchMemory(ev.eventId, ev.teamId).catch(() => {});
      }
      return ranked.slice(0, limit);
    } catch (err) {
      console.error('[memory-hub:pgvector] search failed:', err instanceof Error ? err.message : err);
      return [];
    }
  }

  async touchMemory(eventId: string, teamId: string): Promise<{ ok: boolean }> {
    try {
      await this.pool.query(
        `UPDATE team_memory_events
         SET metadata = metadata || jsonb_build_object(
           'lastAccessedAt', to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
           'accessCount', COALESCE((metadata->>'accessCount')::int, 0) + 1
         )
         WHERE event_id = $1 AND team_id = $2`,
        [eventId, teamId],
      );
      return { ok: true };
    } catch {
      return { ok: false };
    }
  }

  async setMemoryFlags(
    eventId: string,
    teamId: string,
    flags: { pinned?: boolean; clearTombstoneCandidate?: boolean },
  ): Promise<{ ok: boolean }> {
    try {
      const patch: Record<string, unknown> = {};
      if (flags.pinned !== undefined) patch.pinned = flags.pinned;
      if (flags.clearTombstoneCandidate) patch.tombstoneCandidate = false;
      await this.pool.query(
        `UPDATE team_memory_events
         SET metadata = metadata || $3::jsonb
         WHERE event_id = $1 AND team_id = $2`,
        [eventId, teamId, JSON.stringify(patch)],
      );
      return { ok: true };
    } catch {
      return { ok: false };
    }
  }

  async archiveMemory(eventId: string, teamId: string): Promise<{ ok: boolean }> {
    try {
      await this.pool.query(
        `UPDATE team_memory_events
         SET metadata = metadata || jsonb_build_object(
           'tombstonedAt', to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
           'tombstoneCandidate', true
         )
         WHERE event_id = $1 AND team_id = $2`,
        [eventId, teamId],
      );
      return { ok: true };
    } catch {
      return { ok: false };
    }
  }

  async listDecayCandidates(teamId: string): Promise<TeamMemoryEvent[]> {
    const events = await this.read({
      teamId,
      sinceTs: new Date(0).toISOString(),
      limit: 1000,
      includeTombstoned: false,
    });
    return filterDecayCandidates(events, loadTeamHubConfig().salience);
  }

  // ──────────────────────────────────────────────────────────────────────
  // blockersFor — generic over the read() path. Same shape as the other
  // adapters; nothing pgvector-specific here.
  // ──────────────────────────────────────────────────────────────────────
  async blockersFor(teamId: string, agentId: string): Promise<BlockerEdge[]> {
    const events = await this.read({ teamId, eventTypes: ['derived_blocker'] });
    const latest = new Map<string, BlockerEdge>();
    for (const ev of events) {
      const edge = ev.payload as Partial<BlockerEdge>;
      if (
        typeof edge.blockedTaskId !== 'string' ||
        typeof edge.blockingTaskId !== 'string' ||
        typeof edge.blockedAgentId !== 'string' ||
        typeof edge.blockingAgentId !== 'string' ||
        (edge.status !== 'active' && edge.status !== 'cleared')
      ) continue;
      if (edge.blockedAgentId !== agentId) continue;
      const key = `${edge.blockedTaskId}::${edge.blockingTaskId}`;
      if (!latest.has(key)) {
        latest.set(key, {
          blockedTaskId: edge.blockedTaskId,
          blockedAgentId: edge.blockedAgentId,
          blockingTaskId: edge.blockingTaskId,
          blockingAgentId: edge.blockingAgentId,
          status: edge.status,
          observedAt: typeof edge.observedAt === 'string' ? edge.observedAt : ev.timestamp,
        });
      }
    }
    return [...latest.values()].filter((e) => e.status === 'active');
  }

  // ──────────────────────────────────────────────────────────────────────
  // health — verifies (a) connection, (b) `vector` extension present,
  // (c) team_memory_events table exists. Reports the missing piece so the
  // user can run setup-pgvector.sql if needed.
  // ──────────────────────────────────────────────────────────────────────
  async health(): Promise<MemoryHubHealth> {
    try {
      await this.pool.query('SELECT 1');
    } catch (err) {
      return {
        ok: false,
        backend: this.backend,
        detail: `connection failed: ${err instanceof Error ? err.message : String(err)}`,
      };
    }
    try {
      const ext = await this.pool.query(`SELECT extname FROM pg_extension WHERE extname = 'vector'`);
      if (ext.rowCount === 0) {
        return { ok: false, backend: this.backend, detail: "extension 'vector' not installed — run setup-pgvector.sql" };
      }
    } catch (err) {
      return {
        ok: false,
        backend: this.backend,
        detail: `extension check failed: ${err instanceof Error ? err.message : String(err)}`,
      };
    }
    try {
      const tbl = await this.pool.query(`SELECT to_regclass('public.team_memory_events') AS t`);
      if (!tbl.rows[0]?.t) {
        return { ok: false, backend: this.backend, detail: "table 'team_memory_events' not found — run setup-pgvector.sql" };
      }
    } catch (err) {
      return {
        ok: false,
        backend: this.backend,
        detail: `table check failed: ${err instanceof Error ? err.message : String(err)}`,
      };
    }
    return { ok: true, backend: this.backend, detail: 'connection + vector extension + schema present' };
  }

  // ──────────────────────────────────────────────────────────────────────
  // OrchestrationHub — atomic claim via UPDATE...WHERE CAS
  //
  // Atomicity is guaranteed by Postgres's row-level locking on UPDATE.
  // The WHERE clause encodes the claim predicate (unclaimed / expired /
  // same-agent re-claim), so two concurrent claim attempts on the same
  // row result in exactly one matching UPDATE. The loser's UPDATE returns
  // 0 rows; we re-read to typify the failure (already_claimed /
  // completed / cancelled / not_found).
  //
  // No explicit BEGIN/COMMIT needed — each UPDATE is its own implicit txn
  // and acquires the row lock for the predicate evaluation.
  // ──────────────────────────────────────────────────────────────────────

  private rowToSharedTask(row: {
    id: string;
    team_id: string;
    title: string;
    description: string | null;
    deadline: string | null;
    priority: string | null;
    originator_agent_id: string;
    originator_team_id: string;
    created_at: Date | string;
    current_claim: ClaimInfo | null;
    claim_history: unknown[] | null;
    status: string;
  }): SharedTask {
    return {
      id: row.id,
      teamId: row.team_id,
      title: row.title,
      description: row.description ?? undefined,
      deadline: row.deadline ?? undefined,
      priority: row.priority ?? undefined,
      originatorAgentId: row.originator_agent_id,
      originatorTeamId: row.originator_team_id,
      createdAt:
        row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at),
      currentClaim: row.current_claim ?? null,
      claimHistory: (row.claim_history ?? []) as SharedTask['claimHistory'],
      status: row.status as SharedTaskStatus,
    };
  }

  async create(input: CreateSharedTaskInput): Promise<SharedTask> {
    if (!(await this.verifyAgent(input.originatorAgentId))) {
      throw new Error(
        `unauthorized agent ${input.originatorAgentId} — register via xmem_register_agent or remove agentToken from config`,
      );
    }
    const id = `st_${randomUUID()}`;
    const result = await this.pool.query(
      `INSERT INTO shared_tasks
         (id, team_id, title, description, deadline, priority, originator_agent_id, originator_team_id, status)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'claimable')
       RETURNING *`,
      [
        id,
        input.teamId,
        input.title,
        input.description ?? null,
        input.deadline ?? null,
        input.priority ?? null,
        input.originatorAgentId,
        input.originatorTeamId,
      ],
    );
    return this.rowToSharedTask(result.rows[0]);
  }

  async claim(
    sharedTaskId: string,
    agentId: string,
    agentTeamId: string,
    ttlMs: number,
  ): Promise<ClaimResult> {
    if (!(await this.verifyAgent(agentId))) {
      return { ok: false, reason: 'unauthorized_agent' };
    }
    try {
      const result = await this.pool.query(
        `UPDATE shared_tasks
         SET current_claim = jsonb_build_object(
               'holderAgentId', $2::text,
               'holderTeamId', $3::text,
               'claimedAt', to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
               'leaseUntil', to_char((now() + ($4::bigint || ' milliseconds')::interval) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')
             ),
             claim_history = claim_history || jsonb_build_array(jsonb_build_object(
               'event', 'claimed',
               'agentId', $2::text,
               'agentTeamId', $3::text,
               'at', to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')
             )),
             status = 'claimed'
         WHERE id = $1
           AND status IN ('claimable', 'claimed')
           AND (
             current_claim IS NULL
             OR (current_claim->>'leaseUntil')::timestamptz < now()
             OR current_claim->>'holderAgentId' = $2::text
           )
         RETURNING *`,
        [sharedTaskId, agentId, agentTeamId, ttlMs],
      );
      if (result.rows.length > 0) {
        const sharedTask = this.rowToSharedTask(result.rows[0]);
        if (!sharedTask.currentClaim) return { ok: false, reason: 'not_found' };
        return { ok: true, sharedTask, claim: sharedTask.currentClaim };
      }
      const current = await this.get(sharedTaskId);
      if (!current) return { ok: false, reason: 'not_found' };
      if (current.status === 'completed') return { ok: false, reason: 'completed' };
      if (current.status === 'cancelled') return { ok: false, reason: 'cancelled' };
      if (current.currentClaim) {
        return { ok: false, reason: 'already_claimed', currentClaim: current.currentClaim };
      }
      return {
        ok: false,
        reason: 'already_claimed',
        currentClaim: {
          holderAgentId: 'unknown',
          holderTeamId: 'unknown',
          claimedAt: new Date().toISOString(),
          leaseUntil: new Date().toISOString(),
        },
      };
    } catch (err) {
      console.error('[memory-hub:pgvector] claim failed:', err instanceof Error ? err.message : err);
      return { ok: false, reason: 'not_found' };
    }
  }

  async release(
    sharedTaskId: string,
    agentId: string,
    outcome: ClaimOutcome,
  ): Promise<ReleaseResult> {
    if (!(await this.verifyAgent(agentId))) {
      return { ok: false, reason: 'unauthorized_agent' };
    }
    try {
      const result = await this.pool.query(
        `UPDATE shared_tasks
         SET current_claim = NULL,
             claim_history = claim_history || jsonb_build_array(jsonb_build_object(
               'event', 'released',
               'agentId', $2::text,
               'agentTeamId', current_claim->>'holderTeamId',
               'at', to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
               'outcome', $3::text
             )),
             status = CASE $3::text
               WHEN 'completed' THEN 'completed'
               WHEN 'cancelled' THEN 'cancelled'
               ELSE 'claimable'
             END
         WHERE id = $1
           AND current_claim IS NOT NULL
           AND current_claim->>'holderAgentId' = $2::text
         RETURNING *`,
        [sharedTaskId, agentId, outcome],
      );
      if (result.rows.length > 0) {
        return { ok: true, sharedTask: this.rowToSharedTask(result.rows[0]) };
      }
      const current = await this.get(sharedTaskId);
      if (!current) return { ok: false, reason: 'not_found' };
      if (!current.currentClaim) return { ok: false, reason: 'not_currently_claimed' };
      return { ok: false, reason: 'not_claim_holder' };
    } catch (err) {
      console.error('[memory-hub:pgvector] release failed:', err instanceof Error ? err.message : err);
      return { ok: false };
    }
  }

  async cancel(sharedTaskId: string, byAgentId: string): Promise<CancelResult> {
    if (!(await this.verifyAgent(byAgentId))) {
      return { ok: false, reason: 'unauthorized_agent' };
    }
    try {
      const result = await this.pool.query(
        `UPDATE shared_tasks
            SET status = 'cancelled'
          WHERE id = $1
            AND originator_agent_id = $2::text
            AND current_claim IS NULL
            AND status = 'claimable'
        RETURNING *`,
        [sharedTaskId, byAgentId],
      );
      if (result.rows.length > 0) {
        return { ok: true, sharedTask: this.rowToSharedTask(result.rows[0]) };
      }
      const current = await this.get(sharedTaskId);
      if (!current) return { ok: false, reason: 'not_found' };
      if (current.originatorAgentId !== byAgentId) return { ok: false, reason: 'unauthorized_originator' };
      if (current.currentClaim) return { ok: false, reason: 'currently_claimed' };
      return { ok: false, reason: 'already_terminal' };
    } catch (err) {
      console.error('[memory-hub:pgvector] cancel failed:', err instanceof Error ? err.message : err);
      return { ok: false };
    }
  }

  async renewLease(
    sharedTaskId: string,
    agentId: string,
    ttlMs: number,
  ): Promise<RenewResult> {
    if (!(await this.verifyAgent(agentId))) {
      return { ok: false, reason: 'unauthorized_agent' };
    }
    try {
      const result = await this.pool.query(
        `UPDATE shared_tasks
         SET current_claim = jsonb_set(
               current_claim,
               '{leaseUntil}',
               to_jsonb(to_char((now() + ($3::bigint || ' milliseconds')::interval) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'))
             ),
             claim_history = claim_history || jsonb_build_array(jsonb_build_object(
               'event', 'renewed',
               'agentId', $2::text,
               'agentTeamId', current_claim->>'holderTeamId',
               'at', to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')
             ))
         WHERE id = $1
           AND current_claim IS NOT NULL
           AND current_claim->>'holderAgentId' = $2::text
           AND (current_claim->>'leaseUntil')::timestamptz > now()
         RETURNING *`,
        [sharedTaskId, agentId, ttlMs],
      );
      if (result.rows.length > 0) {
        const sharedTask = this.rowToSharedTask(result.rows[0]);
        if (!sharedTask.currentClaim) return { ok: false, reason: 'not_claim_holder' };
        return { ok: true, claim: sharedTask.currentClaim };
      }
      const current = await this.get(sharedTaskId);
      if (!current) return { ok: false, reason: 'not_found' };
      if (!current.currentClaim || current.currentClaim.holderAgentId !== agentId) {
        return { ok: false, reason: 'not_claim_holder' };
      }
      if (new Date(current.currentClaim.leaseUntil).getTime() < Date.now()) {
        return { ok: false, reason: 'expired' };
      }
      return { ok: false, reason: 'not_claim_holder' };
    } catch (err) {
      console.error('[memory-hub:pgvector] renewLease failed:', err instanceof Error ? err.message : err);
      return { ok: false };
    }
  }

  async get(sharedTaskId: string): Promise<SharedTask | null> {
    try {
      const result = await this.pool.query(`SELECT * FROM shared_tasks WHERE id = $1`, [sharedTaskId]);
      if (result.rows.length === 0) return null;
      return this.rowToSharedTask(result.rows[0]);
    } catch (err) {
      console.error('[memory-hub:pgvector] get failed:', err instanceof Error ? err.message : err);
      return null;
    }
  }

  async list(filter: SharedTaskFilter): Promise<SharedTask[]> {
    try {
      const conds: string[] = ['team_id = $1'];
      const params: unknown[] = [filter.teamId];
      if (filter.status) {
        params.push(filter.status);
        conds.push(`status = $${params.length}`);
      }
      const limit = filter.limit && filter.limit > 0 ? Math.min(filter.limit, 1000) : 200;
      const result = await this.pool.query(
        `SELECT * FROM shared_tasks WHERE ${conds.join(' AND ')} ORDER BY created_at DESC LIMIT ${limit}`,
        params,
      );
      return result.rows.map((r) => this.rowToSharedTask(r));
    } catch (err) {
      console.error('[memory-hub:pgvector] list failed:', err instanceof Error ? err.message : err);
      return [];
    }
  }

  async getMyClaims(teamId: string, agentId: string): Promise<SharedTask[]> {
    try {
      const result = await this.pool.query(
        `SELECT * FROM shared_tasks
         WHERE team_id = $1
           AND current_claim IS NOT NULL
           AND current_claim->>'holderAgentId' = $2
           AND (current_claim->>'leaseUntil')::timestamptz > now()
         ORDER BY (current_claim->>'claimedAt')::timestamptz DESC`,
        [teamId, agentId],
      );
      return result.rows.map((r) => this.rowToSharedTask(r));
    } catch (err) {
      console.error('[memory-hub:pgvector] getMyClaims failed:', err instanceof Error ? err.message : err);
      return [];
    }
  }

  async searchSharedTasks(teamId: string, query: string, limit = 20): Promise<SharedTask[]> {
    const cap = limit && limit > 0 ? Math.min(limit, 200) : 20;
    try {
      const result = await this.pool.query(
        `SELECT * FROM shared_tasks
          WHERE team_id = $1
            AND (title ILIKE $2 OR description ILIKE $2)
          ORDER BY created_at DESC
          LIMIT ${cap}`,
        [teamId, `%${query}%`],
      );
      return result.rows.map((r) => this.rowToSharedTask(r));
    } catch (err) {
      console.error('[memory-hub:pgvector] searchSharedTasks failed:', err instanceof Error ? err.message : err);
      return [];
    }
  }
}
