import { toPng } from "html-to-image";

interface CaptureVideoOptions {
  durationMs?: number;
  fileName: string;
  fps?: number;
  includeAudio?: boolean;
  pixelRatio?: number;
  videoSrc: string;
}

interface AudioCapture {
  close: () => Promise<void>;
  play: () => Promise<void>;
  reset: () => Promise<void>;
  stream: MediaStream;
}

function getSupportedMimeType() {
  if (typeof MediaRecorder === "undefined") return "";

  const candidates = [
    "video/mp4;codecs=avc1.42E01E,mp4a.40.2",
    "video/mp4;codecs=h264,aac",
    "video/mp4",
    "video/webm;codecs=vp9,opus",
    "video/webm;codecs=vp8,opus",
    "video/webm;codecs=avc1,opus",
    "video/webm;codecs=vp9",
    "video/webm;codecs=vp8",
    "video/webm",
  ];

  return candidates.find((type) => MediaRecorder.isTypeSupported(type)) || "";
}

function resolveVideoFileName(fileName: string, mimeType: string) {
  const extension = mimeType.includes("mp4") ? "mp4" : "webm";
  if (/\.(webm|mp4)$/i.test(fileName)) {
    return fileName.replace(/\.(webm|mp4)$/i, `.${extension}`);
  }

  return `${fileName}.${extension}`;
}

function waitForVideoReady(video: HTMLVideoElement) {
  return new Promise<void>((resolve, reject) => {
    if (video.readyState >= 2) {
      resolve();
      return;
    }

    const cleanup = () => {
      video.removeEventListener("loadeddata", handleReady);
      video.removeEventListener("canplay", handleReady);
      video.removeEventListener("error", handleError);
    };
    const handleReady = () => {
      cleanup();
      resolve();
    };
    const handleError = () => {
      cleanup();
      reject(new Error("Video konnte nicht geladen werden."));
    };

    video.addEventListener("loadeddata", handleReady, { once: true });
    video.addEventListener("canplay", handleReady, { once: true });
    video.addEventListener("error", handleError, { once: true });
  });
}

function seekMediaToStart(media: HTMLMediaElement) {
  return new Promise<void>((resolve) => {
    if (!Number.isFinite(media.duration) || media.readyState < 1) {
      resolve();
      return;
    }

    const cleanup = () => {
      window.clearTimeout(timeoutId);
      media.removeEventListener("seeked", handleSeeked);
    };
    const handleSeeked = () => {
      cleanup();
      resolve();
    };
    const timeoutId = window.setTimeout(() => {
      cleanup();
      resolve();
    }, 350);

    media.addEventListener("seeked", handleSeeked, { once: true });
    media.currentTime = 0;
  });
}

function loadImage(src: string) {
  return new Promise<HTMLImageElement>((resolve, reject) => {
    const image = new Image();
    image.onload = () => resolve(image);
    image.onerror = () => reject(new Error("Overlay konnte nicht geladen werden."));
    image.src = src;
  });
}

function drawCoverVideo(
  ctx: CanvasRenderingContext2D,
  video: HTMLVideoElement,
  width: number,
  height: number,
) {
  const videoWidth = video.videoWidth || width;
  const videoHeight = video.videoHeight || height;
  const scale = Math.max(width / videoWidth, height / videoHeight);
  const drawWidth = videoWidth * scale;
  const drawHeight = videoHeight * scale;
  const drawX = (width - drawWidth) / 2;
  const drawY = (height - drawHeight) / 2;

  ctx.drawImage(video, drawX, drawY, drawWidth, drawHeight);
}

function cloneNodeForTransparentOverlay(node: HTMLElement) {
  const clone = node.cloneNode(true) as HTMLElement;

  clone.querySelectorAll("video").forEach((video) => {
    video.remove();
  });

  clone.style.background = "transparent";
  clone.style.backgroundImage = "none";
  clone.style.backgroundColor = "transparent";

  return clone;
}

async function createAudioCapture(videoSrc: string): Promise<AudioCapture | null> {
  if (typeof window === "undefined" || typeof AudioContext === "undefined") {
    return null;
  }

  try {
    const audioVideo = document.createElement("video");
    audioVideo.muted = false;
    audioVideo.playsInline = true;
    audioVideo.loop = true;
    audioVideo.preload = "auto";
    audioVideo.volume = 1;
    audioVideo.src = videoSrc;

    await waitForVideoReady(audioVideo);

    const audioContext = new AudioContext();
    const source = audioContext.createMediaElementSource(audioVideo);
    const gain = audioContext.createGain();
    const destination = audioContext.createMediaStreamDestination();

    gain.gain.value = 1;
    source.connect(gain);
    gain.connect(destination);

    return {
      close: async () => {
        audioVideo.pause();
        audioVideo.removeAttribute("src");
        audioVideo.load();
        source.disconnect();
        gain.disconnect();
        await audioContext.close().catch(() => undefined);
      },
      play: async () => {
        audioVideo.currentTime = 0;
        if (audioContext.state === "suspended") {
          await audioContext.resume();
        }
        await audioVideo.play();
      },
      reset: () => seekMediaToStart(audioVideo),
      stream: destination.stream,
    };
  } catch {
    return null;
  }
}

