import * as monaco from 'monaco-editor';
import APIClient from './APIClient';

class APIScenarioExecutor {
  constructor(graph, apiTester, outputEditor) {
    this.graph = graph; // The graph data
    this.apiClient = new APIClient();
    this.apiTester = apiTester;
    this.outputEditor = outputEditor;
    this.reset();
  }

  // Helper method to find the start node ID
  findStartNodeId() {
    for (let nodeId in this.graph) {
      if (this.graph[nodeId].class === 'start') {
        return nodeId;
      }
    }
    return null; // Return null if no start node is found
  }

  reset() {
    this.currentNodeId = this.findStartNodeId(); // Initialize at the 'start' node
    this.visitedNodes = new Set(); // Keep track of visited nodes
    this.isRunning = false;
    this.isDone = false;

    let allNodes = document.querySelector('#drawflow').querySelectorAll('.drawflow-node');
    allNodes.forEach(node => {
      node.classList.remove('current-execution');
      node.classList.remove('result-success');
      node.classList.remove('result-error');
      node.classList.remove('result-abort');
    });
  }

  executeCurrentNode() {
    return new Promise((resolve, reject) => {
      console.log(this.currentNodeId);

      let allNodes = document.querySelector('#drawflow').querySelectorAll('.drawflow-node');
      allNodes.forEach(node => {
        node.classList.remove('current-execution');
      });

      if (!this.currentNodeId) {
        console.log('No current node to execute.');
        return;
      }

      const currentNode = this.graph[this.currentNodeId];

      if (currentNode) {
        this.isRunning = true;
        this.visitedNodes.add(this.currentNodeId); // Mark this node as visited

        let nodeElement = document.querySelector('#drawflow').querySelector(`.drawflow-node[id="node-${this.currentNodeId}"]`);
        if (nodeElement) {
          nodeElement.classList.add('current-execution');
        } else {
          console.error('Node with ID ' + this.currentNodeId + ' not found');
        }

        if (currentNode.class === 'api') {
          let model = JSON.parse(JSON.stringify(this.apiTester.getApiRequestModel(currentNode.data.id)));
          model.responseModel = {};
          this.appendLog(`===============================================================\nExecuting API [${model.name}]`);
          let request = null;
          if (model.type === 'TCP') {
            request = this.logTcpRequest;
          }else {
            request = this.logHttpRequest;
          }

          this.apiClient.sendAPIRequest(model, request.bind(this)).then(response => {
            this.isRunning = false;
            if (model.type === 'TCP') {
              this.logTcpResponse(response);
              nodeElement.classList.add('result-success');
            } else {
              this.logHttpResponse(response);
              if (response.status === 200 || response.status === 201) {
                nodeElement.classList.add('result-success');
              } else {
                nodeElement.classList.add('result-error');
              }
            }

            resolve();
          }).catch(error => {
            nodeElement.classList.add('result-abort');
            $('.toast-body').text(error.message);
            $('.toast').toast('show');
            this.appendLog(error.message);
            reject(error);
          });
          console.log(`===============================================================\nExecuting API request for node: ${currentNode.id} - ${currentNode.name}`);
        } else if (currentNode.class === 'start') {
          this.appendLog('Starting API scenario\n');
          nodeElement.classList.add('result-success');
          resolve();
        } else {
          console.log(`Executing script for node: ${currentNode.id} - ${currentNode.name}`);
          resolve();
        }
      }
    });

  }

  appendLog(text) {
    var range = new monaco.Range(this.outputEditor.getModel().getLineCount(), 1, this.outputEditor.getModel().getLineCount(), 1);
    var id = {major: 1, minor: 1};
    var op = {identifier: id, range: range, text: text + '\n\n', forceMoveMarkers: true};
    this.outputEditor.executeEdits('my-source', [op]);
  }

  logHttpRequest(model) {
    console.log(model);
    let log = `[HTTP API Request]\n${model.method} ${model.path} HTTP/1.1\n[Request Headers]\n${this.parseRequestHeaders(model.headers)}\n[Request Body]\n${model.requestBody}`;
    this.appendLog(log);
  }

  logTcpRequest(model) {
    console.log(model);
    let log = `[TCP API Request]\n[Request Body]\n${model.requestBody}`;
    this.appendLog(log);
  }

  logHttpResponse(response) {
    let log = `[Response]\nHTTP/1.1 ${response.status}\n[Response Headers]\n${this.parseHeaders(response.headers)}\n[Response Body]\n${response.body}\n`;
    this.appendLog(log);
  }

  logTcpResponse(response) {
    let log = `[Response]\n${response.body}\n`;
    this.appendLog(log);
  }

  parseRequestHeaders(headers) {
    let parsedHeaders = '';
    headers.forEach(header => {
      if (header.enabled) {
        parsedHeaders += `${header.key}: ${header.value}\n`;
      }
    });

    return parsedHeaders;
  }

  parseHeaders(headers) {
    //convert object to string with \n separation
    //sort header by key

    let parsedHeaders = '';
    const sortedKeys = Object.keys(headers).sort(); // Sorting the keys alphabetically
    sortedKeys.forEach(key => {
      parsedHeaders += `${key}: ${headers[key]}\n`;
    });
    return parsedHeaders;
  }

  // Method to move to the next node
  moveToNextNode() {
    const currentNode = this.graph[this.currentNodeId];
    console.log(currentNode);

    if (currentNode && currentNode.connections.length > 0) {
      // Find the next unvisited node
      const nextNode = currentNode.connections.find(nodeId => !this.visitedNodes.has(nodeId));
      if (nextNode) {
        this.currentNodeId = nextNode;
      } else {
        console.log('No more unvisited connections to move to.');
        this.currentNodeId = null; // No more nodes to visit
      }
    } else {
      console.log('Current node has no connections or does not exist.');
      this.currentNodeId = null; // No more nodes to visit
      this.isDone = true;
    }
  }

  async executeAll() {
    while (this.currentNodeId) {
      console.log(this.currentNodeId);
      try {
        await this.executeCurrentNode();
      } catch (error) {
        console.error('Error executing node: ', error);
        // Handle error or break loop if necessary
      }
      await this.moveToNextNodeWithDelay(250);
    }
    console.log('Completed execution of all API nodes.');
  }

  moveToNextNodeWithDelay(delay) {
    return new Promise(resolve => {
      setTimeout(() => {
        this.moveToNextNode();
        resolve();
      }, delay);
    });
  }

  static extractGraphFromStart(graphData) {
    console.log(graphData);
    let graph = {};

    // Helper function to recursively build the graph
    function buildGraph(nodeId) {
      const node = graphData[nodeId];
      if (!node || graph[nodeId]) {
        // If the node doesn't exist or has already been visited, stop the recursion
        return;
      }

      // Initialize the node in the graph
      graph[nodeId] = {
        id: node.id,
        name: node.name,
        class: node.class,
        data: node.data,
        connections: []
      };

      // If the node has outputs, recursively add connected nodes
      if (node.outputs) {
        Object.values(node.outputs).forEach(output => {
          output.connections.forEach(connection => {
            graph[nodeId].connections.push(connection.node);
            buildGraph(connection.node);  // Recurse
          });
        });
      }
    }

    // Find the start node
    for (let nodeId in graphData) {
      if (graphData[nodeId].class === 'start') {
        buildGraph(nodeId);  // Start building the graph from the 'start' node
        break;  // Assuming there's only one start node
      }
    }

    return graph;
  }
}

export default APIScenarioExecutor;
