// Ordering for the priority queue (max-heap as min-delay-first min-heap).
// A timer with smaller (delay, insertion_order) compares as "greater" so that
// @priority_queue.pop() always extracts the soonest-due timer first.
// Eq uses timer ID as the unique identity.

///|
pub impl Eq for TimerTask with fn equal(self : TimerTask, other : TimerTask) -> Bool {
  self.id == other.id
}

///|
pub impl Compare for TimerTask with fn compare(
  self : TimerTask,
  other : TimerTask,
) -> Int {
  let d = self.delay.compare(other.delay)
  if d != 0 {
    -d
  } else {
    -self.insertion_order.compare(other.insertion_order)
  }
}

///|
/// Enqueue a microtask to be executed when the current execution context completes.
/// This implements HostEnqueuePromiseJob from the ECMAScript spec.
pub fn Interpreter::enqueue_microtask(
  self : Interpreter,
  callback : Value,
  args : Array[Value],
) -> Unit {
  microtask_policy_enqueue(self.host.microtask_queue, { callback, args })
}

///|
/// A microtask-run failure paired with trustworthy callback provenance.
pub struct MicrotaskRunFailure {
  cause_ : Error
  source_identity_ : String?
}

///|
fn MicrotaskRunFailure::MicrotaskRunFailure(
  cause : Error,
  source_identity : String?,
) -> MicrotaskRunFailure {
  { cause_: cause, source_identity_: source_identity }
}

///|
pub fn MicrotaskRunFailure::cause(self : MicrotaskRunFailure) -> Error {
  self.cause_
}

///|
pub fn MicrotaskRunFailure::source_identity(
  self : MicrotaskRunFailure,
) -> String? {
  self.source_identity_
}

///|
/// Run all pending microtasks while retaining callback provenance on failure.
pub fn Interpreter::run_microtasks_observed(
  self : Interpreter,
) -> Result[Unit, MicrotaskRunFailure] raise Error {
  with_cleared_active_callee_realm(self.realm_state, fn() raise {
    let policy = MicrotaskQueuePolicy(self.host.microtask_queue)
    while true {
      match policy.readiness() {
        MicrotaskQueueEmpty => break
        MicrotaskObservationRequired => ()
      }
      self.observe_execution_step() catch {
        error => {
          policy.reject_dispatch_observation()
          raise error
        }
      }
      let task = match policy.select_next() {
        Some(task) => task
        None => fail("microtask queue readiness changed before selection")
      }
      // Call the callback with queued arguments. Applicable activation
      // observation belongs to call_value; queue dispatch must not charge it a
      // second time.
      match task.callback {
        Object(obj_data) =>
          match obj_data.callable {
            Some(_) => {
              let loc = @token.Loc::default()
              let call_result = observe_source_failure(self.realm_state, fn() raise {
                self.call_value(task.callback, Undefined, task.args, loc)
              })
              match call_result {
                Ok(_) => ()
                Err(failure) => {
                  policy.fail_selected()
                  return Err(
                    MicrotaskRunFailure(
                      failure.cause(),
                      failure.source_identity(),
                    ),
                  )
                }
              }
            }
            None => ()
          }
        _ => ()
      }
      policy.complete_selected()
    }
    policy.complete_drain()
    Ok(())
  }) catch {
    e => raise e
  }
}

///|
/// Run all pending microtasks until the queue is empty.
/// This is called after each task (script execution) completes.
/// Implements the microtask checkpoint from WHATWG Event Loop spec.
pub fn Interpreter::run_microtasks(self : Interpreter) -> Unit raise Error {
  match self.run_microtasks_observed() {
    Ok(_) => ()
    Err(failure) => raise failure.cause()
  }
}

///|
/// ES object-like values that participate in thenable assimilation.
pub fn is_thenable_object_candidate(value : Value) -> Bool {
  match value {
    Object(_) | Array(_) | Map(_) | Set(_) | Promise(_) | Proxy(_) => true
    _ => false
  }
}

