/**
 * In-memory datastore provider — for testing and local dev.
 *
 * All data is stored in Maps. No persistence. Fast and deterministic.
 */

import { matchesFilter } from '../filter.js';
import type {
  AggregateQuery,
  BaseDocument,
  CollectionQuery,
  ConcurrencyToken,
  DatastoreProvider,
  DocumentCollection,
  FilterMap,
  UpdateIfMatchResult,
} from '../types.js';

function deepClone<T>(obj: T): T {
  return JSON.parse(JSON.stringify(obj)) as T;
}

export class MemoryDatastoreProvider implements DatastoreProvider {
  private collections = new Map<string, MemoryCollection<BaseDocument>>();

  getCollection<T extends BaseDocument = BaseDocument>(
    name: string,
    _partitionKeyPath: string
  ): DocumentCollection<T> {
    let collection = this.collections.get(name);
    if (!collection) {
      collection = new MemoryCollection<BaseDocument>();
      this.collections.set(name, collection);
    }
    return collection as unknown as DocumentCollection<T>;
  }

  async isHealthy(): Promise<boolean> {
    return true;
  }

  /** Clear all collections (for test cleanup). */
  clear(): void {
    this.collections.clear();
  }
}

class MemoryCollection<T extends BaseDocument> implements DocumentCollection<T> {
  private docs = new Map<string, T>();

  async findById(id: string, _partitionKey: string): Promise<T | null> {
    return this.docs.get(id) ?? null;
  }

  async findMany(query?: CollectionQuery<T>): Promise<T[]> {
    let results = [...this.docs.values()];

    if (query?.filter) {
      results = results.filter(doc => matchesFilter(doc as Record<string, unknown>, query.filter!));
    }

    if (query?.sort) {
      const sortEntries = Object.entries(query.sort) as Array<[string, 1 | -1]>;
      results.sort((a, b) => {
        for (const [field, dir] of sortEntries) {
          const aVal = (a as Record<string, unknown>)[field];
          const bVal = (b as Record<string, unknown>)[field];
          const cmp = compareValues(aVal, bVal);
          if (cmp !== 0) return cmp * dir;
        }
        return 0;
      });
    }

    if (query?.offset) {
      results = results.slice(query.offset);
    }

    if (query?.limit) {
      results = results.slice(0, query.limit);
    }

    if (query?.select && query.select.length > 0) {
      results = results.map(doc => {
        const picked: Record<string, unknown> = {};
        for (const key of query.select!) {
          picked[key as string] = (doc as Record<string, unknown>)[key as string];
        }
        return picked as T;
      });
    }

    return results;
  }

  async findOne(query?: CollectionQuery<T>): Promise<T | null> {
    const results = await this.findMany({ ...query, limit: 1 });
    return results[0] ?? null;
  }

  async count(filter?: FilterMap): Promise<number> {
    if (!filter) return this.docs.size;
    const results = [...this.docs.values()].filter(doc =>
      matchesFilter(doc as Record<string, unknown>, filter)
    );
    return results.length;
  }

  async create(doc: T): Promise<T> {
    if (this.docs.has(doc.id)) {
      throw new Error(`Document with id '${doc.id}' already exists`);
    }
    const clone = deepClone(doc);
    this.docs.set(doc.id, clone);
    return clone;
  }

  async update(id: string, _partitionKey: string, updates: Partial<T>): Promise<T> {
    const existing = this.docs.get(id);
    if (!existing) {
      throw new Error(`Document with id '${id}' not found`);
    }
    const merged = { ...existing, ...updates } as T;
    this.docs.set(id, deepClone(merged));
    return deepClone(merged);
  }

  /**
   * Atomic compare-and-set. The entire get → compare → set runs SYNCHRONOUSLY in
   * one block with NO await/yield, so under the single-threaded event loop two
   * concurrent callers can never interleave between the compare and the set — the
   * first wins (bumps `rev` + `_etag`), the rest see a mismatch and get `conflict`.
   */
  async updateIfMatch(
    id: string,
    _partitionKey: string,
    expected: ConcurrencyToken,
    patch: Partial<T>
  ): Promise<UpdateIfMatchResult<T>> {
    const existing = this.docs.get(id);
    if (!existing) return { ok: false, reason: 'not_found' };
    const rec = existing as BaseDocument & { rev?: number };
    const curRev = typeof rec.rev === 'number' ? rec.rev : 0;
    if (expected.rev !== undefined && expected.rev !== curRev) {
      return { ok: false, reason: 'conflict' };
    }
    if (expected.etag !== undefined && expected.etag !== rec._etag) {
      return { ok: false, reason: 'conflict' };
    }
    const nextRev = curRev + 1;
    const merged = { ...existing, ...patch, rev: nextRev, _etag: `m${nextRev}` } as T;
    this.docs.set(id, deepClone(merged));
    return { ok: true, doc: deepClone(merged) };
  }

  async upsert(doc: T): Promise<T> {
    const clone = deepClone(doc);
    this.docs.set(doc.id, clone);
    return clone;
  }

  async delete(id: string, _partitionKey: string): Promise<void> {
    this.docs.delete(id);
  }

  async aggregate<R = Record<string, unknown>>(query: AggregateQuery): Promise<R[]> {
    let docs = [...this.docs.values()];
    if (query.filter) {
      docs = docs.filter(doc => matchesFilter(doc as Record<string, unknown>, query.filter!));
    }

    // Group by field
    const groups = new Map<string, T[]>();
    for (const doc of docs) {
      const key = String((doc as Record<string, unknown>)[query.groupBy] ?? '__null__');
      const group = groups.get(key) ?? [];
      group.push(doc);
      groups.set(key, group);
    }

    const results: Record<string, unknown>[] = [];
    for (const [groupKey, groupDocs] of groups) {
      const row: Record<string, unknown> = { [query.groupBy]: groupKey };
      for (const agg of query.aggregations) {
        row[agg.alias] = computeAggregation(groupDocs, agg.field, agg.op);
      }
      results.push(row);
    }

    return results as R[];
  }

  async rawQuery<R = unknown>(_query: string, _parameters?: Record<string, unknown>): Promise<R[]> {
    throw new Error(
      'rawQuery is not supported by MemoryDatastoreProvider. Use findMany/aggregate instead.'
    );
  }
}

function compareValues(a: unknown, b: unknown): number {
  if (a === b) return 0;
  if (a == null) return -1;
  if (b == null) return 1;
  if (typeof a === 'number' && typeof b === 'number') return a - b;
  if (typeof a === 'string' && typeof b === 'string') return a.localeCompare(b);
  return String(a).localeCompare(String(b));
}

function computeAggregation<T extends BaseDocument>(
  docs: T[],
  field: string,
  op: 'count' | 'sum' | 'avg' | 'min' | 'max'
): number {
  if (op === 'count') return docs.length;

  const values = docs
    .map(d => (d as Record<string, unknown>)[field])
    .filter((v): v is number => typeof v === 'number');

  if (values.length === 0) return 0;

  switch (op) {
    case 'sum':
      return values.reduce((a, b) => a + b, 0);
    case 'avg':
      return values.reduce((a, b) => a + b, 0) / values.length;
    case 'min':
      return Math.min(...values);
    case 'max':
      return Math.max(...values);
  }
}
