function detailEvent(type, detail) {
  if (typeof CustomEvent === "function") {
    return new CustomEvent(type, { detail });
  }
  const event = new Event(type);
  Object.defineProperty(event, "detail", { value: detail });
  return event;
}

function abortError(signal) {
  if (signal?.reason instanceof Error) return signal.reason;
  const message = signal?.reason ? String(signal.reason) : "The operation was aborted.";
  if (typeof DOMException === "function") {
    return new DOMException(message, "AbortError");
  }
  const error = new Error(message);
  error.name = "AbortError";
  return error;
}

function stateId(sprite) {
  return typeof sprite === "string" ? sprite : sprite?.id || String(sprite);
}

/**
 * Optional semantic timing helper. The host explicitly begins and completes work;
 * the director only handles presentation thresholds and never infers AI state.
 */
export class StateDirector extends EventTarget {
  constructor(engine, options = {}) {
    super();
    if (!engine?.setSprite) throw new TypeError("StateDirector requires an engine.");
    this.engine = engine;
    this.options = {
      deepThinkingAfterMs: 7000,
      stillWorkingAfterMs: 18000,
      transition: "field-morph",
      ...options,
    };
    this.timers = new Set();
    this.state = engine.currentRecipe?.id || "ai.idle";
    this.operation = 0;
    this.destroyed = false;
  }

  clearTimers() {
    for (const timer of this.timers) clearTimeout(timer);
    this.timers.clear();
  }

  schedule(callback, delay) {
    const timer = setTimeout(() => {
      this.timers.delete(timer);
      callback();
    }, Math.max(0, Number(delay) || 0));
    this.timers.add(timer);
    return timer;
  }

  changeState(sprite, engineOptions = {}, metadata = {}) {
    if (metadata.clearTimers !== false) this.clearTimers();
    const from = this.state;
    const to = stateId(sprite);
    this.state = to;
    this.engine.setSprite(sprite, engineOptions);
    this.dispatchEvent(
      detailEvent("statechange", {
        state: to,
        from,
        to,
        reason: metadata.reason || "set",
      }),
    );
    return this;
  }

  set(sprite, options = {}) {
    this.operation += 1;
    return this.changeState(
      sprite,
      {
        transition: options.transition || this.options.transition,
        preservePhase: options.preservePhase ?? true,
        duration: options.duration,
      },
      { reason: options.reason || "set" },
    );
  }

  startThinking(options, operation) {
    this.changeState(
      "ai.thinking",
      {
        transition: options.transition || this.options.transition,
        preservePhase: options.preservePhase ?? true,
        duration: options.duration,
      },
      { reason: options.reason || "thinking-start" },
    );
    this.schedule(() => {
      if (this.operation !== operation || this.state !== "ai.thinking") return;
      this.changeState(
        "ai.thinking-deep",
        { transition: "neighbor-ignite", preservePhase: true },
        { clearTimers: false, reason: "thinking-escalation" },
      );
    }, options.deepThinkingAfterMs ?? this.options.deepThinkingAfterMs);
    this.schedule(() => {
      if (
        this.operation !== operation ||
        !["ai.thinking", "ai.thinking-deep"].includes(this.state)
      ) {
        return;
      }
      this.changeState(
        "ai.still-working",
        { transition: "radial-cascade", preservePhase: true },
        { clearTimers: false, reason: "thinking-escalation" },
      );
    }, options.stillWorkingAfterMs ?? this.options.stillWorkingAfterMs);
    return this;
  }

  beginThinking(options = {}) {
    const operation = ++this.operation;
    return this.startThinking(options, operation);
  }

  finish(options = {}, reason = "complete") {
    const sprite = options.sprite || "status.success";
    this.changeState(
      sprite,
      {
        transition: options.transition || "path-draw",
        preservePhase: options.preservePhase ?? true,
        duration: options.duration,
      },
      { reason },
    );
    this.engine.signal?.("complete", { energy: 1 });
    return this;
  }

  complete(options = {}) {
    this.operation += 1;
    return this.finish(options, options.reason || "complete");
  }

  reject(options = {}, reason = "failure") {
    return this.changeState(
      options.sprite || "status.error",
      {
        transition: options.transition || "glitch-bands",
        preservePhase: options.preservePhase ?? true,
        duration: options.duration,
      },
      { reason },
    );
  }

  fail(options = {}) {
    this.operation += 1;
    return this.reject(options, options.reason || "failure");
  }

  cancel(options = {}) {
    this.operation += 1;
    return this.changeState(
      options.sprite || "status.cancelled",
      {
        transition: options.transition || "seeded-dissolve",
        preservePhase: options.preservePhase ?? true,
        duration: options.duration,
      },
      { reason: options.reason || "cancelled" },
    );
  }

  reset(options = {}) {
    this.operation += 1;
    return this.changeState(
      options.sprite || "ai.idle",
      {
        transition: options.transition || "field-morph",
        preservePhase: options.preservePhase ?? false,
        duration: options.duration,
      },
      { reason: options.reason || "reset" },
    );
  }

  /**
   * Present one asynchronous task through thinking escalation and a terminal
   * state. The work receives `{ signal, director, engine }`; existing zero-arg
   * functions remain valid because JavaScript ignores extra arguments.
   */
  async run(work, options = {}) {
    if (this.destroyed) throw new Error("StateDirector has been destroyed.");
    if (typeof work !== "function" && !work?.then) {
      throw new TypeError("StateDirector.run requires a function or Promise.");
    }

    const signal = options.signal;
    const operation = ++this.operation;
    if (signal?.aborted) {
      if (this.operation === operation) {
        this.changeState(
          options.cancel?.sprite || options.cancelledSprite || "status.cancelled",
          {
            transition: options.cancel?.transition || "seeded-dissolve",
            preservePhase: options.cancel?.preservePhase ?? true,
            duration: options.cancel?.duration,
          },
          { reason: "run-aborted" },
        );
      }
      throw abortError(signal);
    }

    this.startThinking(options.thinking || options, operation);
    let removeAbortListener = () => {};
    let task;
    try {
      task =
        typeof work === "function"
          ? Promise.resolve().then(() =>
              work({ signal, director: this, engine: this.engine }),
            )
          : Promise.resolve(work);

      if (signal) {
        const aborted = new Promise((_, reject) => {
          const onAbort = () => reject(abortError(signal));
          signal.addEventListener("abort", onAbort, { once: true });
          removeAbortListener = () => signal.removeEventListener("abort", onAbort);
        });
        task = Promise.race([task, aborted]);
      }

      const result = await task;
      if (this.operation === operation) {
        this.finish(
          {
            ...(options.complete || {}),
            sprite: options.complete?.sprite || options.successSprite,
          },
          "run-complete",
        );
      }
      return result;
    } catch (error) {
      if (this.operation === operation) {
        if (signal?.aborted || error?.name === "AbortError") {
          this.changeState(
            options.cancel?.sprite || options.cancelledSprite || "status.cancelled",
            {
              transition: options.cancel?.transition || "seeded-dissolve",
              preservePhase: options.cancel?.preservePhase ?? true,
              duration: options.cancel?.duration,
            },
            { reason: "run-aborted" },
          );
        } else {
          this.reject(
            {
              ...(options.fail || {}),
              sprite: options.fail?.sprite || options.errorSprite,
            },
            "run-failed",
          );
        }
      }
      throw error;
    } finally {
      removeAbortListener();
    }
  }

  destroy() {
    if (this.destroyed) return;
    this.destroyed = true;
    this.operation += 1;
    this.clearTimers();
  }
}

export default StateDirector;