///|
/// Fulfill a promise with the given value.
/// Triggers all fulfill reactions as microtasks.
pub fn fulfill_promise(
  interp : Interpreter,
  promise_data : PromiseData,
  value : Value,
) -> Unit {
  // Can only transition from Pending
  guard promise_data.state is Pending else { return }
  promise_data.state = Fulfilled
  promise_data.result = value

  // Trigger all fulfill reactions
  for reaction in promise_data.fulfill_reactions {
    enqueue_promise_reaction_job(interp, reaction, value)
  }

  // Clear reaction queues
  promise_data.fulfill_reactions.clear()
  promise_data.reject_reactions.clear()
}

///|
/// Reject a promise with the given reason.
/// Triggers all reject reactions as microtasks.
pub fn reject_promise(
  interp : Interpreter,
  promise_data : PromiseData,
  reason : Value,
) -> Unit {
  // Can only transition from Pending
  guard promise_data.state is Pending else { return }
  promise_data.state = Rejected
  promise_data.result = reason

  // Trigger all reject reactions
  for reaction in promise_data.reject_reactions {
    enqueue_promise_reaction_job(interp, reaction, reason)
  }

  // Clear reaction queues
  promise_data.fulfill_reactions.clear()
  promise_data.reject_reactions.clear()
}

///|
/// Enqueue a promise reaction job (microtask).
/// This implements NewPromiseReactionJob from ECMAScript spec.
pub fn enqueue_promise_reaction_job(
  interp : Interpreter,
  reaction : PromiseReaction,
  argument : Value,
) -> Unit {
  // Create a job that will execute the reaction handler
  let job_callback = make_interp_method_func(
    name="PromiseReactionJob",
    length=0,
    realm_state=Some(interp.realm_state),
    fn(i, _this, args) raise {
      let value = if args.length() > 0 { args[0] } else { Undefined }
      let loc = @token.Loc::default()

      // If handler is None, use identity (fulfill) or thrower (reject)
      match reaction.handler {
        Some(handler) => {
          // Try to call the handler
          let result = i.call_value(handler, Undefined, [value], loc) catch {
            e => {
              guard is_js_catchable_error(e) else { raise e }
              // Handler threw - reject the dependent promise
              let error_value = js_error_to_value_with_env(e, Some(i.global))
              let _ = i.call_value(
                reaction.reject,
                Undefined,
                [error_value],
                loc,
              )
              return Undefined
            }
          }
          // Resolve the dependent promise with handler result
          let _ = i.call_value(reaction.resolve, Undefined, [result], loc)
          Undefined
        }
        None =>
          // No handler - pass through value/reason
          match reaction.reaction_type {
            Fulfill => {
              let _ = i.call_value(reaction.resolve, Undefined, [value], loc)
              Undefined
            }
            Reject => {
              let _ = i.call_value(reaction.reject, Undefined, [value], loc)
              Undefined
            }
          }
      }
    },
  )
  interp.enqueue_microtask(job_callback, [argument])
}

