---
import { getSecret } from 'astro:env/server'
import type { PrefixedProps as PrefixedCaptionProps } from './caption-props-map'
import type { OembedInfo } from './utils/oembed'
import type { Service, ServiceConfig, VideoInfo } from './utils/video'
import type { PrefixedProps as PrefixedZoomProps } from './zoom-props-map'
import { toCaptionProps } from './caption-props-map'
import Caption from './Caption.astro'
import InternalOembed from './internal/OEmbed.astro'
import VideoPlayer from './internal/VideoPlayer.astro'
import { fetchOEmbed } from './utils/oembed'
import { getVideoInfo, resolveVideoSource } from './utils/video'
import { toZoomerProps } from './zoom-props-map'
import Zoomer from './Zoomer.astro'

type Controls = 'full' | 'lightbox' | 'minimal' | 'native' | 'none'

/**
 * Props for the `<Video>` component — unified video player supporting Bunny,
 * Cloudflare, Mux, YouTube, Vimeo, oEmbed, and local files.
 */
export type Props = PrefixedCaptionProps &
	PrefixedZoomProps & {
		/**
		 * Start playback automatically (muted by default for browser autoplay
		 * policies).
		 *
		 * @default false
		 */
		autoPlay?: boolean | undefined
		/**
		 * Limit HLS quality to the element's rendered size. Saves bandwidth.
		 *
		 * @default true
		 */
		capQualityToSize?: boolean | undefined
		/**
		 * Player chrome level. `'none'` hides all controls. `'native'` skips
		 * media-chrome entirely and uses the underlying element's native controls —
		 * the service's iframe chrome for YouTube/Vimeo, the browser's built-in
		 * controls for local files and HLS. `zoom` is ignored when `'native'`.
		 *
		 * @default 'full'
		 */
		controls?: Controls | undefined
		/**
		 * Initial HLS bandwidth estimate in bits/s. Higher values start at higher
		 * quality.
		 */
		initialBandwidth?: number | undefined
		/** Accessible label for the video player. Falls back to the video title. */
		label?: string | undefined
		/**
		 * Loop playback.
		 *
		 * @default false
		 */
		loop?: boolean | undefined
		/**
		 * Mute audio.
		 *
		 * @default true
		 */
		muted?: boolean | undefined
		/** Poster image URL. Overrides the service-provided thumbnail. */
		poster?: string | undefined
		/**
		 * Preload behavior hint.
		 *
		 * @default 'metadata'
		 */
		preload?: 'auto' | 'metadata' | 'none' | undefined
		/**
		 * Video service override. Inferred from `src` when omitted. Required for
		 * Bunny title search.
		 */
		service?: Service | undefined
		/**
		 * Video source: a URL, raw service ID, local file path, or Bunny title
		 * string.
		 */
		src: string
	}

const {
	autoPlay = false,
	capQualityToSize,
	controls = 'full',
	credit,
	creditMediaType,
	creditMediaTypeFallback,
	creditOrganization,
	initialBandwidth,
	label,
	loop = false,
	muted = true,
	poster,
	preload = 'metadata',
	service,
	src,
	zoom = false,
	zoomLevel,
	zoomScope,
} = Astro.props

const zoomerProps = toZoomerProps({ zoom, zoomLevel, zoomScope })

const { identifier, service: resolvedService } = resolveVideoSource(src, service)

const config: ServiceConfig = {
	bunny: {
		apiAccessKey: getSecret('BUNNY_API_ACCESS_KEY') ?? '',
		hostname: getSecret('BUNNY_HOSTNAME') ?? '',
		libraryId: getSecret('BUNNY_LIBRARY_ID') ?? '',
	},
	cloudflare: {
		accountId: getSecret('CLOUDFLARE_STREAM_ACCOUNT_ID') ?? '',
		apiToken: getSecret('CLOUDFLARE_STREAM_API_TOKEN') ?? '',
	},
	local: {},
	mux: {
		accessToken: getSecret('MUX_TOKEN_ID') ?? '',
		secret: getSecret('MUX_TOKEN_SECRET') ?? '',
	},
	oembed: {},
	vimeo: {},
	youtube: {},
}

let videoInfo: undefined | VideoInfo
let oembedInfo: OembedInfo | undefined
let fetchError: string | undefined

if (resolvedService === 'oembed') {
	try {
		oembedInfo = await fetchOEmbed(identifier)
	} catch (error) {
		fetchError = `[astro-media-kit] Video oEmbed "${identifier}" failed to load: ${error instanceof Error ? error.message : String(error)}`
		console.error(fetchError)
	}
} else {
	try {
		videoInfo = await getVideoInfo(identifier, resolvedService, config, poster ?? '')
	} catch (error) {
		fetchError = `[astro-media-kit] Video "${identifier}" (${resolvedService}) failed to load: ${error instanceof Error ? error.message : String(error)}`
		console.error(fetchError)
	}
}

