use std::path::Path;

use arcstr::ArcStr;
use oxc::diagnostics::OxcDiagnostic;
use oxc::{diagnostics::LabeledSpan, span::Span};
use oxc_resolver::ResolveError;

use crate::types::event_kind::EventKind;
use crate::utils::ByteLocator;

#[cfg(feature = "napi")]
use super::events::napi_error::NapiError;

use super::BuildDiagnostic;
use super::Severity;
use super::events::DiagnosableArcstr;
use super::events::already_closed::AlreadyClosed;
use super::events::assign_to_import::AssignToImport;
use super::events::bundler_initialize_error::BundlerInitializeError;
use super::events::cannot_call_namespace::CannotCallNamespace;
use super::events::configuration_field_conflict::ConfigurationFieldConflict;
use super::events::could_not_clean_directory::CouldNotCleanDirectory;
use super::events::duplicate_shebang::DuplicateShebang;
use super::events::filename_conflict::FilenameConflict;
use super::events::filename_outside_output_directory::FilenameOutsideOutputDirectory;
use super::events::illegal_identifier_as_name::IllegalIdentifierAsName;
use super::events::import_is_undefined::ImportIsUndefined;
use super::events::invalid_annotation::InvalidAnnotation;
use super::events::invalid_define_config::InvalidDefineConfig;
use super::events::invalid_option::{InvalidOption, InvalidOptionType};
use super::events::json_parse::JsonParse;
use super::events::missing_global_name::MissingGlobalName;
use super::events::missing_name_option_for_iife_export::MissingNameOptionForIifeExport;
use super::events::plugin_error::{CausedPlugin, PluginError};
use super::events::plugin_timings::{PluginTimingInfo, PluginTimings};
use super::events::prefer_builtin_feature::PreferBuiltinFeature;
use super::events::require_tla::RequireTla;
use super::events::resolve_error::DiagnosableResolveError;

use super::events::tsconfig_error::TsConfigError;
use super::events::unhandleable_error::UnhandleableError;
use super::events::unloadable_dependency::{UnloadableDependency, UnloadableDependencyContext};
use super::events::unsupported_feature::UnsupportedFeature;
use super::events::unsupported_tsconfig_option::UnsupportedTsconfigOption;
use super::events::untranspiled_syntax::UntranspiledSyntax;
use super::events::{
  ambiguous_external_namespace::{AmbiguousExternalNamespace, AmbiguousExternalNamespaceModule},
  circular_dependency::CircularDependency,
  circular_reexport::CircularReexport,
  commonjs_variable_in_esm::{CjsExportSpan, CommonJsVariableInEsm},
  eval::Eval,
  external_entry::ExternalEntry,
  forbid_const_assign::ForbidConstAssign,
  invalid_export_option::InvalidExportOption,
  missing_export::MissingExport,
  mixed_exports::MixedExports,
  oxc_error::OxcError,
  unresolved_entry::UnresolvedEntry,
};

impl BuildDiagnostic {
  // --- Rollup related
  pub fn entry_cannot_be_external(unresolved_id: impl AsRef<Path>) -> Self {
    Self::new_inner(ExternalEntry { id: unresolved_id.as_ref().to_path_buf() })
  }

  pub fn ambiguous_external_namespace(
    ambiguous_export_name: String,
    importee: String,
    importer: AmbiguousExternalNamespaceModule,
    exporter: Vec<AmbiguousExternalNamespaceModule>,
  ) -> Self {
    Self::new_inner(AmbiguousExternalNamespace {
      ambiguous_export_name,
      importee,
      importer,
      exporter,
    })
  }

  pub fn unresolved_entry(
    unresolved_id: impl AsRef<Path>,
    resolve_error: Option<ResolveError>,
  ) -> Self {
    Self::new_inner(UnresolvedEntry {
      unresolved_id: unresolved_id.as_ref().to_path_buf(),
      resolve_error,
    })
  }

  pub fn invalid_annotation(
    module_id: String,
    annotation: String,
    source: ArcStr,
    span: oxc::span::Span,
  ) -> Self {
    Self::new_inner(InvalidAnnotation { module_id, annotation, source, span })
  }

  pub fn resolve_error(
    source: ArcStr,
    importer_id: ArcStr,
    importee: DiagnosableArcstr,
    reason: String,
    diagnostic_kind: crate::types::event_kind::EventKind,
    help: Option<String>,
  ) -> Self {
    Self::new_inner(DiagnosableResolveError {
      source,
      importer_id,
      importee,
      reason,
      help,
      import_chain: None,
      diagnostic_kind,
    })
  }

  pub fn unloadable_dependency(
    resolved: ArcStr,
    context: Option<UnloadableDependencyContext>,
    reason: ArcStr,
  ) -> Self {
    Self::new_inner(UnloadableDependency { reason, resolved, context })
  }

  pub fn circular_dependency(paths: Vec<String>) -> Self {
    Self::new_inner(CircularDependency { paths })
  }

