/**
 * App-facing ownership boundary for ephemeral webcam capture.
 *
 * It has no direct browser acquisition capability: callers inject `acquire`
 * and `createVideo`, then call `start` only from an explicit user action. It
 * owns every stream returned by that acquisition and stops every track on
 * Stop, cancellation, failure, or disposal. Nothing is recorded or persisted.
 */

export interface WebcamCaptureBinding {
  stream: MediaStream;
  video: HTMLVideoElement;
}

export type WebcamCaptureStatus = "off" | "requesting" | "active" | "disposed";

export interface WebcamCaptureOptions {
  acquire: () => Promise<MediaStream>;
  createVideo: () => HTMLVideoElement;
}

export interface WebcamCaptureController {
  status(): WebcamCaptureStatus;
  binding(): WebcamCaptureBinding | null;
  /** Resolves null when Stop/dispose overtakes acquisition or playback. */
  start(): Promise<WebcamCaptureBinding | null>;
  stop(): void;
  dispose(): void;
}

export function createWebcamCapture(options: WebcamCaptureOptions): WebcamCaptureController {
  if (typeof options.acquire !== "function" || typeof options.createVideo !== "function") {
    throw new RangeError("webcam capture: acquire and createVideo are required");
  }
  let state: WebcamCaptureStatus = "off";
  let current: WebcamCaptureBinding | null = null;
  let operation = 0;

  function release(binding: WebcamCaptureBinding): void {
    for (const track of binding.stream.getTracks()) track.stop();
    binding.video.pause();
    binding.video.srcObject = null;
    binding.video.remove();
  }

  function stop(): void {
    if (state === "disposed") return;
    operation += 1;
    const binding = current;
    current = null;
    if (binding) release(binding);
    state = "off";
  }

  return {
    status: () => state,
    binding: () => current ? { ...current } : null,
    async start() {
      if (state === "disposed") throw new Error("webcam capture: disposed");
      if (state === "requesting" || state === "active") throw new Error("webcam capture: already started");
      const started = ++operation;
      state = "requesting";
      let stream: MediaStream;
      try {
        stream = await options.acquire();
      } catch (error) {
        if (started !== operation) return null;
        state = "off";
        throw error;
      }
      if (started !== operation) {
        for (const track of stream.getTracks()) track.stop();
        return null;
      }
      let video: HTMLVideoElement;
      try {
        video = options.createVideo();
        video.srcObject = stream;
      } catch (error) {
        for (const track of stream.getTracks()) track.stop();
        state = "off";
        throw error;
      }
      const binding = { stream, video };
      // Publish ownership before playback yields. A pagehide during a pending
      // play promise can now stop the tracks synchronously.
      current = binding;
      try {
        await video.play();
      } catch (error) {
        if (current === binding) {
          current = null;
          release(binding);
        }
        if (started !== operation) return null;
        state = "off";
        throw error;
      }
      if (started !== operation || current !== binding) {
        if (current === binding) {
          current = null;
          release(binding);
        }
        return null;
      }
      state = "active";
      return { ...binding };
    },
    stop,
    dispose() {
      if (state === "disposed") return;
      stop();
      state = "disposed";
    },
  };
}
