/*
 * Priority Forge — memory-hub adapter TEMPLATE
 *
 * Copy this file to <vendor>-adapter.ts and fill in the TODOs.
 * Then complete steps 2-6 from ADAPTERS.md (types union, factory case,
 * MCP enum, smoke test, docs).
 *
 * Conventions:
 *   - Never throw from any of the five methods. Log + return empty /
 *     {accepted:false}. The reader retries; the subscriber moves on.
 *   - Push as many filters as the vendor supports server-side; apply the
 *     rest client-side after read() returns.
 *   - Use TeamMemoryEvent.eventId as the vendor's idempotency key when
 *     supported.
 */

import {
  BlockerEdge,
  MemoryHub,
  MemoryHubHealth,
  TeamActivityFilter,
  TeamMemoryEvent,
  TeamMemoryEventType,
} from '../types';

interface VendorAdapterOptions {
  baseUrl: string;
  apiKey: string;
  // TODO: add any vendor-specific options here (region, project_id, etc.)
  fetchImpl?: typeof fetch;
  timeoutMs?: number;
}

export class VendorMemoryHub implements MemoryHub {
  // TODO: change to your backend's literal name; must match what you add to
  // the MemoryHubBackend union in types.ts and to the factory.ts switch.
  readonly backend = 'vendor' as const as unknown as MemoryHub['backend'];

  private baseUrl: string;
  private apiKey: string;
  private fetchImpl: typeof fetch;
  private timeoutMs: number;

  constructor(opts: VendorAdapterOptions) {
    this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
    this.apiKey = opts.apiKey;
    this.fetchImpl = opts.fetchImpl ?? fetch;
    this.timeoutMs = opts.timeoutMs ?? 10000;
  }

  // ──────────────────────────────────────────────────────────────────────
  // write — persist a TeamMemoryEvent. Called once per PF mutation that
  // passes privacy rules, plus once per cross-agent blocker transition.
  // ──────────────────────────────────────────────────────────────────────
  async write(event: TeamMemoryEvent): Promise<{ accepted: boolean; remoteId?: string }> {
    try {
      // TODO: translate `event` into the vendor's wire format.
      // Tenancy: PF event.teamId → vendor's tenant/namespace dimension.
      //          PF event.agentId → vendor's per-user/per-agent dimension.
      // Two channels (preferred): structured payload for faithful round-trip,
      //                           natural-language summary for semantic search.
      //
      // Example shape:
      //
      // const body = {
      //   tenant_id: event.teamId,
      //   user_id:   event.agentId,
      //   raw_payload: JSON.stringify(event.payload),  // structured
      //   text:        event.summary,                  // natural-language
      //   metadata: {
      //     event_type:   event.eventType,
      //     event_id:     event.eventId,    // ← idempotency key
      //     workspace_id: event.workspaceId,
      //     timestamp:    event.timestamp,
      //     ...event.metadata,
      //   },
      // };
      //
      // const res = await this.fetchImpl(`${this.baseUrl}/<vendor-write-path>`, {
      //   method: 'POST',
      //   headers: { Authorization: `Bearer ${this.apiKey}`, 'Content-Type': 'application/json' },
      //   body: JSON.stringify(body),
      //   signal: AbortSignal.timeout(this.timeoutMs),
      // });
      // if (!res.ok) throw new Error(`HTTP ${res.status}`);
      // const json = await res.json();
      // return { accepted: true, remoteId: json.id };

      throw new Error('TODO: implement write');
    } catch (err) {
      console.error('[memory-hub:vendor] write failed:', err instanceof Error ? err.message : err);
      return { accepted: false };
    }
  }

  // ──────────────────────────────────────────────────────────────────────
  // read — return events for filter.teamId, sorted desc by timestamp.
  // Push server-side filters where supported; apply the rest client-side.
  // ──────────────────────────────────────────────────────────────────────
  async read(filter: TeamActivityFilter): Promise<TeamMemoryEvent[]> {
    try {
      // TODO: build the vendor's read query. The reader passes filter.sinceTs
      // every poll for incremental sync. Translate to whatever the vendor
      // supports (cursor, since= param, query AST, etc.).
      //
      // const params = new URLSearchParams({ tenant_id: filter.teamId });
      // if (filter.limit) params.set('limit', String(filter.limit));
      //
      // const res = await this.fetchImpl(`${this.baseUrl}/<list-path>?${params}`, {
      //   method: 'GET',
      //   headers: { Authorization: `Bearer ${this.apiKey}` },
      //   signal: AbortSignal.timeout(this.timeoutMs),
      // });
      // if (!res.ok) throw new Error(`HTTP ${res.status}`);
      // const json = await res.json();
      // const events = (json.results ?? []).map(recordToTeamMemoryEvent);
      //
      // // Client-side filters that the vendor didn't apply:
      // return events.filter((ev) => {
      //   if (filter.sinceTs && ev.timestamp < filter.sinceTs) return false;
      //   if (filter.agentIds?.length && !filter.agentIds.includes(ev.agentId)) return false;
      //   if (filter.eventTypes?.length && !filter.eventTypes.includes(ev.eventType)) return false;
      //   return true;
      // });

      throw new Error('TODO: implement read');
    } catch (err) {
      console.error('[memory-hub:vendor] read failed:', err instanceof Error ? err.message : err);
      return [];
    }
  }

