import * as net from 'net';
import { AsanagiDbError, Document, InsertResult, WireResponse } from './document.js';

/**
 * Client for the AsanagiDB Compository (document store).
 *
 * Connects to asanagidb via plain TCP on the configured host/port (default 127.0.0.1:8080).
 * Protocol: newline-delimited JSON. Node.js only (not browser-compatible).
 *
 * Documents returned by this client are pre-bound so you can call
 * {@link Document.save}, {@link Document.fetch}, {@link Document.update},
 * {@link Document.delete} on them without passing the client again.
 *
 * @example
 * const client = new AsanagiDbClient();
 * await client.connect();
 * await client.useDatabase('mydb');
 *
 * const id = BigInt(Date.now());
 * const doc = new Document('Article', '_id', id)
 *   .add('title', 'Hello World')
 *   .add('body',  'My first Compository document.');
 * await client.insertOne(doc);
 *
 * const found = await client.findById(id);
 * console.log(found?.getString('title')); // Hello World
 * await client.close();
 */
export class AsanagiDbClient {
  protected socket: net.Socket | null = null;
  protected pending: Array<(line: string) => void> = [];
  protected buffer = '';

  async connect(host = '127.0.0.1', port = 8080): Promise<void> {
    return new Promise((resolve, reject) => {
      const sock = net.createConnection({ host, port }, () => {
        this.socket = sock;
        resolve();
      });

      sock.setEncoding('utf8');
      sock.on('data', (chunk: string) => {
        this.buffer += chunk;
        let nl: number;
        while ((nl = this.buffer.indexOf('\n')) !== -1) {
          const line = this.buffer.slice(0, nl);
          this.buffer = this.buffer.slice(nl + 1);
          this.pending.shift()?.(line);
        }
      });

      sock.on('error', reject);
      sock.on('close', () => { this.socket = null; });
    });
  }

  async close(): Promise<void> {
    return new Promise((resolve) => {
      if (!this.socket) return resolve();
      this.socket.end(resolve);
    });
  }

  /** Switch the active database (tenant). */
  async useDatabase(name: string): Promise<void> {
    await this.send({ op: 'use', database: name });
  }

  /**
   * Store a Document. Uses {@link Document.clazz} as the vertex label and
   * {@link Document.id} as the vertex ID (when set).
   */
  async insertOne(doc: Document): Promise<InsertResult> {
    const id = doc.id;
    const req: Record<string, unknown> = { op: 'store', label: doc.clazz, entity: doc.toJSON() };
    if (id !== undefined) req['id'] = Number(id);
    const msg = await this.send(req);
    // Server returns the assigned vertex ID as a JSON number.
    if (typeof msg === 'number') {
      (doc as any)._id = BigInt(msg);
      (doc as any).syncInternalFields();
    }
    doc._client = this;
    return { insertedId: doc.id };
  }

  /** Deep-fetch a Document by its internal vertex ID. Returns null if not found. */
  async findById(id: bigint | number): Promise<Document | null> {
    const msg = await this.send({ op: 'retrieve', id: Number(id) });
    if (msg === null || typeof msg !== 'object' || Array.isArray(msg)) return null;
    const doc = Document.fromObject(msg as Record<string, unknown>);
    doc._client = this;
    return doc;
  }

  /** Deep-fetch a Document by an indexed property key/value (e.g. the _key field). Returns null if not found. */
  async findByKey(key: string, value: unknown): Promise<Document | null> {
    const wire = typeof value === 'bigint' ? Number(value) : value;
    const msg = await this.send({ op: 'retrieve', key, value: wire });
    if (msg === null || typeof msg !== 'object' || Array.isArray(msg)) return null;
    const doc = Document.fromObject(msg as Record<string, unknown>);
    doc._client = this;
    return doc;
  }

  /** Replace an existing Document in-place by ID. */
  async replaceOne(id: bigint | number, doc: Document): Promise<void> {
    const payload = { ...doc.toJSON(), [Document.name]: undefined, _id: Number(id) };
    await this.send({ op: 'store', entity: payload, id: Number(id) });
  }

  /** Delete a Document by its internal vertex ID. */
  async deleteOne(id: bigint | number): Promise<void> {
    await this.send({ op: 'delete', id: Number(id) });
  }

  /**
   * Return a shallow listing of all vertices. Each Document contains only
   * identifying fields (_id, _class, key field). Use {@link findById} for full content.
   */
  async listAll(): Promise<Document[]> {
    const msg = await this.send({ op: 'ls' });
    if (!Array.isArray(msg)) return [];
    return msg.map((item) => {
      const doc = Document.fromObject(item as Record<string, unknown>);
      doc._client = this;
      return doc;
    });
  }

  protected send(request: Record<string, unknown>): Promise<unknown> {
    return new Promise((resolve, reject) => {
      if (!this.socket) {
        return reject(new Error('Not connected — call connect() first'));
      }

      this.pending.push((line: string) => {
        try {
          const resp = JSON.parse(line) as WireResponse;
          if (resp.status !== 'ok') {
            reject(new AsanagiDbError(typeof resp.message === 'string' ? resp.message : 'Unknown error'));
          } else {
            resolve(resp.message ?? null);
          }
        } catch (e) {
          reject(e);
        }
      });

      this.socket.write(JSON.stringify(request) + '\n', 'utf8', (err) => {
        if (err) {
          this.pending.pop();
          reject(err);
        }
      });
    });
  }
}
