import { EF_RENDERING } from "../EF_RENDERING.js";
import { efContext } from "./efContext.js";
import { currentTimeContext } from "./currentTimeContext.js";
import { durationContext } from "./durationContext.js";
import { loopContext, playingContext } from "./playingContext.js";
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
import { isEFTemporal } from "../elements/EFTemporal.js";
import { globalURLTokenDeduplicator } from "../transcoding/cache/URLTokenDeduplicator.js";
import { efConfigurationContext } from "./EFConfiguration.js";
import { fetchContext } from "./fetchContext.js";
import { focusContext } from "./focusContext.js";
import { focusedElementContext } from "./focusedElementContext.js";
import { shouldSignUrl } from "./shouldSignUrl.js";
import { ContextProvider, consume, createContext, provide } from "@lit/context";
import { property, state } from "lit/decorators.js";

//#region src/gui/ContextMixin.ts
const targetTemporalContext = createContext(Symbol("target-temporal"));
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.durationMs = 0;
			this.endTimeMs = 0;
			this.fetch = async (url, init = {}) => {
				if (init.body) {
					init.headers ||= {};
					Object.assign(init.headers, { "Content-Type": "application/json" });
				}
				if (!EF_RENDERING() && this.signingURL && shouldSignUrl(url, window.location.origin)) {
					const { cacheKey, signingPayload } = this.#getTokenCacheKey(url);
					const urlToken = await globalURLTokenDeduplicator.getToken(cacheKey, async () => {
						try {
							const response = await fetch(this.signingURL, {
								method: "POST",
								body: JSON.stringify(signingPayload)
							});
							if (response.ok) return (await response.json()).token;
							throw new Error(`Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`);
						} catch (error) {
							console.error("ContextMixin urlToken fetch error", url, error);
							throw error;
						}
					}, (token) => this.#parseTokenExpiration(token));
					init.headers ||= {};
					Object.assign(init.headers, { authorization: `Bearer ${urlToken}` });
				} else {
					if (!shouldSignUrl(url, window.location.origin)) init.credentials = "include";
					if (this.#isEditframeDomain(url)) console.warn(`[Editframe] Request to ${new URL(url).hostname} has no signing URL configured. Ensure <ef-configuration signing-url="..."> is an ancestor of your <ef-preview> or <ef-workbench>.`);
				}
				try {
					return fetch(url, init).catch((error) => {
						if (error instanceof DOMException && error.name === "AbortError") throw error;
						console.error("ContextMixin fetch error", url, error, window.location.href);
						const enhancedError = new (error instanceof Error ? error.constructor : Error)(`Failed to fetch: ${url}. Original error: ${error instanceof Error ? error.message : String(error)}`);
						if (error instanceof Error && !(error instanceof DOMException)) {
							enhancedError.name = error.name;
							enhancedError.stack = error.stack;
							Object.assign(enhancedError, error);
						}
						throw enhancedError;
					});
				} catch (error) {
					console.error("ContextMixin fetch error (synchronous)", url, error, window.location.href);
					throw error;
				}
			};
			this.rendering = false;
		}
		static {
			this[contextMixinSymbol] = true;
		}
		#playingProvider;
		#loopProvider;
		#currentTimeMsProvider;
		#targetTemporalProvider;
		#loop = false;
		#apiHost;
		get apiHost() {
			return this.#apiHost ?? this.efConfiguration?.apiHost ?? "";
		}
		set apiHost(value) {
			this.#apiHost = value;
		}
		#targetTemporal = null;
		get targetTemporal() {
			return this.#targetTemporal;
		}
		#controllerSubscribed = false;
		/**
		* Find the first root temporal element (recursively searches through children)
		* Supports ef-timegroup, ef-video, ef-audio, and any other temporal elements
		* even when they're wrapped in non-temporal elements like divs
		*/
		findRootTemporal() {
			const findRecursive = (element) => {
				if (isEFTemporal(element)) return element;
				for (const child of element.children) {
					const found = findRecursive(child);
					if (found) return found;
				}
				return null;
			};
			for (const child of this.children) {
				const found = findRecursive(child);
				if (found) return found;
			}
			return null;
		}
		#subscribedController = null;
		set targetTemporal(value) {
			if (this.#targetTemporal === value && value?.playbackController === this.#subscribedController && this.#controllerSubscribed) return;
			if (this.#subscribedController) {
				this.#subscribedController.removeListener(this.#onControllerUpdate);
				this.#controllerSubscribed = false;
				this.#subscribedController = null;
			}
			this.#targetTemporal = value;
			this.#targetTemporalProvider?.setValue(value);
			this.requestUpdate("targetTemporal");
			this.requestUpdate("playing");
			this.requestUpdate("loop");
			this.requestUpdate("currentTimeMs");
			if (value?.playbackController && this.#loop) value.playbackController.setLoop(this.#loop);
			if (value && !value.playbackController) value.updateComplete?.then(() => {
				if (value === this.#targetTemporal && !this.#controllerSubscribed) this.requestUpdate();
			});
		}
		#onControllerUpdate = (event) => {
			switch (event.property) {
				case "playing":
					this.#playingProvider.setValue(event.value);
					break;
				case "loop":
					this.#loopProvider.setValue(event.value);
					break;
				case "currentTimeMs":
					this.#currentTimeMsProvider.setValue(event.value);
					break;
			}
		};
		#isEditframeDomain(url) {
			try {
				const hostname = new URL(url).hostname;
				return hostname === "editframe.dev" || hostname === "editframe.com" || hostname.endsWith(".editframe.dev") || hostname.endsWith(".editframe.com");
			} catch {
				return false;
			}
		}
		#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`;
						return {
							cacheKey: `${basePath}?url=${urlParam}`,
							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 tenPercentBufferMs = (iat ? exp - iat : 3600) * .1 * 1e3;
				const bufferMs = Math.min(300 * 1e3, 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;
		}
		get playing() {
			return this.targetTemporal?.playbackController?.playing ?? false;
		}
		set playing(value) {
			if (this.targetTemporal?.playbackController) this.targetTemporal.playbackController.setPlaying(value);
		}
		get loop() {
			return this.targetTemporal?.playbackController?.loop ?? this.#loop;
		}
		set loop(value) {
			const oldValue = this.#loop;
			this.#loop = value;
			if (this.targetTemporal?.playbackController) this.targetTemporal.playbackController.setLoop(value);
			this.requestUpdate("loop", oldValue);
		}
		get currentTimeMs() {
			return this.targetTemporal?.playbackController?.currentTimeMs ?? NaN;
		}
		set currentTimeMs(value) {
			if (this.targetTemporal?.playbackController) this.targetTemporal.playbackController.setCurrentTimeMs(value);
		}
		#timegroupObserver = new MutationObserver((mutations) => {
			let shouldUpdate = false;
			const undefinedEFTags = /* @__PURE__ */ new Set();
			for (const mutation of mutations) if (mutation.type === "childList") {
				const newTemporal = this.findRootTemporal();
				if (newTemporal !== this.targetTemporal) {
					this.targetTemporal = newTemporal;
					shouldUpdate = true;
				} else if (mutation.target instanceof Element && isEFTemporal(mutation.target)) shouldUpdate = true;
				if (!this.targetTemporal) {
					for (const node of mutation.addedNodes) if (node instanceof Element) this.#collectUndefinedEFTags(node, undefinedEFTags);
				}
			} else if (mutation.type === "attributes") {
				if ([
					"duration",
					"mode",
					"trimstart",
					"trimend",
					"sourcein",
					"sourceout"
				].includes(mutation.attributeName || "") || mutation.target instanceof Element && isEFTemporal(mutation.target)) shouldUpdate = true;
			}
			if (undefinedEFTags.size > 0) this.#retryTemporalDiscovery(undefinedEFTags);
			if (shouldUpdate) queueMicrotask(() => {
				this.updateDurationProperties();
				this.requestUpdate();
				if (this.targetTemporal) this.targetTemporal.requestUpdate();
			});
		});
		/**
		* Recursively collect ef-* tag names from an element tree that
		* have not yet been registered as custom elements.
		*/
		#collectUndefinedEFTags(el, tags) {
			const tag = el.tagName.toLowerCase();
			if (tag.startsWith("ef-") && !customElements.get(tag)) tags.add(tag);
			for (const child of el.children) this.#collectUndefinedEFTags(child, tags);
		}
		/**
		* Wait for unregistered ef-* custom elements to upgrade, then
		* retry findRootTemporal(). Mirrors the whenDefined pattern in play().
		*/
		async #retryTemporalDiscovery(tags) {
			await Promise.all([...tags].map((tag) => customElements.whenDefined(tag).catch(() => {})));
			if (this.targetTemporal) return;
			const found = this.findRootTemporal();
			if (found) {
				this.targetTemporal = found;
				await found.updateComplete;
				this.updateDurationProperties();
				this.requestUpdate();
			}
		}
		/**
		* Update duration properties when temporal element changes
		*/
		updateDurationProperties() {
			const newDuration = this.targetTemporal?.durationMs ?? 0;
			const newEndTime = this.targetTemporal?.endTimeMs ?? 0;
			if (this.durationMs !== newDuration) this.durationMs = newDuration;
			if (this.endTimeMs !== newEndTime) this.endTimeMs = newEndTime;
		}
		connectedCallback() {
			super.connectedCallback();
			this.#playingProvider = new ContextProvider(this, {
				context: playingContext,
				initialValue: this.playing
			});
			this.#loopProvider = new ContextProvider(this, {
				context: loopContext,
				initialValue: this.loop
			});
			this.#currentTimeMsProvider = new ContextProvider(this, {
				context: currentTimeContext,
				initialValue: this.currentTimeMs
			});
			this.#targetTemporalProvider = new ContextProvider(this, {
				context: targetTemporalContext,
				initialValue: this.targetTemporal
			});
			this.targetTemporal = this.findRootTemporal();
			this.updateDurationProperties();
			this.#timegroupObserver.observe(this, {
				childList: true,
				subtree: true,
				attributes: true
			});
		}
		disconnectedCallback() {
			super.disconnectedCallback();
			this.#timegroupObserver.disconnect();
			if (this.#subscribedController) {
				this.#subscribedController.removeListener(this.#onControllerUpdate);
				this.#controllerSubscribed = false;
				this.#subscribedController = null;
			}
			this.pause();
		}
		updated(changedProperties) {
			super.updated?.(changedProperties);
			const currentController = this.#targetTemporal?.playbackController;
			if (currentController && (!this.#controllerSubscribed || this.#subscribedController !== currentController)) {
				if (this.#subscribedController && this.#subscribedController !== currentController) this.#subscribedController.removeListener(this.#onControllerUpdate);
				currentController.addListener(this.#onControllerUpdate);
				this.#controllerSubscribed = true;
				this.#subscribedController = currentController;
				if (this.#loop) currentController.setLoop(this.#loop);
				this.#playingProvider.setValue(this.playing);
				this.#loopProvider.setValue(this.loop);
				this.#currentTimeMsProvider.setValue(this.currentTimeMs);
			}
		}
		async play() {
			if (!this.targetTemporal) {
				const potentialTemporalTags = Array.from(this.children).map((el) => el.tagName.toLowerCase()).filter((tag) => tag.startsWith("ef-"));
				await Promise.all(potentialTemporalTags.map((tag) => customElements.whenDefined(tag).catch(() => {})));
				const foundTemporal = this.findRootTemporal();
				if (foundTemporal) {
					this.targetTemporal = foundTemporal;
					await foundTemporal.updateComplete;
				} else {
					console.warn("No temporal element found to play");
					return;
				}
			}
			if (!this.targetTemporal.playbackController) {
				await this.targetTemporal.updateComplete;
				if (!this.targetTemporal.playbackController) {
					console.warn("PlaybackController not available for temporal element");
					return;
				}
			}
			this.targetTemporal.playbackController.play();
		}
		pause() {
			if (this.targetTemporal?.playbackController) this.targetTemporal.playbackController.pause();
		}
	}
	__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([state()], ContextElement.prototype, "targetTemporal", null);
	__decorate([provide({ context: durationContext }), property({ type: Number })], ContextElement.prototype, "durationMs", void 0);
	__decorate([property({ type: Number })], ContextElement.prototype, "endTimeMs", void 0);
	__decorate([provide({ context: fetchContext })], ContextElement.prototype, "fetch", void 0);
	__decorate([property({
		type: String,
		attribute: "signing-url"
	})], ContextElement.prototype, "signingURL", null);
	__decorate([property({
		type: Boolean,
		reflect: true
	})], ContextElement.prototype, "playing", null);
	__decorate([property({
		type: Boolean,
		reflect: true,
		attribute: "loop"
	})], ContextElement.prototype, "loop", null);
	__decorate([property({ type: Boolean })], ContextElement.prototype, "rendering", void 0);
	__decorate([property({ type: Number })], ContextElement.prototype, "currentTimeMs", null);
	return ContextElement;
}

//#endregion
export { ContextMixin, isContextMixin, targetTemporalContext };
//# sourceMappingURL=ContextMixin.js.map