//load local dependencies
const EventEmitter = require('events');
const StateManager = require('./stateManager');
const MovementManager = require('./movementManager');

//load all config modules
const defaultConfig = require('./stateConfig.json');
const ConfigUtils = require('../helper/configUtils');

class state{
  constructor(config = {}, logger) {

    this.emitter = new EventEmitter();  // Own EventEmitter
    this.configUtils = new ConfigUtils(defaultConfig);
    this.config = this.configUtils.initConfig(config);
    this.abortController = null;  // new abort controller for aborting async tasks
    // Init after config is set
    this.logger = logger;

    // Initialize StateManager for state handling
    this.stateManager = new StateManager(this.config,this.logger);
    this.movementManager = new MovementManager(this.config, this.logger, this.emitter);

    this.delayedMove = null;
    this.mode = this.config.mode.current;
    // Monotonic counter incremented on every EXTERNAL abort (i.e. one
    // initiated outside the in-flight sequence — typically MGC reacting
    // to a new demand). executeSequence captures the value at entry and
    // breaks its for-loop if the counter advances mid-sequence, so a
    // shutdown that was already past its ramp-down step doesn't barge
    // through stopping → coolingdown when a re-engage arrives.
    this.sequenceAbortToken = 0;

    // Log initialization
    this.logger.info("State class initialized.");

  }

  // -------- Delegate State Management -------- //

  getMoveTimeLeft() {
    return this.movementManager.timeleft;
  }
  
  getCurrentState() {
    return this.stateManager.currentState;
  }

  getStateDescription() {
    return this.stateManager.getStateDescription();
  }

  // -------- Movement Methods -------- //
  getCurrentPosition() {
    return this.movementManager.getCurrentPosition();
  }

  getRunTimeHours() {
    return this.stateManager.getRunTimeHours();
  }

  getMaintenanceTimeHours(){
    return this.stateManager.getMaintenanceTimeHours();
  }

 
  async moveTo(targetPosition) {

    // Check for invalid conditions and throw errors
    if (targetPosition === this.getCurrentPosition()) {
      this.logger.warn(`Target position=${targetPosition} is the same as the current position ${this.getCurrentPosition()}. Not executing move.`);
      return;
    }

    if (this.stateManager.getCurrentState() !== "operational") {
      // 'accelerating' / 'decelerating' here is post-abort residue —
      // the previous moveTo was aborted (e.g. MGC's per-tick
      // abortActiveMovements) and the catch block intentionally
      // doesn't auto-return to operational (avoids a bounce loop).
      // BUT a new setpoint just arrived, so there's nothing for the
      // anti-bounce policy to protect: the caller IS asking for a
      // move. Fall through to operational and execute it. Without
      // this the FSM gets parked, all subsequent setpoints land in
      // delayedMove which never fires, and currentPosition freezes —
      // see test/integration/abort-deadlock.integration.test.js for
      // the exact deadlock scenario.
      const movementResidueStates = ['accelerating', 'decelerating'];
      if (movementResidueStates.includes(this.stateManager.getCurrentState())) {
        this.logger.debug(`moveTo(${targetPosition}) arrived while parked in '${this.stateManager.getCurrentState()}' (post-abort). Returning to operational to service the new setpoint.`);
        try {
          await this.transitionToState("operational");
        } catch (e) {
          this.logger.warn(`Could not transition out of '${this.stateManager.getCurrentState()}': ${e?.message || e}`);
          return;
        }
        // Fall through — state is now operational, proceed with new move.
      } else {
        // Genuine non-operational state (starting, warmingup, stopping,
        // coolingdown, idle, off, emergencystop, maintenance) — these
        // are sequence steps the caller can't legitimately interrupt
        // with a setpoint. Save for later, exactly as before.
        if (this.config.mode.current === "auto") {
          this.delayedMove = targetPosition;
          this.logger.warn(`Saving setpoint=${targetPosition} to execute once back in 'operational' state.`);
        }
        else{
          this.logger.warn(`Not able to accept setpoint=${targetPosition} while not in ${this.stateManager.getCurrentState()} state`);
        }
        return;
      }
    }
    this.abortController = new AbortController();
    const { signal } = this.abortController;
    try {
      const newState = targetPosition < this.getCurrentPosition() ? "decelerating" : "accelerating";
      await this.transitionToState(newState,signal);  // awaits transition
      await this.movementManager.moveTo(targetPosition,signal);  // awaits moving
      this.emitter.emit("movementComplete", { position: targetPosition });
      await this.transitionToState("operational");
    } catch (error) {
      // Abort path: only return to 'operational' when explicitly requested
      // (shutdown/emergency-stop needs it to unblock the FSM). Routine MGC
      // demand-update aborts must NOT auto-transition — doing so causes a
      // bounce loop where every tick aborts → operational → new move →
      // abort → operational → ... and the pump never reaches its setpoint.
      const msg = typeof error === 'string' ? error : error?.message;
      if (msg === 'Transition aborted' || msg === 'Movement aborted') {
        if (this._returnToOperationalOnAbort) {
          this.logger.debug(`Movement aborted; returning to 'operational' (requested by caller).`);
          try {
            await this.transitionToState("operational");
          } catch (e) {
            this.logger.debug(`Post-abort transition to operational failed: ${e?.message || e}`);
          }
        } else {
          this.logger.debug(`Movement aborted; staying in current state (routine abort).`);
        }
        this._returnToOperationalOnAbort = false;
        this.emitter.emit("movementAborted", { position: targetPosition });
      } else {
        this.logger.error(error);
      }
    }
  }
  
