use std::borrow::Cow;

use nodejs_built_in_modules::is_nodejs_builtin_module;
use rolldown_common::{ImportKind, OutputFormat, ResolvedExternal};
use rolldown_plugin::{HookLoadOutput, HookResolveIdOutput, HookUsage, Plugin};
use rolldown_utils::{concat_string, pattern_filter::StringOrRegex, rustc_hash::FxHashSetExt as _};
use rustc_hash::FxHashSet;

const CJS_EXTERNAL_FACADE_PREFIX: &str = "builtin:esm-external-require-";

#[derive(Debug, Default)]
pub struct EsmExternalRequirePlugin {
  pub external: Vec<StringOrRegex>,
  pub skip_duplicate_check: bool,
}

impl Plugin for EsmExternalRequirePlugin {
  fn name(&self) -> Cow<'static, str> {
    Cow::Borrowed("builtin:esm-external-require")
  }

  fn register_hook_usage(&self) -> HookUsage {
    if self.external.is_empty() {
      HookUsage::empty()
    } else if self.skip_duplicate_check {
      HookUsage::ResolveId | HookUsage::Load
    } else {
      HookUsage::BuildStart | HookUsage::ResolveId | HookUsage::Load
    }
  }

  async fn build_start(
    &self,
    ctx: &rolldown_plugin::PluginContext,
    args: &rolldown_plugin::HookBuildStartArgs<'_>,
  ) -> rolldown_plugin::HookNoopReturn {
    if let rolldown_common::IsExternal::StringOrRegex(ref option_externals) = args.options.external
    {
      #[derive(PartialEq, Eq, Hash)]
      enum StringOrRegexPattern<'a> {
        String(&'a str),
        Regex(&'a str),
      }

      let mut externals = FxHashSet::with_capacity(option_externals.len());
      for external in option_externals {
        match external {
          StringOrRegex::String(s) => {
            externals.insert(StringOrRegexPattern::String(s.as_str()));
          }
          StringOrRegex::Regex(r) => {
            if let Some(pattern) = r.regex_pattern() {
              externals.insert(StringOrRegexPattern::Regex(pattern));
            }
          }
        }
      }

      let mut duplicates = Vec::with_capacity(self.external.len().min(option_externals.len()));
      for plugin_external in &self.external {
        match plugin_external {
          StringOrRegex::String(s) => {
            if externals.contains(&StringOrRegexPattern::String(s)) {
              duplicates.push(s.as_str());
            }
          }
          StringOrRegex::Regex(r) => {
            if let Some(pattern) = r.regex_pattern()
              && externals.contains(&StringOrRegexPattern::Regex(pattern))
            {
              duplicates.push(pattern);
            }
          }
        }
      }

      if !duplicates.is_empty() {
        ctx.warn(rolldown_plugin::LogWithoutPlugin {
          message: format!(
            "Found {} duplicate external: `{}`. Remove them from top-level `external` as they're already handled by `{}` plugin. To disable this check, set `skipDuplicateCheck: true`.",
            duplicates.len(),
            duplicates.join("`, `"),
            self.name()
          ),
          ..Default::default()
        });
      }
    }

    Ok(())
  }

  async fn resolve_id(
    &self,
    ctx: &rolldown_plugin::PluginContext,
    args: &rolldown_plugin::HookResolveIdArgs<'_>,
  ) -> rolldown_plugin::HookResolveIdReturn {
    if args.importer.is_some_and(|importer| importer.starts_with(CJS_EXTERNAL_FACADE_PREFIX)) {
      return Ok(Some(HookResolveIdOutput {
        id: args.specifier.into(),
        external: Some(ResolvedExternal::Bool(true)),
        ..Default::default()
      }));
    }

    let is_external = self.external.iter().any(|v| match v {
      StringOrRegex::String(string) => string == args.specifier,
      StringOrRegex::Regex(regex) => regex.matches(args.specifier),
    });

    if is_external {
      if matches!(ctx.options().format, OutputFormat::Cjs) || args.kind != ImportKind::Require {
        return Ok(Some(HookResolveIdOutput {
          id: args.specifier.into(),
          external: Some(ResolvedExternal::Bool(true)),
          ..Default::default()
        }));
      }

      return Ok(Some(HookResolveIdOutput {
        id: concat_string!(CJS_EXTERNAL_FACADE_PREFIX, args.specifier).into(),
        ..Default::default()
      }));
    }

    Ok(None)
  }

  fn resolve_id_meta(&self) -> Option<rolldown_plugin::PluginHookMeta> {
    Some(rolldown_plugin::PluginHookMeta { order: Some(rolldown_plugin::PluginOrder::Pre) })
  }

  async fn load(
    &self,
    _ctx: rolldown_plugin::SharedLoadPluginContext,
    args: &rolldown_plugin::HookLoadArgs<'_>,
  ) -> rolldown_plugin::HookLoadReturn {
    Ok(args.id.strip_prefix(CJS_EXTERNAL_FACADE_PREFIX).map(|module_id| {
      let code = concat_string!(
        "import * as m from '",
        module_id,
        "';module.exports = ",
        if is_nodejs_builtin_module(module_id) { "m.default" } else { "{ ...m }" },
        ";"
      );
      HookLoadOutput { code: code.into(), ..Default::default() }
    }))
  }
}
