/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */

import { RemoteStrategy } from '../driver/remote-connection.js';
import DriverRemoteConnection from '../driver/driver-remote-connection.js';
import Client from '../driver/client.js';
import GremlinLang from './gremlin-lang.js';
import { GraphTraversalSource } from './graph-traversal.js';
import { TraversalStrategies } from './traversal-strategy.js';

/**
 * Controls an explicit remote transaction. A thin wrapper around a Client that
 * adds transaction lifecycle (begin/commit/rollback/close) and attaches a
 * transactionId to every request.
 *
 * Created via Client.transact() or g.tx(). The traversal source (g alias) is
 * inherited from the Client and cannot be changed.
 *
 * Transactions are short-lived and single-use. After commit or rollback, the
 * transaction ID is invalid and the object cannot be reused.
 *
 * Each traversal on the transaction-bound source must be awaited before
 * submitting the next, to ensure the server receives requests in order.
 *
 * This class is NOT thread-safe. Do not share a Transaction across concurrent
 * async contexts without external synchronization.
 */
export class Transaction {
  private _transactionId?: string;
  private _isOpen = false;
  private _failed = false;
  private readonly _client: Client;

  /**
   * Creates a Transaction. Accepts a Client which provides the connection
   * and traversal source for all transaction requests.
   */
  constructor(client: Client) {
    if (!client) {
      throw new Error('Client is required for transactions');
    }
    this._client = client;
  }

  /**
   * Spawns a GraphTraversalSource that is bound to a remote transaction.
   *
   * begin() is idempotent: calling it while a transaction is already open does not send a
   * second begin to the server and does not throw - it reuses the existing transaction ID and
   * returns a source bound to the same transaction. A transaction is single-use, so calling
   * begin() after it has been closed (commit/rollback/failed begin) throws.
   * @returns {Promise<GraphTraversalSource>}
   */
  async begin(): Promise<GraphTraversalSource> {
    if (this._failed) {
      throw new Error('Transaction is closed and cannot be reused; begin a new transaction');
    }

    // idempotent: if a transaction is already open, reuse the existing transactionId without
    // sending a second begin to the server, and return a source bound to the same transaction
    if (!this._isOpen) {
      let result;
      try {
        result = await this._client.submit('g.tx().begin()', null);
      } catch (e) {
        this._failed = true;
        throw e;
      }

      const resultArray = result.toArray();
      if (!resultArray || resultArray.length === 0) {
        this._failed = true;
        throw new Error('Server did not return transaction ID');
      }

      const resultMap = resultArray[0];
      if (!resultMap || !(resultMap instanceof Map) || !resultMap.get('transactionId')) {
        this._failed = true;
        throw new Error('Server did not return transaction ID in expected format');
      }

      this._transactionId = resultMap.get('transactionId');
      this._isOpen = true;
      this._client.trackTransaction(this);
    }

    // Create a DriverRemoteConnection bound to this transaction. The DRC
    // will automatically attach the transactionId to all requests.
    const txConnection = new DriverRemoteConnection(
      this._client.url,
      this._client.options,
      this._transactionId,
    );

    const strategies = new TraversalStrategies();
    strategies.addStrategy(new RemoteStrategy(txConnection));
    const gtx = new GraphTraversalSource(null as any, strategies, new GremlinLang());
    gtx._boundTransaction = this;
    return gtx;
  }

  /**
   * Commits the transaction.
   * @returns {Promise<void>}
   */
  async commit(): Promise<void> {
    await this.#closeTransaction('g.tx().commit()');
  }

  /**
   * Rolls back the transaction.
   * @returns {Promise<void>}
   */
  async rollback(): Promise<void> {
    await this.#closeTransaction('g.tx().rollback()');
  }

  /**
   * Returns true if transaction is open.
   */
  get isOpen(): boolean {
    return this._isOpen;
  }

  /**
   * Returns the server-generated transaction ID, or undefined if not yet begun.
   */
  get transactionId(): string | undefined {
    return this._transactionId;
  }

  /**
   * Closes the transaction. Default behavior is rollback: partial work is
   * discarded rather than accidentally persisted.
   * @returns {Promise<void>}
   */
  async close(): Promise<void> {
    if (this._isOpen) {
      await this.rollback();
    }
  }

  /**
   * Submits a gremlin-lang string within this transaction. The transactionId
   * is automatically attached. Has the same signature as Client.submit().
   */
  async submit(gremlin: string, parameters?: any, requestOptions?: any): Promise<any> {
    if (!this._isOpen) {
      throw new Error('Transaction is not open');
    }
    const opts = {
      ...requestOptions,
      transactionId: this._transactionId,
    };
    return this._client.submit(gremlin, parameters || null, opts);
  }

  async #closeTransaction(script: string): Promise<void> {
    if (!this._isOpen) {
      throw new Error('Transaction is not open');
    }

    await this._client.submit(script, null, {
      transactionId: this._transactionId
    });

    this._isOpen = false;
    this._failed = true; // Terminal state: transaction cannot be reused
    this._client.untrackTransaction(this);
  }
}