  // -------- State Transition Methods -------- //

  /**
   * @param {string} reason - human-readable abort reason
   * @param {object} [options]
   * @param {boolean} [options.returnToOperational=false] - when true the FSM
   *   transitions back to 'operational' after the abort so a subsequent
   *   shutdown/emergency-stop sequence can proceed. Set to false (default)
   *   for routine demand updates where the caller will send a new movement
   *   immediately — auto-transitioning would cause a bounce loop.
   */
  abortCurrentMovement(reason = "group override", options = {}) {
    if (this.abortController && !this.abortController.signal.aborted) {
      this.logger.warn(`Aborting movement: ${reason}`);
      this._returnToOperationalOnAbort = Boolean(options.returnToOperational);
      // Only external aborts (returnToOperational=false) advance the
      // sequence-abort token. Sequence-internal aborts (e.g. shutdown's
      // own setpoint(0) being pre-empted by a fresher shutdown/estop)
      // come from inside executeSequence and must not terminate their
      // own loop.
      if (!options.returnToOperational) {
        this.sequenceAbortToken += 1;
      }
      this.abortController.abort();
    }
  }

  async transitionToState(targetState, signal) {

    const fromState = this.getCurrentState();
    const position = this.getCurrentPosition();

    // Define states that cannot be aborted for safety reasons
    const protectedStates = ['warmingup', 'coolingdown'];
    const isProtectedTransition = protectedStates.includes(fromState);

    try {
      
      this.logger.debug(`Starting transition from ${fromState} to ${targetState}.`);
      if( isProtectedTransition){
        //overrule signal to prevent abortion
        signal = null; // Disable abortion for protected states
        //spit warning
        this.logger.warn(`Transition from ${fromState} to ${targetState} is protected and cannot be aborted.`);
      }

      // Await the state transition and pass signal for abortion
      const feedback = await this.stateManager.transitionTo(targetState,signal);
      this.logger.info(`Statemanager: ${feedback}`);
      
      /* -- Auto pick setpoints in auto mode when operational--*/
      if (
          targetState === "operational" &&
          this.config.mode.current === "auto" &&
          this.delayedMove !== position &&
          this.delayedMove
      ) {
          this.logger.info(`Automatically picking up on last requested setpoint ${this.delayedMove}`);
          //trigger move 
          await this.moveTo(this.delayedMove,signal);
          this.delayedMove = null;
          this.logger.info(`moveTo : ${feedback} `);
      }

      this.logger.info(`State change to ${targetState} completed.`);
      this.emitter.emit('stateChange', targetState); // <-- Implement Here
    } catch (error) {
        if (
        error.message === "Transition aborted" ||
        error.message === "Movement aborted"
      ) {
        throw error;
      }
        this.logger.error(error);
    }
  }

}

module.exports = state;