async function createTransparentOverlay(node: HTMLElement, pixelRatio: number) {
  const rect = node.getBoundingClientRect();
  const clone = cloneNodeForTransparentOverlay(node);
  const wrapper = document.createElement("div");

  wrapper.style.position = "fixed";
  wrapper.style.left = "-10000px";
  wrapper.style.top = "0";
  wrapper.style.width = `${rect.width}px`;
  wrapper.style.height = `${rect.height}px`;
  wrapper.style.pointerEvents = "none";
  wrapper.appendChild(clone);
  document.body.appendChild(wrapper);

  try {
    return await toPng(clone, {
      backgroundColor: "transparent",
      cacheBust: true,
      pixelRatio,
    });
  } finally {
    wrapper.remove();
  }
}

export async function captureElementWithVideoBackground(
  node: HTMLElement,
  options: CaptureVideoOptions,
) {
  if (typeof MediaRecorder === "undefined") {
    throw new Error("Video-Export wird von diesem Browser nicht unterstützt.");
  }

  const mimeType = getSupportedMimeType();
  if (!mimeType) {
    throw new Error("Dieser Browser unterstützt keinen WebM-Export.");
  }

  const rect = node.getBoundingClientRect();
  const pixelRatio = options.pixelRatio ?? 2;
  const width = Math.max(1, Math.round(rect.width * pixelRatio));
  const height = Math.max(1, Math.round(rect.height * pixelRatio));
  const fps = options.fps ?? 30;
  const durationMs = options.durationMs ?? 6000;

  const video = document.createElement("video");
  video.muted = true;
  video.playsInline = true;
  video.loop = true;
  video.preload = "auto";
  video.volume = 0;
  video.src = options.videoSrc;

  await waitForVideoReady(video);
  video.currentTime = 0;
  const audioCapture = options.includeAudio === false ? null : await createAudioCapture(options.videoSrc);

  // Start media before the expensive DOM-to-image work so browser audio permission
  // still belongs to the user's export click.
  await Promise.all([video.play(), audioCapture?.play() ?? Promise.resolve()]);

  const overlayDataUrl = await createTransparentOverlay(node, pixelRatio);
  const overlayImage = await loadImage(overlayDataUrl);
  await Promise.all([seekMediaToStart(video), audioCapture?.reset() ?? Promise.resolve()]);

  const canvas = document.createElement("canvas");
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext("2d");
  if (!ctx) throw new Error("Canvas konnte nicht erstellt werden.");
  const context = ctx;

  const canvasStream = canvas.captureStream(fps);
  const stream = new MediaStream([
    ...canvasStream.getVideoTracks(),
    ...(audioCapture?.stream.getAudioTracks() ?? []),
  ]);
  const chunks: BlobPart[] = [];
  const recorder = new MediaRecorder(stream, {
    audioBitsPerSecond: 160_000,
    mimeType,
    videoBitsPerSecond: 8_000_000,
  });

  recorder.ondataavailable = (event) => {
    if (event.data.size > 0) chunks.push(event.data);
  };

  const stopped = new Promise<void>((resolve, reject) => {
    recorder.onstop = () => resolve();
    recorder.onerror = () => reject(new Error("Video-Aufnahme fehlgeschlagen."));
  });

  const startedAt = performance.now();
  let animationFrame = 0;

  function drawFrame() {
    context.clearRect(0, 0, width, height);
    drawCoverVideo(context, video, width, height);
    context.drawImage(overlayImage, 0, 0, width, height);

    if (performance.now() - startedAt < durationMs) {
      animationFrame = requestAnimationFrame(drawFrame);
    } else if (recorder.state !== "inactive") {
      recorder.stop();
    }
  }

  recorder.start();
  drawFrame();
  await stopped;

  cancelAnimationFrame(animationFrame);
  video.pause();
  await audioCapture?.close();
  canvasStream.getTracks().forEach((track) => track.stop());
  stream.getTracks().forEach((track) => track.stop());

  const blob = new Blob(chunks, { type: mimeType });
  return new File([blob], resolveVideoFileName(options.fileName, mimeType), { type: mimeType });
}