  pub fn circular_reexport(importer_id: String, imported_specifier: String) -> Self {
    Self::new_inner(CircularReexport { importer_id, imported_specifier })
  }

  #[expect(clippy::too_many_arguments)]
  pub fn missing_export(
    importer: String,
    stable_importer: String,
    importee: String,
    stable_importee: String,
    importer_source: ArcStr,
    imported_specifier: String,
    imported_specifier_span: Span,
    note: Option<String>,
  ) -> Self {
    Self::new_inner(MissingExport {
      importer,
      stable_importer,
      importee,
      stable_importee,
      importer_source,
      imported_specifier,
      imported_specifier_span,
      note,
    })
  }

  pub fn mixed_export(
    module_id: String,
    module_name: ArcStr,
    entry_module: String,
    export_keys: Vec<ArcStr>,
  ) -> Self {
    Self::new_inner(MixedExports { module_id, module_name, entry_module, export_keys })
  }

  pub fn missing_global_name(module_id: String, module_name: ArcStr, guessed_name: ArcStr) -> Self {
    Self::new_inner(MissingGlobalName { module_id, module_name, guessed_name })
  }

  pub fn missing_name_option_for_iife_export(is_umd: bool) -> Self {
    Self::new_inner(MissingNameOptionForIifeExport { is_umd })
  }

  pub fn illegal_identifier_as_name(identifier_name: ArcStr) -> Self {
    Self::new_inner(IllegalIdentifierAsName { identifier_name })
  }

  pub fn invalid_export_option(
    export_mode: ArcStr,
    entry_module: ArcStr,
    export_keys: Vec<ArcStr>,
  ) -> Self {
    Self::new_inner(InvalidExportOption { export_mode, entry_module, export_keys })
  }

  pub fn filename_conflict(filename: ArcStr) -> Self {
    Self::new_inner(FilenameConflict { filename })
  }

  pub fn filename_outside_output_directory(filename: String) -> Self {
    Self::new_inner(FilenameOutsideOutputDirectory { filename })
  }

  // Esbuild
  pub fn commonjs_variable_in_esm(
    filename: String,
    source: ArcStr,
    esm_export_span: Span,
    cjs_export_ident_span: CjsExportSpan,
  ) -> Self {
    Self::new_inner(CommonJsVariableInEsm {
      filename,
      source,
      esm_export_span,
      cjs_export_ident_span,
    })
  }

  pub fn import_is_undefined(
    filename: ArcStr,
    source: ArcStr,
    span: Span,
    name: ArcStr,
    stable_importer: String,
  ) -> Self {
    Self::new_inner(ImportIsUndefined { filename, source, span, name, stable_importer })
  }

  pub fn unsupported_feature(
    filename: ArcStr,
    source: ArcStr,
    span: Span,
    error_message: String,
  ) -> Self {
    Self::new_inner(UnsupportedFeature { source, filename, span, error_message })
  }

  pub fn empty_import_meta(
    filename: String,
    source: ArcStr,
    span: Span,
    format: ArcStr,
    is_import_meta_url: bool,
  ) -> Self {
    Self::new_inner(super::events::empty_import_meta::EmptyImportMeta {
      filename,
      source,
      span,
      format,
      is_import_meta_url,
    })
  }

  // --- Rolldown related

  pub fn require_tla(inner: RequireTla) -> Self {
    Self::new_inner(inner)
  }

  pub fn oxc_error(
    source: ArcStr,
    id: String,
    error_help: String,
    error_message: String,
    error_labels: Vec<LabeledSpan>,
    event_kind: EventKind,
  ) -> Self {
    Self::new_inner(OxcError { source, id, error_help, error_message, error_labels, event_kind })
  }

  pub fn from_oxc_diagnostics<T>(
    diagnostics: T,
    source: &ArcStr,
    id: &str,
    severity: Severity,
    event_kind: EventKind,
  ) -> Vec<Self>
  where
    T: IntoIterator<Item = OxcDiagnostic>,
  {
    diagnostics
      .into_iter()
      .map(|mut error| {
        let diagnostic = BuildDiagnostic::oxc_error(
          source.clone(),
          id.to_string(),
          error.help.take().unwrap_or_default().into(),
          error.message.to_string(),
          error.labels.take().unwrap_or_default(),
          event_kind,
        );
        if matches!(severity, Severity::Warning) {
          diagnostic.with_severity_warning()
        } else {
          diagnostic
        }
      })
      .collect::<Vec<_>>()
  }
  pub fn forbid_const_assign(
    filename: String,
    source: ArcStr,
    name: String,
    reference_span: Span,
    re_assign_span: Span,
  ) -> Self {
    Self::new_inner(ForbidConstAssign { filename, source, name, reference_span, re_assign_span })
  }

  pub fn invalid_option(invalid_option_type: InvalidOptionType) -> Self {
    Self::new_inner(InvalidOption { invalid_option_type })
  }

