use std::{
    collections::HashMap,
    env,
    path::{Component, Path, PathBuf},
    sync::{Arc, RwLock},
};

use anyhow::{Context, anyhow};
use lumen_engine::{
    audio::{AudioBuffer, AudioResolver, AudioSourceProvider},
    ffmpeg::{FfmpegAudioResolver, FfmpegResolverOptions, FfmpegVideoResolver},
    image::ImageFileResolver,
    media::{FontResolver, ImageResolver, MediaFrame, MediaStore, VideoFrameResolver},
};

pub const MEDIA_ROOT_ENV: &str = "LUMEN_MEDIA_ROOT";
pub const HARDWARE_DECODE_ENV: &str = "LUMEN_HARDWARE_DECODE";

pub(super) struct LocalMediaStore {
    root: PathBuf,
    font_root: Option<PathBuf>,
    video_options: FfmpegResolverOptions,
    audios: RwLock<HashMap<String, Arc<FfmpegAudioResolver>>>,
    images: RwLock<HashMap<String, Arc<ImageFileResolver>>>,
    videos: RwLock<HashMap<String, Arc<FfmpegVideoResolver>>>,
}

impl LocalMediaStore {
    pub(super) fn new(root: PathBuf) -> Self {
        let video_options = video_resolver_options_from_env();
        let font_root = env::var(MEDIA_ROOT_ENV)
            .ok()
            .and_then(|raw| PathBuf::from(raw).canonicalize().ok())
            .filter(|font_root| font_root != &root);
        tracing::info!(
            media_root = %root.display(),
            font_root = font_root.as_ref().map(|root| root.display().to_string()).as_deref().unwrap_or("<media-root>"),
            prefer_hardware_decode = video_options.prefer_hardware_decode,
            hardware_decode_env = env::var(HARDWARE_DECODE_ENV).ok().as_deref().unwrap_or("<unset>"),
            "created local media store"
        );
        Self {
            root,
            font_root,
            video_options,
            audios: RwLock::new(HashMap::new()),
            images: RwLock::new(HashMap::new()),
            videos: RwLock::new(HashMap::new()),
        }
    }

    fn resolve_source(&self, source: &str) -> Option<String> {
        if is_http_url(source) {
            return None;
        }

        resolve_local_path_with_root(source, &self.root)
            .ok()
            .map(|path| path.to_string_lossy().to_string())
    }

    fn resolve_font_family(&self, font_family: &str) -> Option<Vec<String>> {
        if font_family.trim().is_empty() {
            return None;
        }

        if let Some(direct) = self.resolve_font_source(font_family) {
            return Some(vec![direct]);
        }

        let file_stem = font_family.replace(['/', '\\'], "");
        let mut resolved = self.resolve_font_family_files(&file_stem);
        for candidate in [
            format!("fonts/{file_stem}.ttf"),
            format!("fonts/{file_stem}.otf"),
            format!("fonts/{file_stem}.ttc"),
            format!("fonts/{file_stem}.otc"),
            format!("fonts/{file_stem}-Regular.ttf"),
            format!("fonts/{file_stem}-Regular.otf"),
            format!("fonts/{file_stem}-Regular.ttc"),
            format!("fonts/{file_stem}-Regular.otc"),
        ] {
            if let Some(path) = self.resolve_font_source(&candidate)
                && !resolved.contains(&path)
            {
                resolved.push(path);
            }
        }
        (!resolved.is_empty()).then_some(resolved)
    }

    fn resolve_font_source(&self, source: &str) -> Option<String> {
        self.resolve_source(source).or_else(|| {
            self.font_root
                .as_ref()
                .and_then(|root| resolve_local_path_with_root(source, root).ok())
                .map(|path| path.to_string_lossy().to_string())
        })
    }

    fn resolve_font_family_files(&self, file_stem: &str) -> Vec<String> {
        let mut paths = [Some(&self.root), self.font_root.as_ref()]
            .into_iter()
            .flatten()
            .flat_map(|root| {
                let fonts_dir = root.join("fonts");
                std::fs::read_dir(fonts_dir)
                    .into_iter()
                    .flat_map(|entries| entries.filter_map(Result::ok))
                    .filter_map(|entry| {
                        let path = entry.path();
                        let extension = path.extension()?.to_str()?.to_ascii_lowercase();
                        if !matches!(extension.as_str(), "ttf" | "otf" | "ttc" | "otc") {
                            return None;
                        }
                        let stem = path.file_stem()?.to_str()?;
                        if stem == file_stem || stem.starts_with(&format!("{file_stem}-")) {
                            path.canonicalize()
                                .ok()
                                .map(|path| path.to_string_lossy().to_string())
                        } else {
                            None
                        }
                    })
                    .collect::<Vec<_>>()
            })
            .fold(Vec::new(), |mut paths, path| {
                if !paths.contains(&path) {
                    paths.push(path);
                }
                paths
            });
        paths.sort();
        paths
    }

