import { efConfigurationContext } from "./EFConfiguration.js";
import { efContext } from "./efContext.js";
import { fetchContext } from "./fetchContext.js";
import { focusContext } from "./focusContext.js";
import { focusedElementContext } from "./focusedElementContext.js";
import { loopContext, playingContext } from "./playingContext.js";
import { consume, createContext, provide } from "@lit/context";
import { property, state } from "lit/decorators.js";
import _decorate from "@oxc-project/runtime/helpers/decorate";
const targetTimegroupContext = createContext("target-timegroup");
const contextMixinSymbol = Symbol("contextMixin");
function isContextMixin(value) {
	return typeof value === "object" && value !== null && contextMixinSymbol in value.constructor;
}
function ContextMixin(superClass) {
	class ContextElement extends superClass {
		constructor(..._args) {
			super(..._args);
			this.efConfiguration = null;
			this.focusContext = this;
			this.efContext = this;
			this.targetTimegroup = null;
			this.fetch = async (url, init = {}) => {
				init.headers ||= {};
				Object.assign(init.headers, { "Content-Type": "application/json" });
				if (this.signingURL) {
					const now = Date.now();
					const { cacheKey, signingPayload } = this.#getTokenCacheKey(url);
					const tokenExpiration = this.#URLTokenExpirations[cacheKey] || 0;
					if (!this.#URLTokens[cacheKey] || now >= tokenExpiration) this.#URLTokens[cacheKey] = fetch(this.signingURL, {
						method: "POST",
						body: JSON.stringify(signingPayload)
					}).then(async (response) => {
						if (response.ok) {
							const tokenData = await response.json();
							const token = tokenData.token;
							this.#URLTokenExpirations[cacheKey] = this.#parseTokenExpiration(token);
							return token;
						}
						throw new Error(`Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`);
					});
					const urlToken = await this.#URLTokens[cacheKey];
					Object.assign(init.headers, { authorization: `Bearer ${urlToken}` });
				} else init.credentials = "include";
				return fetch(url, init);
			};
			this.playing = false;
			this.loop = false;
			this.rendering = false;
			this.currentTimeMs = 0;
		}
		static {
			this[contextMixinSymbol] = true;
		}
		#apiHost;
		get apiHost() {
			return this.#apiHost ?? this.efConfiguration?.apiHost ?? "";
		}
		set apiHost(value) {
			this.#apiHost = value;
		}
		get durationMs() {
			return this.targetTimegroup?.durationMs ?? 0;
		}
		get endTimeMs() {
			return this.targetTimegroup?.endTimeMs ?? 0;
		}
		#URLTokens = {};
		#URLTokenExpirations = {};
		/**
		* Generate a cache key for URL token based on signing strategy
		*
		* Uses unified prefix + parameter matching approach:
		* - For transcode URLs: signs base "/api/v1/transcode" + params like {url: "source.mp4"}
		* - For regular URLs: signs full URL with empty params {}
		* - All validation uses prefix matching + exhaustive parameter matching
		* - Multiple transcode segments with same source share one token (reduces round-trips)
		*/
		#getTokenCacheKey(url) {
			try {
				const urlObj = new URL(url);
				if (urlObj.pathname.includes("/api/v1/transcode/")) {
					const urlParam = urlObj.searchParams.get("url");
					if (urlParam) {
						const basePath = `${urlObj.origin}/api/v1/transcode`;
						const cacheKey = `${basePath}?url=${urlParam}`;
						return {
							cacheKey,
							signingPayload: {
								url: basePath,
								params: { url: urlParam }
							}
						};
					}
				}
				return {
					cacheKey: url,
					signingPayload: { url }
				};
			} catch {
				return {
					cacheKey: url,
					signingPayload: { url }
				};
			}
		}
		/**
		* Parse JWT token to extract safe expiration time (with buffer)
		* @param token JWT token string
		* @returns Safe expiration timestamp in milliseconds (actual expiry minus buffer), or 0 if parsing fails
		*/
		#parseTokenExpiration(token) {
			try {
				const parts = token.split(".");
				if (parts.length !== 3) return 0;
				const payload = parts[1];
				if (!payload) return 0;
				const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
				const parsed = JSON.parse(decoded);
				const exp = parsed.exp;
				const iat = parsed.iat;
				if (!exp) return 0;
				const lifetimeSeconds = iat ? exp - iat : 3600;
				const tenPercentBufferMs = lifetimeSeconds * .1 * 1e3;
				const fiveMinutesMs = 5 * 60 * 1e3;
				const bufferMs = Math.min(fiveMinutesMs, tenPercentBufferMs);
				return exp * 1e3 - bufferMs;
			} catch {
				return 0;
			}
		}
		#signingURL;
		/**
		* A URL that will be used to generated signed tokens for accessing media files from the
		* editframe API. This is used to authenticate media requests per-user.
		*/
		get signingURL() {
			return this.#signingURL ?? this.efConfiguration?.signingURL ?? "";
		}
		set signingURL(value) {
			this.#signingURL = value;
		}
		#FPS = 30;
		#MS_PER_FRAME = 1e3 / this.#FPS;
		#timegroupObserver = new MutationObserver((mutations) => {
			let shouldUpdate = false;
			for (const mutation of mutations) if (mutation.type === "childList") {
				const newTimegroup = this.querySelector("ef-timegroup");
				if (newTimegroup !== this.targetTimegroup) {
					this.targetTimegroup = newTimegroup;
					shouldUpdate = true;
				} else if (mutation.target instanceof Element && (mutation.target.tagName === "EF-TIMEGROUP" || mutation.target.closest("ef-timegroup"))) shouldUpdate = true;
			} else if (mutation.type === "attributes") {
				if (mutation.attributeName === "duration" || mutation.attributeName === "mode" || mutation.target instanceof Element && (mutation.target.tagName === "EF-TIMEGROUP" || mutation.target.closest("ef-timegroup"))) shouldUpdate = true;
			}
			if (shouldUpdate) queueMicrotask(() => {
				this.requestUpdate();
				if (this.targetTimegroup) this.targetTimegroup.requestUpdate();
			});
		});
		connectedCallback() {
			super.connectedCallback();
			this.targetTimegroup = this.querySelector("ef-timegroup");
			this.#timegroupObserver.observe(this, {
				childList: true,
				subtree: true,
				attributes: true
			});
			if (this.playing) this.startPlayback();
		}
		disconnectedCallback() {
			super.disconnectedCallback();
			this.#timegroupObserver.disconnect();
			this.stopPlayback();
			this.#URLTokens = {};
			this.#URLTokenExpirations = {};
		}
		update(changedProperties) {
			if (changedProperties.has("playing")) if (this.playing) this.startPlayback();
			else this.stopPlayback();
			if (changedProperties.has("currentTimeMs") && this.targetTimegroup) {
				if (this.targetTimegroup.currentTimeMs !== this.currentTimeMs) {
					this.targetTimegroup.currentTimeMs = this.currentTimeMs;
					if (this.isConnected) this.dispatchEvent(new CustomEvent("timeupdate", { detail: {
						currentTimeMs: this.currentTimeMs,
						progress: this.currentTimeMs / this.targetTimegroup.durationMs
					} }));
				}
			}
			super.update(changedProperties);
		}
		play() {
			this.playing = true;
		}
		pause() {
			this.playing = false;
		}
		#playbackAudioContext = null;
		#playbackAnimationFrameRequest = null;
		#AUDIO_PLAYBACK_SLICE_MS = 47 * 1024 / 48e3 * 1e3;
		#syncPlayheadToAudioContext(target, startMs) {
			const rawTimeMs = startMs + (this.#playbackAudioContext?.currentTime ?? 0) * 1e3;
			const nextTimeMs = Math.round(rawTimeMs / this.#MS_PER_FRAME) * this.#MS_PER_FRAME;
			if (nextTimeMs !== this.currentTimeMs) this.currentTimeMs = nextTimeMs;
			this.#playbackAnimationFrameRequest = requestAnimationFrame(() => {
				this.#syncPlayheadToAudioContext(target, startMs);
			});
		}
		async stopPlayback() {
			if (this.#playbackAudioContext) {
				if (this.#playbackAudioContext.state !== "closed") await this.#playbackAudioContext.close();
			}
			if (this.#playbackAnimationFrameRequest) cancelAnimationFrame(this.#playbackAnimationFrameRequest);
			this.#playbackAudioContext = null;
			this.#playbackAnimationFrameRequest = null;
		}
		async startPlayback() {
			await this.stopPlayback();
			const timegroup = this.targetTimegroup;
			if (!timegroup) return;
			await timegroup.waitForMediaDurations();
			let currentMs = timegroup.currentTimeMs;
			const fromMs = currentMs;
			const toMs = timegroup.endTimeMs;
			if (fromMs >= toMs) {
				this.pause();
				return;
			}
			let bufferCount = 0;
			this.#playbackAudioContext = new AudioContext({ latencyHint: "playback" });
			if (this.#playbackAnimationFrameRequest) cancelAnimationFrame(this.#playbackAnimationFrameRequest);
			this.#syncPlayheadToAudioContext(timegroup, currentMs);
			const playbackContext = this.#playbackAudioContext;
			if (playbackContext.state === "suspended") {
				console.warn("AudioContext is suspended, media playback will not work until user has interacted with page.");
				this.playing = false;
				return;
			}
			await playbackContext.suspend();
			const fillBuffer = async () => {
				if (bufferCount > 2) return;
				const canFillBuffer = await queueBufferSource();
				if (canFillBuffer) fillBuffer();
			};
			const queueBufferSource = async () => {
				if (currentMs >= toMs) return false;
				const startMs = currentMs;
				const endMs = currentMs + this.#AUDIO_PLAYBACK_SLICE_MS;
				currentMs += this.#AUDIO_PLAYBACK_SLICE_MS;
				const audioBuffer = await timegroup.renderAudio(startMs, endMs);
				bufferCount++;
				const source = playbackContext.createBufferSource();
				source.buffer = audioBuffer;
				source.connect(playbackContext.destination);
				source.start((startMs - fromMs) / 1e3);
				source.onended = () => {
					bufferCount--;
					if (endMs >= toMs) {
						this.pause();
						if (this.loop) this.updateComplete.then(() => {
							this.currentTimeMs = 0;
							this.updateComplete.then(() => {
								this.play();
							});
						});
					} else fillBuffer();
				};
				return true;
			};
			await fillBuffer();
			await playbackContext.resume();
		}
	}
	_decorate([consume({
		context: efConfigurationContext,
		subscribe: true
	})], ContextElement.prototype, "efConfiguration", void 0);
	_decorate([provide({ context: focusContext })], ContextElement.prototype, "focusContext", void 0);
	_decorate([provide({ context: focusedElementContext }), state()], ContextElement.prototype, "focusedElement", void 0);
	_decorate([property({
		type: String,
		attribute: "api-host"
	})], ContextElement.prototype, "apiHost", null);
	_decorate([provide({ context: efContext })], ContextElement.prototype, "efContext", void 0);
	_decorate([provide({ context: targetTimegroupContext }), state()], ContextElement.prototype, "targetTimegroup", void 0);
	_decorate([state()], ContextElement.prototype, "durationMs", null);
	_decorate([state()], ContextElement.prototype, "endTimeMs", null);
	_decorate([provide({ context: fetchContext })], ContextElement.prototype, "fetch", void 0);
	_decorate([property({
		type: String,
		attribute: "signing-url"
	})], ContextElement.prototype, "signingURL", null);
	_decorate([provide({ context: playingContext }), property({
		type: Boolean,
		reflect: true
	})], ContextElement.prototype, "playing", void 0);
	_decorate([provide({ context: loopContext }), property({
		type: Boolean,
		reflect: true
	})], ContextElement.prototype, "loop", void 0);
	_decorate([property({ type: Boolean })], ContextElement.prototype, "rendering", void 0);
	_decorate([state()], ContextElement.prototype, "currentTimeMs", void 0);
	return ContextElement;
}
export { ContextMixin, isContextMixin, targetTimegroupContext };