  #[cfg(feature = "napi")]
  pub fn napi_error(err: napi::Error) -> Self {
    Self::new_inner(NapiError(err))
  }

  pub fn eval(filename: String, source: ArcStr, span: Span) -> Self {
    Self::new_inner(Eval { span, source, filename })
  }

  pub fn configuration_field_conflict(
    a_config_name: &str,
    a_field_name: &str,
    b_config_name: &str,
    b_field_name: &str,
  ) -> Self {
    Self::new_inner(ConfigurationFieldConflict {
      a_field: a_field_name.to_string(),
      a_config_name: a_config_name.to_string(),
      b_field: b_field_name.to_string(),
      b_config_name: b_config_name.to_string(),
    })
  }

  pub fn assign_to_import(
    filename: ArcStr,
    source: ArcStr,
    span: Span,
    name: ArcStr,
    import_decl_span: Option<Span>,
    imported_name: Option<ArcStr>,
  ) -> Self {
    Self::new_inner(AssignToImport {
      filename,
      source,
      span,
      name,
      import_decl_span,
      imported_name,
    })
  }

  pub fn cannot_call_namespace(
    filename: ArcStr,
    source: ArcStr,
    span: Span,
    name: ArcStr,
    declaration_span: Span,
  ) -> Self {
    Self::new_inner(CannotCallNamespace { filename, source, span, name, declaration_span })
  }

  pub fn prefer_builtin_feature(
    builtin_feature: Option<String>,
    plugin_name: String,
    additional_message: Option<&'static str>,
  ) -> Self {
    Self::new_inner(PreferBuiltinFeature { builtin_feature, plugin_name, additional_message })
  }

  #[expect(clippy::cast_possible_truncation)]
  pub fn json_parse(
    filename: ArcStr,
    source: ArcStr,
    line: usize,
    column: usize,
    message: ArcStr,
  ) -> Self {
    // This function expects 0-based `line` and `column` values, matching `ByteLocator::byte_offset`.
    // Note: `serde_json::Error` reports 1-based line/column; conversion to 0-based is handled at the call site.
    let offset = ByteLocator::new(source.as_str()).byte_offset(line, column);
    let span = Span::new(offset as u32, offset as u32);
    Self::new_inner(JsonParse { filename, source, span, message })
  }

  pub fn invalid_define_config(message: String) -> Self {
    Self::new_inner(InvalidDefineConfig { message })
  }

  pub fn already_closed() -> Self {
    Self::new_inner(AlreadyClosed {})
  }

  pub fn unhandleable_error(err: anyhow::Error) -> Self {
    Self::new_inner(UnhandleableError(err))
  }

  pub fn untranspiled_syntax(filename: String, syntax_kind: &'static str) -> Self {
    Self::new_inner(UntranspiledSyntax { filename, syntax_kind })
  }

  pub fn bundler_initialize_error(message: String, hint: Option<String>) -> Self {
    Self::new_inner(BundlerInitializeError { message, hint })
  }

  pub fn plugin_error(caused_plugin: CausedPlugin, err: anyhow::Error) -> Self {
    Self::new_inner(PluginError { plugin: caused_plugin, error: err })
  }

  pub fn could_not_clean_directory(dir: String, reason: String) -> Self {
    Self::new_inner(CouldNotCleanDirectory { dir, reason })
  }

  pub fn plugin_timings(plugins: Vec<PluginTimingInfo>) -> Self {
    Self::new_inner(PluginTimings { plugins })
  }

  pub fn duplicate_shebang(filename: String, source: &str) -> Self {
    Self::new_inner(DuplicateShebang { filename, source: source.to_string() })
  }

  pub fn tsconfig_error(file_path: String, reason: ResolveError) -> Self {
    Self::new_inner(TsConfigError { file_paths: vec![file_path], reason })
  }

  pub fn unsupported_tsconfig_option(message: String) -> Self {
    Self::new_inner(UnsupportedTsconfigOption { message })
  }

  pub fn runtime_module_symbol_not_found(
    symbol_names: Vec<String>,
    modified_by_plugins: Vec<String>,
  ) -> Self {
    Self::new_inner(super::events::runtime_module_symbol_not_found::RuntimeModuleSymbolNotFound {
      symbol_names,
      modified_by_plugins,
    })
  }

  pub fn ineffective_dynamic_import(
    module_id: String,
    mut static_importers: Vec<String>,
    mut dynamic_importers: Vec<String>,
  ) -> Self {
    static_importers.sort();
    dynamic_importers.sort();
    Self::new_inner(super::events::ineffective_dynamic_import::IneffectiveDynamicImport {
      module_id,
      static_importers,
      dynamic_importers,
    })
  }

  pub fn large_barrel_modules(module_id: String, reexport_count: usize) -> Self {
    Self::new_inner(super::events::large_barrel_modules::LargeBarrelModules {
      module_id,
      reexport_count,
    })
  }
}