  // ──────────────────────────────────────────────────────────────────────
  // search — semantic / keyword search across the team's events.
  // ──────────────────────────────────────────────────────────────────────
  async search(teamId: string, query: string, limit = 20): Promise<TeamMemoryEvent[]> {
    try {
      // TODO: hit the vendor's search endpoint. If the vendor doesn't have
      // one, fall back to read() + client-side substring scoring (see
      // dryrun-adapter.ts for a 30-line reference impl).
      throw new Error('TODO: implement search');
    } catch (err) {
      console.error('[memory-hub:vendor] search failed:', err instanceof Error ? err.message : err);
      return [];
    }
  }

  // ──────────────────────────────────────────────────────────────────────
  // blockersFor — return ACTIVE BlockerEdges where blockedAgentId === agentId.
  // For most adapters this is read({eventTypes: ['derived_blocker']}) +
  // last-status-wins-per-pair logic. See dryrun-adapter.ts or xmem-adapter.ts
  // for a copy-pasteable implementation (≈20 lines).
  // ──────────────────────────────────────────────────────────────────────
  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 — cheap reachability probe. Don't burn the user's API quota.
  // ──────────────────────────────────────────────────────────────────────
  async health(): Promise<MemoryHubHealth> {
    try {
      // TODO: a HEAD on a known-public endpoint, or a single GET with limit=1.
      // Treat 4xx as "ok" if it means "auth needed" — the route exists.
      // Treat 404 / 5xx / network errors as not-ok.
      //
      // const res = await this.fetchImpl(`${this.baseUrl}/<health-path>`, {
      //   method: 'GET',
      //   headers: { Authorization: `Bearer ${this.apiKey}` },
      //   signal: AbortSignal.timeout(this.timeoutMs),
      // });
      // if (res.status === 404) return { ok: false, backend: this.backend, detail: 'route not deployed' };
      // if (res.status >= 500) return { ok: false, backend: this.backend, detail: `HTTP ${res.status}` };
      // return { ok: true, backend: this.backend, detail: `HTTP ${res.status}` };

      return { ok: false, backend: this.backend, detail: 'TODO: implement health' };
    } catch (err) {
      return {
        ok: false,
        backend: this.backend,
        detail: err instanceof Error ? err.message : String(err),
      };
    }
  }
}

// Convenience type if your adapter parses raw vendor records into events.
// (Not all adapters need this — depends on the vendor's response shape.)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function _exampleRecordToEvent(record: Record<string, unknown>): TeamMemoryEvent | null {
  const meta = (record.metadata ?? {}) as Record<string, unknown>;
  const payloadRaw = typeof record.raw_payload === 'string' ? record.raw_payload : '';
  let payload: Record<string, unknown> = {};
  try {
    if (payloadRaw) payload = JSON.parse(payloadRaw) as Record<string, unknown>;
  } catch {
    payload = { raw: payloadRaw };
  }
  const eventType = typeof meta.event_type === 'string' ? meta.event_type : 'task_updated';
  return {
    eventId: typeof meta.event_id === 'string' ? meta.event_id : String(record.id ?? ''),
    teamId: typeof record.tenant_id === 'string' ? record.tenant_id : '',
    agentId: typeof record.user_id === 'string' ? record.user_id : '',
    workspaceId: typeof meta.workspace_id === 'string' ? meta.workspace_id : '',
    eventType: eventType as TeamMemoryEventType,
    timestamp:
      typeof meta.timestamp === 'string'
        ? meta.timestamp
        : (record.created_at as string) ?? new Date().toISOString(),
    payload,
    summary: typeof record.text === 'string' ? record.text : '',
    metadata: meta as Record<string, string | number | boolean | string[] | null>,
  };
}
