import type { Insight, InsightSearchResult, MemoryAdapter } from './types.js';

const STARTUP_CONTEXT_CANDIDATE_LIMIT = 64;

/** Immutable server-derived boundary; callers never choose an adapter namespace. */
export interface OperatorMemoryScope {
  readonly tenantId: string;
  readonly ownerId: string;
  readonly sessionId: string;
}

export interface OperatorMemoryConfig {
  /** Adapter injection is deployment/lifecycle configuration, never caller input. */
  readonly adapter: MemoryAdapter;
  /** Configured agent identity; it is metadata rather than a storage key default. */
  readonly instanceId: string;
  /** Configured storage partition; callers cannot select a namespace. */
  readonly namespace: string;
  readonly maxStartupContext?: number;
  redact(content: string): string;
}

export interface OperatorMemoryResult extends InsightSearchResult {
  provenance: { instanceId: string; namespace: string; source: string };
}

export interface OperatorMemoryPlugin {
  capture(
    scope: OperatorMemoryScope,
    input: { content: string; source: string; category: string },
  ): Promise<Insight>;
  search(
    scope: OperatorMemoryScope,
    query: string,
    limit?: number,
  ): Promise<OperatorMemoryResult[]>;
  recent(scope: OperatorMemoryScope, limit?: number): Promise<OperatorMemoryResult[]>;
  stats(scope: OperatorMemoryScope): Promise<{ namespace: string; resultCount: number }>;
  startupContext(scope: OperatorMemoryScope): Promise<OperatorMemoryResult[]>;
}

function scopedUserId(scope: OperatorMemoryScope, namespace: string): string {
  const normalizedScope = normalizeScope(scope);
  // JSON tuple encoding avoids delimiter collisions between independently scoped IDs.
  return JSON.stringify([
    namespace,
    normalizedScope.tenantId,
    normalizedScope.ownerId,
    normalizedScope.sessionId,
  ]);
}

function normalizeScope(scope: OperatorMemoryScope): OperatorMemoryScope {
  if (typeof scope !== 'object' || scope === null) {
    throw new Error('Operator memory scope is required');
  }
  return Object.freeze({
    tenantId: requiredScopeId(scope.tenantId, 'tenant ID'),
    ownerId: requiredScopeId(scope.ownerId, 'owner ID'),
    sessionId: requiredScopeId(scope.sessionId, 'session ID'),
  });
}

function requiredScopeId(value: unknown, field: string): string {
  if (typeof value !== 'string' || value.trim().length === 0) {
    throw new Error(`Operator memory ${field} is required`);
  }
  return value.trim();
}

function compareStartupContext(left: OperatorMemoryResult, right: OperatorMemoryResult): number {
  return (
    startupSourcePriority(left.provenance.source) - startupSourcePriority(right.provenance.source)
  );
}

function startupSourcePriority(source: string): number {
  if (source === 'project') return 0;
  if (source === 'flat-file') return 1;
  return 2;
}

function normalizeConfig(config: OperatorMemoryConfig): OperatorMemoryConfig {
  const instanceId = config.instanceId.trim();
  const namespace = config.namespace.trim();
  const maxStartupContext = config.maxStartupContext ?? 8;
  if (instanceId.length === 0 || namespace.length === 0) {
    throw new Error('Operator memory instance ID and namespace must be configured');
  }
  if (!Number.isSafeInteger(maxStartupContext) || maxStartupContext < 1) {
    throw new Error('Operator memory startup context limit must be a positive integer');
  }
  return Object.freeze({ ...config, instanceId, namespace, maxStartupContext });
}

/** Creates a leaf-package, replaceable memory adapter facade. */
export function createOperatorMemoryPlugin(config: OperatorMemoryConfig): OperatorMemoryPlugin {
  const pluginConfig = normalizeConfig(config);
  const mapResult = (result: InsightSearchResult): OperatorMemoryResult => ({
    ...result,
    provenance: {
      instanceId: pluginConfig.instanceId,
      namespace: pluginConfig.namespace,
      source: String(result.metadata?.['source'] ?? 'retrieval'),
    },
  });
  const search = async (
    scope: OperatorMemoryScope,
    query: string,
    limit = 10,
  ): Promise<OperatorMemoryResult[]> =>
    (
      await pluginConfig.adapter.searchInsights(
        scopedUserId(scope, pluginConfig.namespace),
        query,
        {
          limit,
        },
      )
    ).map(mapResult);
  return {
    async capture(scope, input) {
      return pluginConfig.adapter.storeInsight({
        userId: scopedUserId(scope, pluginConfig.namespace),
        content: pluginConfig.redact(input.content),
        source: input.source,
        category: input.category,
        relevanceScore: 1,
        metadata: {
          namespace: pluginConfig.namespace,
          instanceId: pluginConfig.instanceId,
          source: input.source,
        },
      });
    },
    search,
    async recent(scope, limit = 10) {
      return search(scope, '*', limit);
    },
    async stats(scope) {
      return {
        namespace: pluginConfig.namespace,
        resultCount: (await search(scope, '*', 100)).length,
      };
    },
    async startupContext(scope) {
      const maxStartupContext = pluginConfig.maxStartupContext ?? 8;
      // Prioritize authoritative sources within a bounded candidate window.
      const candidateLimit = Math.max(maxStartupContext, STARTUP_CONTEXT_CANDIDATE_LIMIT);
      const context = await search(scope, '*', candidateLimit);
      return [...context].sort(compareStartupContext).slice(0, maxStartupContext);
    },
  };
}
