use std::{borrow::Cow, path::Path, sync::Arc};

use arcstr::ArcStr;
use itertools::Either;
use oxc::{transformer::EngineTargets, transformer_plugins::InjectGlobalVariablesConfig};
use rolldown_common::{
  AttachDebugInfo, GlobalsOutputOption, InjectImport, JsxOptions, JsxPreset, LegalComments,
  MinifyOptions, ModuleType, NormalizedBundlerOptions, OutputFormat, Platform,
  PreserveEntrySignatures, RawTransformOptions, TransformOptions, TreeshakeOptions, TsConfig,
  merge_transform_options_with_tsconfig, normalize_optimization_option,
};
use rolldown_error::{BuildDiagnostic, BuildResult, InvalidOptionType};
use rolldown_fs::{OsFileSystem, OxcResolverFileSystem as _};
use rolldown_resolver::Resolver;
use rolldown_utils::ecmascript::is_validate_identifier_name;
use rustc_hash::{FxHashMap, FxHashSet};

use crate::{SharedResolver, utils::determine_minify_internal_exports_default};

pub struct PrepareBuildContext {
  pub fs: OsFileSystem,
  pub resolver: SharedResolver,
  pub options: Arc<NormalizedBundlerOptions>,
  pub warnings: Vec<BuildDiagnostic>,
}

fn verify_raw_options(raw_options: &crate::BundlerOptions) -> BuildResult<Vec<BuildDiagnostic>> {
  let mut warnings: Vec<BuildDiagnostic> = Vec::new();
  let mut errors: Vec<BuildDiagnostic> = Vec::new();

  if raw_options.dir.is_some() && raw_options.file.is_some() {
    warnings.push(
      BuildDiagnostic::invalid_option(InvalidOptionType::InvalidOutputDirOption)
        .with_severity_warning(),
    );
  }

  if let Some(entity) = raw_options.context.as_ref() {
    if !is_validate_identifier_name(entity) {
      warnings.push(
        BuildDiagnostic::invalid_option(InvalidOptionType::InvalidContext(entity.clone()))
          .with_severity_warning(),
      );
    }
  }

  match raw_options.format {
    Some(format @ (OutputFormat::Umd | OutputFormat::Iife)) => {
      if matches!(raw_options.inline_dynamic_imports, Some(false)) {
        warnings.push(
          BuildDiagnostic::invalid_option(InvalidOptionType::UnsupportedInlineDynamicFormat(
            format.to_string(),
          ))
          .with_severity_warning(),
        );
      }
    }
    _ => {}
  }

  if let Some(advanced_chunks) = &raw_options.advanced_chunks {
    let has_groups = advanced_chunks.groups.as_ref().is_some_and(|groups| !groups.is_empty());

    if !has_groups {
      let mut specified_options = Vec::new();
      if advanced_chunks.min_share_count.is_some() {
        specified_options.push("minShareCount".to_string());
      }
      if advanced_chunks.min_size.is_some() {
        specified_options.push("minSize".to_string());
      }
      if advanced_chunks.max_size.is_some() {
        specified_options.push("maxSize".to_string());
      }
      if advanced_chunks.min_module_size.is_some() {
        specified_options.push("minModuleSize".to_string());
      }
      if advanced_chunks.max_module_size.is_some() {
        specified_options.push("maxModuleSize".to_string());
      }
      if advanced_chunks.include_dependencies_recursively.is_some() {
        specified_options.push("includeDependenciesRecursively".to_string());
      }

      if !specified_options.is_empty() {
        warnings.push(
          BuildDiagnostic::invalid_option(InvalidOptionType::AdvancedChunksWithoutGroups(
            specified_options,
          ))
          .with_severity_warning(),
        );
      }
    }

    // Check if `advancedChunks.include_dependencies_recursively` conflict with `preserveEntrySignatures`
    if matches!(advanced_chunks.include_dependencies_recursively, Some(false)) {
      if let Some(preserve_signatures) = &raw_options.preserve_entry_signatures {
        if matches!(
          preserve_signatures,
          PreserveEntrySignatures::Strict | PreserveEntrySignatures::ExportsOnly
        ) {
          errors.push(BuildDiagnostic::invalid_option(
            InvalidOptionType::IncludeDependenciesRecursivelyWithConflictPreserveEntrySignatures(
              preserve_signatures.to_string(),
            ),
          ));
        }
      }
    }
  }

  if errors.is_empty() { Ok(warnings) } else { Err(errors.into()) }
}

