use anyhow::{bail, Result};
use indoc::formatdoc;
use turbo_tasks::Vc;
use turbopack_binding::{
    turbo::{
        tasks::Value,
        tasks_fs::{json::parse_json_with_source_context, FileContent, FileSystemPath},
    },
    turbopack::core::{
        asset::AssetContent,
        resolve::{
            options::{ImportMapResult, ImportMapping, ImportMappingReplacement},
            parse::Request,
            ResolveResult,
        },
        virtual_source::VirtualSource,
    },
};

use self::{
    font_fallback::get_font_fallbacks,
    options::{options_from_request, FontDescriptors, NextFontLocalOptions},
    stylesheet::build_stylesheet,
    util::build_font_family_string,
};
use super::{font_fallback::FontFallbacks, util::FontCssProperties};
use crate::next_font::{
    local::options::FontWeight,
    util::{get_request_hash, get_request_id},
};

pub mod font_fallback;
pub mod options;
pub mod request;
pub mod stylesheet;
pub mod util;

#[turbo_tasks::value(shared)]
pub(crate) struct NextFontLocalReplacer {
    project_path: Vc<FileSystemPath>,
}

#[turbo_tasks::value_impl]
impl NextFontLocalReplacer {
    #[turbo_tasks::function]
    pub fn new(project_path: Vc<FileSystemPath>) -> Vc<Self> {
        Self::cell(NextFontLocalReplacer { project_path })
    }
}

#[turbo_tasks::value_impl]
impl ImportMappingReplacement for NextFontLocalReplacer {
    #[turbo_tasks::function]
    fn replace(&self, _capture: String) -> Vc<ImportMapping> {
        ImportMapping::Ignore.into()
    }

    /// Intercepts requests for `next/font/local/target.css` and returns a
    /// JavaScript object with a generated className from a referenced css
    /// module.
    #[turbo_tasks::function]
    async fn result(
        &self,
        context: Vc<FileSystemPath>,
        request: Vc<Request>,
    ) -> Result<Vc<ImportMapResult>> {
        let Request::Module {
            module: _,
            path: _,
            query: query_vc,
        } = &*request.await?
        else {
            return Ok(ImportMapResult::NoEntry.into());
        };

        let request_hash = get_request_hash(*query_vc);
        let options_vc = font_options_from_query_map(*query_vc);
        let font_fallbacks = get_font_fallbacks(context, options_vc, request_hash);
        let properties =
            &*get_font_css_properties(options_vc, font_fallbacks, request_hash).await?;
        let file_content = formatdoc!(
            r#"
                import cssModule from "@vercel/turbopack-next/internal/font/local/cssmodule.module.css?{}";
                const fontData = {{
                    className: cssModule.className,
                    style: {{
                        fontFamily: "{}",
                        {}{}
                    }},
                }};

                if (cssModule.variable != null) {{
                    fontData.variable = cssModule.variable;
                }}

                export default fontData;
                        "#,
            // Pass along whichever options we received to the css handler
            qstring::QString::from(&**query_vc.await?),
            properties.font_family.await?,
            properties
                .weight
                .await?
                .as_ref()
                .map(|w| format!("fontWeight: {},\n", w))
                .unwrap_or_else(|| "".to_owned()),
            properties
                .style
                .await?
                .as_ref()
                .map(|s| format!("fontStyle: \"{}\",\n", s))
                .unwrap_or_else(|| "".to_owned()),
        );
        let js_asset = VirtualSource::new(
            context.join(format!(
                "{}.js",
                get_request_id(options_vc.font_family(), request_hash).await?
            )),
            AssetContent::file(FileContent::Content(file_content.into()).into()),
        );

        Ok(ImportMapResult::Result(ResolveResult::source(Vc::upcast(js_asset)).into()).into())
    }
}

#[turbo_tasks::value(shared)]
pub struct NextFontLocalCssModuleReplacer {
    project_path: Vc<FileSystemPath>,
}

#[turbo_tasks::value_impl]
impl NextFontLocalCssModuleReplacer {
    #[turbo_tasks::function]
    pub fn new(project_path: Vc<FileSystemPath>) -> Vc<Self> {
        Self::cell(NextFontLocalCssModuleReplacer { project_path })
    }
}

#[turbo_tasks::value_impl]
impl ImportMappingReplacement for NextFontLocalCssModuleReplacer {
    #[turbo_tasks::function]
    fn replace(&self, _capture: String) -> Vc<ImportMapping> {
        ImportMapping::Ignore.into()
    }

    /// Intercepts requests for the css module made by the virtual JavaScript
    /// asset generated by the above replacer. Returns a VirtualSource of a CSS
    /// Module containing font face definitions and exporting class names for
    /// the font and an optional css variable.
    #[turbo_tasks::function]
    async fn result(
        &self,
        context: Vc<FileSystemPath>,
        request: Vc<Request>,
    ) -> Result<Vc<ImportMapResult>> {
        let request = &*request.await?;
        let Request::Module {
            module: _,
            path: _,
            query: query_vc,
        } = request
        else {
            return Ok(ImportMapResult::NoEntry.into());
        };

        let options = font_options_from_query_map(*query_vc);
        let request_hash = get_request_hash(*query_vc);
        let css_virtual_path = context.join(format!(
            "/{}.module.css",
            get_request_id(options.font_family(), request_hash).await?
        ));
        let fallback = get_font_fallbacks(context, options, request_hash);

        let stylesheet = build_stylesheet(
            font_options_from_query_map(*query_vc),
            fallback,
            get_font_css_properties(options, fallback, request_hash),
            get_request_hash(*query_vc),
        )
        .await?;

        let css_asset = VirtualSource::new(
            css_virtual_path,
            AssetContent::file(FileContent::Content(stylesheet.into()).into()),
        );

        Ok(ImportMapResult::Result(ResolveResult::source(Vc::upcast(css_asset)).into()).into())
    }
}

#[turbo_tasks::function]
async fn get_font_css_properties(
    options_vc: Vc<NextFontLocalOptions>,
    font_fallbacks: Vc<FontFallbacks>,
    request_hash: Vc<u32>,
) -> Result<Vc<FontCssProperties>> {
    let options = &*options_vc.await?;

    Ok(FontCssProperties::cell(FontCssProperties {
        font_family: build_font_family_string(options_vc, font_fallbacks, request_hash),
        weight: Vc::cell(match &options.fonts {
            FontDescriptors::Many(_) => None,
            // When the user only provided a top-level font file, include the font weight in the
            // className selector rules
            FontDescriptors::One(descriptor) => descriptor
                .weight
                .as_ref()
                // Don't include values for variable fonts. These are included in font-face
                // definitions only.
                .filter(|w| !matches!(w, FontWeight::Variable(_, _)))
                .map(|w| w.to_string()),
        }),
        style: Vc::cell(match &options.fonts {
            FontDescriptors::Many(_) => None,
            // When the user only provided a top-level font file, include the font style in the
            // className selector rules
            FontDescriptors::One(descriptor) => descriptor.style.clone(),
        }),
        variable: Vc::cell(options.variable.clone()),
    }))
}

#[turbo_tasks::function]
async fn font_options_from_query_map(query: Vc<String>) -> Result<Vc<NextFontLocalOptions>> {
    let query_map = qstring::QString::from(&**query.await?);

    if query_map.len() != 1 {
        bail!("next/font/local queries have exactly one entry");
    }

    let Some((json, _)) = query_map.into_iter().next() else {
        bail!("Expected one entry");
    };

    options_from_request(&parse_json_with_source_context(&json)?)
        .map(|o| NextFontLocalOptions::new(Value::new(o)))
}