const resolvedLabel = label ?? oembedInfo?.title ?? videoInfo?.title ?? 'Video'

// Compute the video source URL based on service type.
let videoSrc: string | undefined
switch (resolvedService) {
	case 'bunny': // Falls through
	case 'cloudflare': // Falls through
	case 'mux': {
		videoSrc = videoInfo?.hlsUrl
		break
	}

	case 'local': {
		videoSrc = src
		break
	}

	case 'oembed': {
		// Handled separately via fetchOEmbed
		break
	}

	case 'vimeo': {
		videoSrc = `https://vimeo.com/${identifier}`
		break
	}

	case 'youtube': {
		videoSrc = `https://www.youtube.com/watch?v=${identifier}`
		break
	}
	// No default — ts/switch-exhaustiveness-check enforces all Service cases are handled
}

const captionProps = toCaptionProps(
	{ credit, creditMediaType, creditMediaTypeFallback, creditOrganization },
	{ typeFallback: 'video' },
)

const aspectRatio =
	videoInfo !== undefined &&
	videoInfo.width !== 0 &&
	!Number.isNaN(videoInfo.width) &&
	videoInfo.height !== 0 &&
	!Number.isNaN(videoInfo.height)
		? `${videoInfo.width} / ${videoInfo.height}`
		: undefined
const oembedAspectRatio =
	oembedInfo !== undefined && oembedInfo.width !== 0 && oembedInfo.height !== 0
		? `${oembedInfo.width} / ${oembedInfo.height}`
		: undefined

const hlsConfig: Record<string, boolean | number> = {}
if (initialBandwidth !== undefined) {
	hlsConfig.abrEwmaDefaultEstimate = initialBandwidth
}

if (capQualityToSize !== undefined) {
	hlsConfig.capLevelToPlayerSize = capQualityToSize
}

const posterUrl =
	resolvedService === 'local'
		? (poster ?? undefined)
		: resolvedService === 'oembed'
			? oembedInfo?.thumbnailUrl
			: videoInfo?.posterUrl

// Map service to the custom element tag name used in the lightbox.
const videoElementMap = {
	bunny: 'hls-video',
	cloudflare: 'hls-video',
	local: 'video',
	mux: 'hls-video',
	vimeo: 'vimeo-video',
	youtube: 'youtube-video',
} as const

const sharedProps = {
	aspectRatio,
	autoPlay,
	controls,
	intrinsicWidth: videoInfo?.width,
	loop,
	muted,
	posterUrl,
	preload,
	resolvedLabel,
	videoSrc: videoSrc ?? '',
	zoom: Boolean(zoom),
} as const
---