    fn image_resolver(&self, source: &str) -> Option<Arc<ImageFileResolver>> {
        if let Ok(cache) = self.images.read()
            && let Some(resolver) = cache.get(source)
        {
            return Some(Arc::clone(resolver));
        }

        let resolver = Arc::new(ImageFileResolver::open(source.to_string()).ok()?);
        if let Ok(mut cache) = self.images.write() {
            cache
                .entry(source.to_string())
                .or_insert_with(|| Arc::clone(&resolver));
        }
        Some(resolver)
    }

    fn video_resolver(&self, source: &str) -> Option<Arc<FfmpegVideoResolver>> {
        if let Ok(cache) = self.videos.read()
            && let Some(resolver) = cache.get(source)
        {
            return Some(Arc::clone(resolver));
        }

        let resolver = Arc::new(
            FfmpegVideoResolver::open_with_options(source.to_string(), self.video_options).ok()?,
        );
        tracing::info!(
            source,
            width = resolver.metadata().width,
            height = resolver.metadata().height,
            frames = resolver.metadata().frame_count,
            fps = resolver.metadata().fps,
            decode_mode = resolver.decode_mode_label(),
            decode_unavailable_reason = resolver.decode_unavailable_reason(),
            "opened video media resolver"
        );
        if let Ok(mut cache) = self.videos.write() {
            cache
                .entry(source.to_string())
                .or_insert_with(|| Arc::clone(&resolver));
        }
        Some(resolver)
    }

    fn audio_resolver(&self, source: &str) -> Option<Arc<FfmpegAudioResolver>> {
        if let Ok(cache) = self.audios.read()
            && let Some(resolver) = cache.get(source)
        {
            return Some(Arc::clone(resolver));
        }

        let resolver = Arc::new(FfmpegAudioResolver::open(source.to_string()).ok()?);
        if let Ok(mut cache) = self.audios.write() {
            cache
                .entry(source.to_string())
                .or_insert_with(|| Arc::clone(&resolver));
        }
        Some(resolver)
    }
}

impl std::fmt::Debug for LocalMediaStore {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("LocalMediaStore")
            .field("root", &self.root)
            .field("video_options", &self.video_options)
            .finish_non_exhaustive()
    }
}

fn video_resolver_options_from_env() -> FfmpegResolverOptions {
    match env::var(HARDWARE_DECODE_ENV) {
        Ok(value) => FfmpegResolverOptions {
            prefer_hardware_decode: matches_env_flag(&value),
        },
        Err(_) => FfmpegResolverOptions::default(),
    }
}

fn matches_env_flag(value: &str) -> bool {
    matches!(
        value.trim().to_ascii_lowercase().as_str(),
        "1" | "true" | "yes" | "on"
    )
}

#[derive(Debug, Clone)]
struct SharedImageResolver(Arc<ImageFileResolver>);

impl ImageResolver for SharedImageResolver {
    fn id(&self) -> &str {
        self.0.id()
    }

    fn metadata(&self) -> lumen_engine::media::ImageMetadata {
        self.0.metadata()
    }

    fn frame(&self) -> Result<MediaFrame, lumen_engine::error::MediaError> {
        self.0.frame()
    }
}

#[derive(Clone)]
struct SharedVideoResolver(Arc<FfmpegVideoResolver>);

impl VideoFrameResolver for SharedVideoResolver {
    fn id(&self) -> &str {
        self.0.id()
    }

    fn metadata(&self) -> lumen_engine::media::VideoMetadata {
        self.0.metadata()
    }

    fn enqueue_frame(&self, frame: u32) -> Result<(), lumen_engine::error::MediaError> {
        self.0.enqueue_frame(frame)
    }

    fn frame(&self, frame: u32) -> Result<MediaFrame, lumen_engine::error::MediaError> {
        self.0.frame(frame)
    }