#[expect(clippy::too_many_lines)] // This function is long, but it's mostly just mapping values
pub fn prepare_build_context(
  mut raw_options: crate::BundlerOptions,
) -> BuildResult<PrepareBuildContext> {
  let mut warnings = verify_raw_options(&raw_options)?;

  let format = raw_options.format.unwrap_or(crate::OutputFormat::Esm);

  let preserve_entry_signatures = if let Some(advanced_chunks) = &raw_options.advanced_chunks
    && matches!(advanced_chunks.include_dependencies_recursively, Some(false))
    && raw_options.preserve_entry_signatures.is_none()
  {
    warnings.push(
      BuildDiagnostic::invalid_option(
        InvalidOptionType::IncludeDependenciesRecursivelyWithImplicitPreserveEntrySignatures,
      )
      .with_severity_warning(),
    );
    PreserveEntrySignatures::AllowExtension
  } else {
    raw_options.preserve_entry_signatures.unwrap_or_default()
  };

  let platform = raw_options.platform.unwrap_or(match format {
    OutputFormat::Cjs => Platform::Node,
    OutputFormat::Esm | OutputFormat::Iife | OutputFormat::Umd => Platform::Browser,
  });

  let raw_minify = raw_options.minify.unwrap_or_default();

  let mut raw_define = raw_options.define.unwrap_or_default();
  if matches!(platform, Platform::Browser) && !raw_define.contains_key("process.env.NODE_ENV") {
    if raw_minify.is_enabled() {
      raw_define.insert("process.env.NODE_ENV".to_string(), "'production'".to_string());
    } else {
      raw_define.insert("process.env.NODE_ENV".to_string(), "'development'".to_string());
    }
  }

  let define = raw_define.into_iter().collect();

  // Take out resolve options
  let mut raw_resolve = std::mem::take(&mut raw_options.resolve).unwrap_or_default();

  // https://github.com/evanw/esbuild/blob/ea453bf687c8e5cf3c5f11aae372c5ca33be0c98/pkg/api/api_impl.go#L1403-L1405
  // https://github.com/evanw/esbuild/commit/5abe0715f9be662b182989d2f38a44c7c8b28a2d
  if raw_resolve.condition_names.is_none() && matches!(platform, Platform::Browser | Platform::Node)
  {
    raw_resolve.condition_names = Some(vec!["module".to_string()]);
  }

  let mut module_types: FxHashMap<Cow<'static, str>, ModuleType> = FxHashMap::from(
    [
      ("js".into(), ModuleType::Js),
      ("mjs".into(), ModuleType::Js),
      ("cjs".into(), ModuleType::Js),
      ("jsx".into(), ModuleType::Jsx),
      ("ts".into(), ModuleType::Ts),
      ("mts".into(), ModuleType::Ts),
      ("cts".into(), ModuleType::Ts),
      ("tsx".into(), ModuleType::Tsx),
      ("json".into(), ModuleType::Json),
      ("txt".into(), ModuleType::Text),
      ("css".into(), ModuleType::Css),
    ]
    .into_iter()
    .collect(),
  );

  if let Some(user_defined_loaders) = raw_options.module_types {
    user_defined_loaders.into_iter().for_each(|(ext, value)| {
      let stripped = ext.strip_prefix('.').map(ToString::to_string).unwrap_or(ext);
      module_types.insert(Cow::Owned(stripped), value);
    });
  }

  let globals = raw_options.globals.unwrap_or(GlobalsOutputOption::FxHashMap(FxHashMap::default()));
  let generated_code = raw_options.generated_code.unwrap_or_default();

  let oxc_inject_global_variables_config = InjectGlobalVariablesConfig::new(
    raw_options
      .inject
      .as_ref()
      .map(|raw_injects| {
        raw_injects
          .iter()
          .map(|raw| match raw {
            InjectImport::Named { imported, alias, from } => {
              oxc::transformer_plugins::InjectImport::named_specifier(
                from,
                Some(imported),
                alias.as_deref().unwrap_or(imported),
              )
            }
            InjectImport::Namespace { alias, from } => {
              oxc::transformer_plugins::InjectImport::namespace_specifier(from, alias)
            }
          })
          .collect()
      })
      .unwrap_or_default(),
  );

  let mut experimental = raw_options.experimental.unwrap_or_default();
  if experimental.hmr.is_some() {
    experimental.incremental_build = Some(true);
  }

  if experimental.attach_debug_info.is_none() {
    experimental.attach_debug_info = Some(AttachDebugInfo::Simple);
  }

  let inline_dynamic_imports = match format {
    OutputFormat::Umd | OutputFormat::Iife => true,
    _ => raw_options.inline_dynamic_imports.unwrap_or(false),
  };

  // If the `file` is provided, use the parent directory of the file as the `out_dir`.
  // Otherwise, use the `dir` if provided, or default to `dist`.
  let out_dir = raw_options.file.as_ref().map_or_else(
    || raw_options.dir.clone().unwrap_or_else(|| "dist".to_string()),
    |file| {
      Path::new(file.as_str())
        .parent()
        .map(|parent| parent.to_string_lossy().to_string())
        .unwrap_or_default()
    },
  );
  let cwd =
    raw_options.cwd.unwrap_or_else(|| std::env::current_dir().expect("Failed to get current dir"));

  let mut raw_treeshake = raw_options.treeshake;
  if experimental.hmr.is_some() {
    // HMR requires treeshaking to be disabled
    raw_treeshake = TreeshakeOptions::Boolean(false);
  }

  let tsconfig = raw_options.tsconfig.clone().map(|tsconfig| tsconfig.with_base(&cwd));
  let fs = OsFileSystem::new(raw_resolve.yarn_pnp.is_some_and(|b| b));
  let resolver =
    Arc::new(Resolver::new(fs.clone(), cwd.clone(), platform, tsconfig.as_ref(), raw_resolve));

  let transform_options = {
    let mut raw_transform_options = raw_options.transform.unwrap_or_default();

    let target = match &raw_transform_options.target {
      Some(Either::Left(target)) => EngineTargets::from_target(target),
      Some(Either::Right(targets)) => EngineTargets::from_target_list(targets),
      None => Ok(EngineTargets::default()),
    }
    .map_err(|message| {
      let hint = message
        .contains("Invalid target")
        .then(|| "Rolldown only supports ES2015 (ES6) and later.".to_owned());
      BuildDiagnostic::bundler_initialize_error(message, hint)
    })?;

    let mut jsx_preset = JsxPreset::Enable;
    if let Some(Either::Left(jsx_str)) = &mut raw_transform_options.jsx {
      match jsx_str.as_str() {
        "react" => {
          raw_transform_options.jsx = Some(Either::Right(JsxOptions {
            runtime: Some(String::from("classic")),
            ..Default::default()
          }));
        }
        "react-jsx" => {
          raw_transform_options.jsx = Some(Either::Right(JsxOptions::default()));
        }
        // Keep JSX syntax as-is in the output (parser enabled, transformer disabled)
        "preserve" => jsx_preset = JsxPreset::Preserve,
        // Disable JSX parser and transformer entirely - will error if JSX syntax is encountered
        "disable" => {
          jsx_preset = JsxPreset::Disable;
          "preserve".clone_into(jsx_str);
        }
        _ => {
          Err(BuildDiagnostic::bundler_initialize_error(
            format!("Invalid jsx option: `{jsx_str}`."),
            Some(
              "Valid options are `false | 'react' | 'react-jsx' | 'preserve'`, or jsx options."
                .to_owned(),
            ),
          ))?;
        }
      }
    }

    // Create TransformOptions based on tsconfig mode:
    // - Auto: Create Raw mode (will resolve tsconfig per file)
    // - None/Manual: Create Normal mode (resolve tsconfig once now)
    match tsconfig {
      Some(TsConfig::Manual(path)) => {
        // Manual mode: Resolve tsconfig now and create Normal mode
        let resolved_tsconfig = resolver.resolve_tsconfig(&path).map_err(|err| {
          anyhow::anyhow!("Failed to resolve `tsconfig` option: {}", path.display()).context(err)
        })?;
        Box::new(TransformOptions::new(
          merge_transform_options_with_tsconfig(
            raw_transform_options,
            Some(&resolved_tsconfig),
            &mut warnings,
          )?,
          target,
          jsx_preset,
        ))
      }
      Some(TsConfig::Auto) => {
        // Auto mode: Create Raw mode TransformOptions
        // Each file will find its nearest tsconfig during compilation
        Box::new(TransformOptions::new_raw(
          RawTransformOptions::new(raw_transform_options),
          target,
          jsx_preset,
        ))
      }
      None => Box::new(TransformOptions::new(
        merge_transform_options_with_tsconfig(raw_transform_options, None, &mut warnings)?,
        target,
        jsx_preset,
      )),
    }
  };

  let mut normalized = NormalizedBundlerOptions {
    input: raw_options.input.unwrap_or_default(),
    external: raw_options.external.unwrap_or_default(),
    treeshake: raw_treeshake.into_normalized_options(),
    platform,
    name: raw_options.name,
    entry_filenames: raw_options.entry_filenames.unwrap_or_else(|| "[name].js".to_string().into()),
    chunk_filenames: raw_options
      .chunk_filenames
      .unwrap_or_else(|| "[name]-[hash].js".to_string().into()),
    asset_filenames: raw_options
      .asset_filenames
      .unwrap_or_else(|| "assets/[name]-[hash][extname]".to_string().into()),
    css_entry_filenames: raw_options
      .css_entry_filenames
      .unwrap_or_else(|| "[name].css".to_string().into()),
    css_chunk_filenames: raw_options
      .css_chunk_filenames
      .unwrap_or_else(|| "[name]-[hash].css".to_string().into()),
    sanitize_filename: raw_options.sanitize_filename.unwrap_or_default(),
    banner: raw_options.banner,
    footer: raw_options.footer,
    intro: raw_options.intro,
    outro: raw_options.outro,
    es_module: raw_options.es_module.unwrap_or_default(),
    dir: raw_options.dir,
    out_dir,
    file: raw_options.file,
    format,
    exports: raw_options.exports.unwrap_or(crate::OutputExports::Auto),
    hash_characters: raw_options.hash_characters.unwrap_or(crate::HashCharacters::Base64),
    globals,
    paths: raw_options.paths,
    generated_code,
    sourcemap: raw_options.sourcemap,
    sourcemap_base_url: raw_options.sourcemap_base_url,
    sourcemap_ignore_list: raw_options.sourcemap_ignore_list,
    sourcemap_path_transform: raw_options.sourcemap_path_transform,
    sourcemap_debug_ids: raw_options.sourcemap_debug_ids.unwrap_or(false),
    shim_missing_exports: raw_options.shim_missing_exports.unwrap_or(false),
    module_types,
    experimental,
    profiler_names: raw_options.profiler_names.unwrap_or(false),
    // Use placeholder for minify options at first
    minify: MinifyOptions::Disabled,
    define,
    inject: raw_options.inject.unwrap_or_default(),
    oxc_inject_global_variables_config,
    extend: raw_options.extend.unwrap_or(false),
    external_live_bindings: raw_options.external_live_bindings.unwrap_or(true),
    inline_dynamic_imports,
    advanced_chunks: raw_options.advanced_chunks,
    checks: raw_options.checks.unwrap_or_default().into(),
    watch: raw_options.watch.unwrap_or_default(),
    legal_comments: raw_options.legal_comments.unwrap_or(LegalComments::Inline),
    drop_labels: FxHashSet::from_iter(raw_options.drop_labels.unwrap_or_default()),
    keep_names: raw_options.keep_names.unwrap_or_default(),
    polyfill_require: raw_options.polyfill_require.unwrap_or(true),
    defer_sync_scan_data: raw_options.defer_sync_scan_data,
    transform_options,
    make_absolute_externals_relative: raw_options
      .make_absolute_externals_relative
      .unwrap_or_default(),
    invalidate_js_side_cache: raw_options.invalidate_js_side_cache,
    log_level: raw_options.log_level,
    on_log: raw_options.on_log,
    preserve_modules: raw_options.preserve_modules.unwrap_or_default(),
    virtual_dirname: raw_options
      .virtual_dirname
      .map(ArcStr::from)
      .unwrap_or_else(|| arcstr::literal!("_virtual")),
    preserve_modules_root: raw_options.preserve_modules_root.map(|preserve_modules_root| {
      let p = Path::new(&preserve_modules_root);
      if p.is_absolute() {
        preserve_modules_root
      } else {
        cwd.join(p).to_string_lossy().to_string()
      }
    }),
    cwd,
    preserve_entry_signatures,
    debug: raw_options.debug.is_some(),
    optimization: normalize_optimization_option(raw_options.optimization, platform),
    top_level_var: raw_options.top_level_var.unwrap_or(false),
    minify_internal_exports: raw_options
      .minify_internal_exports
      .unwrap_or_else(|| determine_minify_internal_exports_default(Some(format), &raw_minify)),
    clean_dir: raw_options.clean_dir.unwrap_or(false),
    context: raw_options.context.unwrap_or_default(),
  };

  normalized.minify = raw_minify.normalize(&normalized);

  Ok(PrepareBuildContext { fs, resolver, options: Arc::new(normalized), warnings })
}