///|
/// Create resolve and reject capability functions for a promise.
/// Returns (resolve_func, reject_func).
pub fn create_resolving_functions(
  interp : Interpreter,
  promise_data : PromiseData,
) -> (Value, Value) {
  // Track whether already resolved (prevents multiple resolution)
  let already_resolved : Ref[Bool] = { val: false }

  // Create resolve function
  let resolve_func = make_interp_method_func(
    name="",
    length=1,
    realm_state=Some(interp.realm_state),
    fn(i, _this, args) raise {
      if already_resolved.val {
        return Undefined
      }
      already_resolved.val = true
      let resolution = if args.length() > 0 { args[0] } else { Undefined }

      // Check for self-resolution (ECMAScript spec requirement)
      match resolution {
        Promise(pd) =>
          if physical_equal(pd, promise_data) {
            // Self-resolution: reject with TypeError
            let error = js_error_to_value_with_env(
              @errors.TypeError(message="Chaining cycle detected for promise"),
              Some(i.global),
            )
            reject_promise(i, promise_data, error)
            return Undefined
          }
        _ => ()
      }

      // Thenable assimilation (ECMAScript §25.6.1.3.2 steps 8-13)
      // If resolution is not an object, fulfill directly.
      // Otherwise, perform Get(resolution, "then") and check if callable.
      // This applies uniformly to both native Promises and plain objects
      // — the spec has no fast-path for native promises.
      if is_thenable_object_candidate(resolution) {
        // Get(resolution, "then") walks the prototype chain and
        // invokes accessors per ECMAScript spec
        let loc = @token.Loc::default()
        let then_val = i.get_property(resolution, "then", loc) catch {
          e => {
            guard is_js_catchable_error(e) else { raise e }
            // If Get throws, reject the promise with the error
            let err = js_error_to_value_with_env(e, Some(i.global))
            reject_promise(i, promise_data, err)
            return Undefined
          }
        }
        match then_val {
          Object(then_od) =>
            match then_od.callable {
              Some(_) => {
                // Thenable: enqueue PromiseResolveThenableJob
                let (res, rej) = create_resolving_functions(i, promise_data)
                let job = make_interp_method_func(
                  name="PromiseResolveThenableJob",
                  length=0,
                  realm_state=Some(i.realm_state),
                  fn(i2, _t, _a) raise {
                    try {
                      let _ = i2.call_value(
                        then_val,
                        resolution,
                        [res, rej],
                        loc,
                      )
                    } catch {
                      e => {
                        guard is_js_catchable_error(e) else { raise e }
                        let err = js_error_to_value_with_env(e, Some(i2.global))
                        let _ = i2.call_value(rej, Undefined, [err], loc)
                      }
                    }
                    Undefined
                  },
                )
                i.enqueue_microtask(job, [])
              }
              None => fulfill_promise(i, promise_data, resolution)
            }
          _ => fulfill_promise(i, promise_data, resolution)
        }
      } else {
        fulfill_promise(i, promise_data, resolution)
      }
      Undefined
    },
  )

  // Create reject function
  let reject_func = make_interp_method_func(
    name="",
    length=1,
    realm_state=Some(interp.realm_state),
    fn(i, _this, args) {
      if already_resolved.val {
        return Undefined
      }
      already_resolved.val = true
      let reason = if args.length() > 0 { args[0] } else { Undefined }
      reject_promise(i, promise_data, reason)
      Undefined
    },
  )
  (resolve_func, reject_func)
}

///|
/// Run the event loop: process all pending timer tasks with
/// microtask checkpoints between each (per WHATWG event loop spec).
/// Timers are sorted by (delay, insertion_order) so they fire in the
/// correct relative order. After each timer callback, microtasks are
/// drained before the next timer fires.
///
/// For setInterval, the timer is re-enqueued after each invocation
/// until cancelled. A safety limit prevents infinite loops.
pub fn Interpreter::run_timers(self : Interpreter) -> Unit raise Error {
  match self.run_timers_observed() {
    Ok(_) => ()
    Err(failure) => raise failure.cause()
  }
}

///|
/// Failure phase retained for the stable root facade's diagnostic adapter.
pub enum TimerRunFailurePhase {
  TimerQueueDispatch
  TimerCallback
  IntervalCallback
  MicrotaskCheckpoint
}

///|
/// A timer-run failure paired atomically with the phase that observed it.
pub struct TimerRunFailure {
  cause_ : Error
  phase_ : TimerRunFailurePhase
  source_identity_ : String?
}

///|
fn TimerRunFailure::TimerRunFailure(
  cause : Error,
  phase : TimerRunFailurePhase,
  source_identity : String?,
) -> TimerRunFailure {
  { cause_: cause, phase_: phase, source_identity_: source_identity }
}

///|
pub fn TimerRunFailure::cause(self : TimerRunFailure) -> Error {
  self.cause_
}