    fn retain_frames(&self, frames: &[u32]) {
        self.0.retain_frames(frames);
    }
}

#[derive(Clone)]
struct SharedAudioResolver(Arc<FfmpegAudioResolver>);

impl AudioResolver for SharedAudioResolver {
    fn id(&self) -> &str {
        self.0.id()
    }

    fn metadata(&self) -> lumen_engine::audio::AudioMetadata {
        self.0.metadata()
    }

    fn resolve_range(
        &self,
        start_sample: u64,
        frames: usize,
    ) -> Result<Arc<AudioBuffer>, lumen_engine::error::MediaError> {
        self.0.resolve_range(start_sample, frames)
    }
}

impl MediaStore for LocalMediaStore {
    fn get_image_resolver(&self, source: &str) -> Option<Box<dyn ImageResolver>> {
        let resolved = self.resolve_source(source)?;
        let resolver = self.image_resolver(&resolved)?;
        Some(Box::new(SharedImageResolver(resolver)))
    }

    fn get_video_resolver(&self, stream_id: &str) -> Option<Box<dyn VideoFrameResolver>> {
        let resolved = self.resolve_source(stream_id)?;
        let resolver = self.video_resolver(&resolved)?;
        Some(Box::new(SharedVideoResolver(resolver)))
    }

    fn get_font_resolver(&self, font_family: &str) -> Option<Box<dyn FontResolver>> {
        let paths = self.resolve_font_family(font_family)?;
        tracing::info!(
            font_family,
            sources = paths.len(),
            "opened font media resolver"
        );
        Some(Box::new(LocalFontResolver {
            id: font_family.to_string(),
            paths,
        }))
    }
}

#[derive(Debug)]
struct LocalFontResolver {
    id: String,
    paths: Vec<String>,
}

impl FontResolver for LocalFontResolver {
    fn id(&self) -> &str {
        &self.id
    }

    fn data(&self) -> Result<Vec<Vec<u8>>, lumen_engine::error::MediaError> {
        self.paths
            .iter()
            .map(|path| {
                std::fs::read(path).map_err(|err| lumen_engine::error::MediaError::Decode {
                    media_source: self.id.clone(),
                    details: format!("failed reading font data `{path}`: {err}"),
                })
            })
            .collect()
    }
}

impl AudioSourceProvider for LocalMediaStore {
    fn get_audio_resolver(&self, source_id: &str) -> Option<Box<dyn AudioResolver>> {
        let resolved = self.resolve_source(source_id)?;
        let resolver = self.audio_resolver(&resolved)?;
        Some(Box::new(SharedAudioResolver(resolver)))
    }
}

pub(super) fn media_root(override_root: Option<&Path>) -> anyhow::Result<PathBuf> {
    let path = match override_root {
        Some(path) => path.to_path_buf(),
        None => {
            let raw = env::var(MEDIA_ROOT_ENV).unwrap_or_else(|_| ".".to_string());
            PathBuf::from(raw)
        }
    };
    path.canonicalize()
        .with_context(|| format!("failed to resolve media root from {MEDIA_ROOT_ENV}"))
}

fn resolve_local_path_with_root(source: &str, root: &Path) -> anyhow::Result<PathBuf> {
    let root = root
        .canonicalize()
        .with_context(|| format!("failed to canonicalize media root `{}`", root.display()))?;

    if source.contains("://") && !source.starts_with("file://") {
        return Err(anyhow!("unsupported asset URI scheme for `{source}`"));
    }

    let raw_path = source.strip_prefix("file://").unwrap_or(source);
    let path = Path::new(raw_path);
    if path.as_os_str().is_empty() {
        return Err(anyhow!("asset path must not be empty"));
    }

    if path
        .components()
        .any(|component| matches!(component, Component::ParentDir))
    {
        return Err(anyhow!(
            "parent traversal is not allowed in asset paths: `{source}`"
        ));
    }

    let candidate = if path.is_absolute() {
        path.to_path_buf()
    } else {
        root.join(path)
    };

    let candidate = candidate.canonicalize().with_context(|| {
        format!(
            "failed to canonicalize asset path `{}`",
            candidate.display()
        )
    })?;

    if !candidate.starts_with(&root) {
        return Err(anyhow!(
            "asset path escapes allowed media root: `{}`",
            candidate.display()
        ));
    }

    Ok(candidate)
}

fn is_http_url(source: &str) -> bool {
    source.starts_with("http://") || source.starts_with("https://")
}
