use std::hash::Hasher;
use std::path::Path;
use std::path::PathBuf;

use anyhow::Context;
use manganis_core::JsAssetOptions;
use swc_common::errors::Emitter;
use swc_common::errors::Handler;
use swc_common::input::SourceFileInput;
use swc_ecma_minifier::option::{ExtraOptions, MinifyOptions};
use swc_ecma_parser::lexer::Lexer;
use swc_ecma_parser::Parser;
use swc_ecma_transforms_base::fixer::fixer;
use swc_ecma_visit::VisitMutWith;

use std::collections::HashMap;

use anyhow::Error;
use swc_bundler::{Bundler, Config, Load, ModuleData, ModuleRecord};
use swc_common::{
    errors::HANDLER, sync::Lrc, FileName, FilePathMapping, Globals, Mark, SourceMap, Span, GLOBALS,
};
use swc_ecma_ast::*;
use swc_ecma_codegen::text_writer::JsWriter;
use swc_ecma_loader::{resolvers::node::NodeModulesResolver, TargetEnv};
use swc_ecma_parser::{parse_file_as_module, Syntax};

use crate::hash::hash_file_contents;

struct TracingEmitter;

impl Emitter for TracingEmitter {
    fn emit(&mut self, db: &swc_common::errors::DiagnosticBuilder<'_>) {
        match db.level {
            swc_common::errors::Level::Bug
            | swc_common::errors::Level::Fatal
            | swc_common::errors::Level::PhaseFatal
            | swc_common::errors::Level::Error => tracing::error!("{}", db.message()),
            swc_common::errors::Level::Warning
            | swc_common::errors::Level::FailureNote
            | swc_common::errors::Level::Cancelled => tracing::warn!("{}", db.message()),
            swc_common::errors::Level::Note | swc_common::errors::Level::Help => {
                tracing::trace!("{}", db.message())
            }
        }
    }
}

/// Run a closure with the swc globals and handler set up
fn inside_handler<O>(f: impl FnOnce(&Globals, Lrc<SourceMap>) -> O) -> O {
    let globals = Globals::new();
    let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
    let handler = Handler::with_emitter_and_flags(Box::new(TracingEmitter), Default::default());
    GLOBALS.set(&globals, || HANDLER.set(&handler, || f(&globals, cm)))
}

fn bundle_js_to_writer(
    file: PathBuf,
    bundle: bool,
    minify: bool,
    write_to: &mut impl std::io::Write,
) -> anyhow::Result<()> {
    inside_handler(|globals, cm| {
        bundle_js_to_writer_inside_handler(globals, cm, file, bundle, minify, write_to)
    })
}

fn resolve_js_inside_handler(
    globals: &Globals,
    file: PathBuf,
    bundle: bool,
    cm: &Lrc<SourceMap>,
) -> anyhow::Result<Module> {
    if bundle {
        let node_resolver = NodeModulesResolver::new(TargetEnv::Browser, Default::default(), true);
        let mut bundler = Bundler::new(
            globals,
            cm.clone(),
            PathLoader { cm: cm.clone() },
            node_resolver,
            Config {
                require: true,
                ..Default::default()
            },
            Box::new(Hook),
        );
        let mut entries = HashMap::default();
        entries.insert("main".to_string(), FileName::Real(file));

        let mut bundles = bundler
            .bundle(entries)
            .context("failed to bundle javascript with swc")?;
        // Since we only inserted one entry, there should only be one bundle in the output
        let bundle = bundles
            .pop()
            .ok_or_else(|| anyhow::anyhow!("swc did not output any bundles"))?;
        Ok(bundle.module)
    } else {
        let fm = cm.load_file(Path::new(&file)).expect("Failed to load file");

        let lexer = Lexer::new(
            Default::default(),
            Default::default(),
            SourceFileInput::from(&*fm),
            None,
        );
        let mut parser = Parser::new_from(lexer);

        parser.parse_module().map_err(|err| {
            HANDLER.with(|handler| {
                let mut error = err.into_diagnostic(handler);
                // swc errors panic on drop if you don't cancel them
                error.cancel();
                anyhow::anyhow!("{}", error.message())
            })
        })
    }
}