<style is:global>
	/* PhotoSwipe lightbox video wrapper. Global because it renders inside
	   PhotoSwipe's DOM, outside Astro's scoped style boundary. */
	.pswp__video-wrapper media-controller {
		width: 100%;
		height: 100%;
	}

	/* Let clicks fall through to the wrapper (which carries `pswp__img`)
	   so PhotoSwipe's built-in click handler treats the slide like an
	   image — single click runs `imageClickAction` (zoom-or-close).
	   Controls live outside the slide via the floating control bar. */
	.pswp__video-wrapper > * {
		pointer-events: none;
	}

	/* Floating control bar, mounted inside `pswp.scrollWrap` so it rides
	   along with PhotoSwipe's open/close transition. Stays anchored to the
	   viewport bottom while the video is zoomed/panned, bound to the active
	   slide's <media-controller> via its `mediacontroller` attribute (see
	   utils/lightbox.ts). `data-unbound` is set whenever the bar is not
	   attached to a video slide's controller; `data-user-inactive` drives
	   the autohide fade. Opacity transition is inherited from
	   `pswp__hide-on-close` so the fade timing/easing matches the native
	   close/arrow buttons on open, close, and autohide. */
	.pswp__floating-controls {
		will-change: opacity;
		/* Force the subtree onto its own GPU composite layer so opacity
		   fades the final rendered result uniformly, instead of fading
		   each semi-transparent child independently (which caused the
		   dark rectangle around the bar mid-transition). `isolation`
		   alone creates a stacking context but doesn't always promote
		   to a layer in Chromium; `will-change: opacity` does. */
		isolation: isolate;
		position: absolute;
		z-index: 10;
		inset: auto 0 0;
		/* Match the bar's default background using media-chrome's own
		   CSS custom property, so user overrides of the variable (or the
		   library's own future changes) propagate here too. The bg lives
		   on the wrapper (light DOM) rather than on the individual
		   shadow-DOM media-chrome elements, because nested shadow-root
		   translucency caused the black-rect artifact during opacity
		   transitions. */
		background: var(--media-control-background, rgb(20 20 30 / 70%));
	}

	/* Strip media-chrome's default semi-transparent dark backgrounds from
	   the bar and its buttons/ranges. Each media-chrome element has its
	   own shadow DOM that composites independently, so nested translucent
	   layers cause a black rectangle artifact during the opacity fade.
	   Flattening all descendants to transparent via the CSS var leaves
	   only the icons / progress bar to fade, eliminating the rect. On the
	   bar itself we also set `background: transparent` in light DOM to
	   belt-and-braces the composite layer. Children use only the var so
	   their shadow-DOM `:host(:hover)` rule — which depends on the
	   background being set from inside the shadow root — can still paint
	   the hover highlight.

	   `--media-control-bar-display: flex` stretches the bar to fill the
	   wrapper's width so the time-range expands across the full viewport
	   (media-chrome defaults the :host display to `inline-flex`). */
	.pswp__floating-controls media-control-bar {
		--media-control-background: transparent;
		--media-control-hover-background: rgb(255 255 255 / 10%);
		--media-control-bar-display: flex;
		background: transparent;
	}

	.pswp__floating-controls media-control-bar > * {
		--media-control-background: transparent;
		--media-control-hover-background: rgb(255 255 255 / 10%);
	}

	/* Specificity (0,3,0) via repeated class so this beats PhotoSwipe core's
	   `.pswp--ui-visible .pswp__hide-on-close { opacity: 1 }` (0,2,0)
	   regardless of stylesheet source order. Consumer builds can merge the
	   global block before the photoswipe vendor CSS, which would let the
	   inherited opacity:1 win and leak the bar onto non-video slides. */
	.pswp__floating-controls.pswp__floating-controls[data-unbound],
	.pswp__floating-controls.pswp__floating-controls[data-user-inactive] {
		pointer-events: none;
		opacity: 0;
	}

	/* Suppress the hover-background while the lightbox is mid-zoom. Each
	   media-chrome control composites its own translucent shadow-DOM layer;
	   stacking those while the wrapper's opacity is animating produces a
	   black-rect artifact. `data-transitioning` is set by lightbox.ts during
	   opening/closing animations (see `openingAnimationStart` listener). */
	.pswp__floating-controls[data-transitioning] media-control-bar > * {
		--media-control-hover-background: transparent;
	}

	/* Reserve space beneath the caption for the floating control bar on
	   video slides. The dynamic-caption plugin measures the caption with
	   getBoundingClientRect() (which includes padding) and subtracts its
	   height from the slide's pan area, so `padding-bottom` here propagates
	   into fit-mode zoom: the video shrinks to leave room, and the empty
	   padding space is where the absolute-positioned bar lands. Scoped to
	   `--below` only — in `--aside` layouts the caption is sideways and the
	   bar overlays the video rather than the caption. `--amk-lightbox-bar-height`
	   can be overridden if you customize media-chrome button sizing. */
	/* Video slides: stack the floating control bar height on top of the
	   plugin's own top padding (15px for --below, 10px for --mobile) so the
	   caption text keeps that same breathing room above the bar. The plugin
	   measures caption height via getBoundingClientRect() which includes
	   padding, so fit-mode zoom shrinks the video to make room automatically. */
	.pswp__item--video .pswp__dynamic-caption--below,
	.pswp__item--video .pswp__dynamic-caption--mobile {
		padding-bottom: calc(15px + var(--amk-lightbox-bar-height, 44px));
	}
</style>

{fetchError !== undefined && fetchError !== '' ? (
	import.meta.env.DEV && <p style="font-family: monospace; color: red;">{fetchError}</p>
) : resolvedService === 'oembed' ? (
	oembedInfo && (
		<Caption {...captionProps}>
			<InternalOembed
				aspectRatio={oembedAspectRatio}
				html={oembedInfo.html}
				resolvedLabel={resolvedLabel}
				slot="media-component"
			/>
			<slot />
		</Caption>
	)
) : videoInfo && videoSrc !== undefined && videoSrc !== '' ? (
	<Caption {...captionProps}>
		<Zoomer
			{...zoomerProps}
			height={videoInfo.height}
			slot="media-component"
			width={videoInfo.width}
		>
			<VideoPlayer
				{...sharedProps}
				captions={videoInfo.captions}
				element={videoElementMap[resolvedService]}
				hlsConfig={hlsConfig}
				hlsUrl={videoInfo.hlsUrl}
				mp4Url={videoInfo.mp4Url}
			/>
		</Zoomer>
		<slot />
	</Caption>
) : undefined}