///|
pub fn TimerRunFailure::phase(self : TimerRunFailure) -> TimerRunFailurePhase {
  self.phase_
}

///|
pub fn TimerRunFailure::source_identity(self : TimerRunFailure) -> String? {
  self.source_identity_
}

///|
/// Run timers while retaining callback-vs-checkpoint context for direct
/// package dependents. Queue-policy defects still raise as internal failures.
pub fn Interpreter::run_timers_observed(
  self : Interpreter,
) -> Result[Unit, TimerRunFailure] raise Error {
  with_cleared_active_callee_realm(self.realm_state, fn() raise {
    let mut policy = TimerQueuePolicyCore(TIMER_SAFETY_LIMIT)
    while true {
      match policy.readiness(self.host.timer_queue.is_empty()) {
        TimerQueueEmpty | TimerQueueSafetyLimit => break
        TimerQueueObservationRequired => ()
      }
      self.observe_execution_step() catch {
        error => {
          let _ = policy.reject_dispatch_observation(
            self.host.timer_queue.is_empty(),
          )
          return Err(
            TimerRunFailure(
              error,
              TimerRunFailurePhase::TimerQueueDispatch,
              None,
            ),
          )
        }
      }
      policy = policy.accept_dispatch_observation(
        self.host.timer_queue.is_empty(),
      )
      let popped = match self.host.timer_queue.pop() {
        Some(timer) => timer
        None =>
          invalid_timer_queue_policy_transition(
            "accepted observation requires a queued timer",
          )
      }
      let cancelled = self.host.cancelled_timer_ids.contains(popped.id)
      let (next_policy, commit_decision) = policy.commit_popped(
        popped, cancelled,
      )
      policy = next_policy
      let timer = match commit_decision {
        TimerQueueDiscardCommitted => continue
        TimerQueueRunCommitted(timer) => timer
      }
      // Execute the timer callback
      let loc = @token.Loc::default()
      let call_result = observe_source_failure(self.realm_state, fn() raise {
        self.call_value(timer.callback, Undefined, timer.args, loc)
      })
      match call_result {
        Ok(_) => ()
        Err(failure) => {
          let _ = policy.callback_failed()
          let phase = if timer.is_interval {
            TimerRunFailurePhase::IntervalCallback
          } else {
            TimerRunFailurePhase::TimerCallback
          }
          return Err(
            TimerRunFailure(failure.cause(), phase, failure.source_identity()),
          )
        }
      }
      policy = policy.callback_succeeded()
      // Microtask checkpoint after each task (per WHATWG event loop spec)
      let microtask_result = self.run_microtasks_observed() catch {
        e => {
          let _ = policy.checkpoint_failed()
          return Err(
            TimerRunFailure(e, TimerRunFailurePhase::MicrotaskCheckpoint, None),
          )
        }
      }
      match microtask_result {
        Ok(_) => ()
        Err(failure) => {
          let _ = policy.checkpoint_failed()
          return Err(
            TimerRunFailure(
              failure.cause(),
              TimerRunFailurePhase::MicrotaskCheckpoint,
              failure.source_identity(),
            ),
          )
        }
      }
      let cancelled = self.host.cancelled_timer_ids.contains(timer.id)
      let insertion_order = self.host.timer_insertion_counter.val
      let (next_policy, checkpoint_decision) = policy.checkpoint_succeeded(
        cancelled, insertion_order,
      )
      policy = next_policy
      match checkpoint_decision {
        TimerQueueDoNotRequeue => ()
        TimerQueueRequeue(timer) => {
          self.host.timer_insertion_counter.val = insertion_order + 1
          self.host.timer_queue.push(timer)
        }
      }
    }
    let (_, clear_cancellations) = policy.complete_run(
      self.host.timer_queue.is_empty(),
    )
    if clear_cancellations {
      self.host.cancelled_timer_ids.clear()
    }
    Ok(())
  }) catch {
    e => raise e
  }
}