fn bundle_js_to_writer_inside_handler(
    globals: &Globals,
    cm: Lrc<SourceMap>,
    file: PathBuf,
    bundle: bool,
    minify: bool,
    write_to: &mut impl std::io::Write,
) -> anyhow::Result<()> {
    let mut module = resolve_js_inside_handler(globals, file, bundle, &cm)?;

    if minify {
        module = swc_ecma_minifier::optimize(
            std::mem::take(&mut module).into(),
            cm.clone(),
            None,
            None,
            &MinifyOptions {
                rename: true,
                compress: None,
                mangle: None,
                ..Default::default()
            },
            &ExtraOptions {
                unresolved_mark: Mark::new(),
                top_level_mark: Mark::new(),
                mangle_name_cache: None,
            },
        )
        .expect_module();
        module.visit_mut_with(&mut fixer(None));
    }

    let mut emitter = swc_ecma_codegen::Emitter {
        cfg: swc_ecma_codegen::Config::default().with_minify(minify),
        cm: cm.clone(),
        comments: None,
        wr: Box::new(JsWriter::new(cm, "\n", write_to, None)),
    };

    emitter.emit_module(&module)?;

    Ok(())
}

struct PathLoader {
    cm: Lrc<SourceMap>,
}

impl Load for PathLoader {
    fn load(&self, file: &FileName) -> anyhow::Result<ModuleData> {
        let file = match file {
            FileName::Real(v) => v,
            _ => anyhow::bail!("Only real files are supported"),
        };

        let fm = self.cm.load_file(file)?;

        let module = HANDLER.with(|handler| {
            parse_file_as_module(
                &fm,
                Syntax::Es(Default::default()),
                Default::default(),
                None,
                &mut Vec::new(),
            )
            .map_err(|err| {
                let mut error = err.into_diagnostic(handler);
                // swc errors panic on drop if you don't cancel them
                error.cancel();
                anyhow::anyhow!("{}", error.message())
            })
            .context("Failed to parse javascript")
        })?;

        Ok(ModuleData {
            fm,
            module,
            helpers: Default::default(),
        })
    }
}

// Adapted from https://github.com/swc-project/swc/blob/624680b7896cef9d8e30bd5ff910538298016974/bindings/binding_core_node/src/bundle.rs#L266-L302
struct Hook;

impl swc_bundler::Hook for Hook {
    fn get_import_meta_props(
        &self,
        span: Span,
        module_record: &ModuleRecord,
    ) -> Result<Vec<KeyValueProp>, Error> {
        let file_name = module_record.file_name.to_string();

        Ok(vec![
            KeyValueProp {
                key: PropName::Ident(IdentName::new("url".into(), span)),
                value: Box::new(Expr::Lit(Lit::Str(Str {
                    span,
                    raw: None,
                    value: file_name.into(),
                }))),
            },
            KeyValueProp {
                key: PropName::Ident(IdentName::new("main".into(), span)),
                value: Box::new(if module_record.is_entry {
                    Expr::Member(MemberExpr {
                        span,
                        obj: Box::new(Expr::MetaProp(MetaPropExpr {
                            span,
                            kind: MetaPropKind::ImportMeta,
                        })),
                        prop: MemberProp::Ident(IdentName::new("main".into(), span)),
                    })
                } else {
                    Expr::Lit(Lit::Bool(Bool { span, value: false }))
                }),
            },
        ])
    }
}

pub(crate) fn process_js(
    js_options: &JsAssetOptions,
    source: &Path,
    output_path: &Path,
    bundle: bool,
) -> anyhow::Result<()> {
    let mut writer = std::io::BufWriter::new(std::fs::File::create(output_path)?);
    if js_options.minified() {
        if let Err(err) = bundle_js_to_writer(source.to_path_buf(), bundle, true, &mut writer) {
            tracing::error!("Failed to minify js. Falling back to non-minified: {err}");
        } else {
            return Ok(());
        }
    }
    let mut source_file = std::fs::File::open(source)?;
    std::io::copy(&mut source_file, &mut writer).with_context(|| {
        format!(
            "Failed to write js to output location: {}",
            output_path.display()
        )
    })?;

    Ok(())
}

fn hash_js_module(file: PathBuf, hasher: &mut impl Hasher, bundle: bool) -> anyhow::Result<()> {
    inside_handler(|globals, cm| {
        _ = resolve_js_inside_handler(globals, file, bundle, &cm)?;

        for file in cm.files().iter() {
            let hash = file.src_hash;
            hasher.write(&hash.to_le_bytes());
        }

        Ok(())
    })
}

pub(crate) fn hash_js(
    js_options: &JsAssetOptions,
    source: &Path,
    hasher: &mut impl Hasher,
    bundle: bool,
) -> anyhow::Result<()> {
    if js_options.minified() {
        if let Err(err) = hash_js_module(source.to_path_buf(), hasher, bundle) {
            tracing::error!("Failed to minify js. Falling back to non-minified: {err}");
            hash_file_contents(source, hasher)?;
        }
    } else {
        hash_file_contents(source, hasher)?;
    }

    Ok(())
}
