///|
pub(all) struct ScanConfig {
  format_version : Int
  options : ScanOptions
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) enum ScanConfigIssueKind {
  NegativeDepth
  InvalidEntryLimit
  InvalidConcurrency
  EmptyIgnorePattern
  DuplicateIgnorePattern
  AbsoluteIgnorePattern
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct ScanConfigIssue {
  kind : ScanConfigIssueKind
  value : String
  message : String
} derive(Eq, Debug, ToJson, FromJson)

///|
pub fn validate_scan_config(config : ScanConfig) -> Array[ScanConfigIssue] {
  let issues : Array[ScanConfigIssue] = []
  if config.options.max_depth < 0 {
    issues.push(ScanConfigIssue::{
      kind: NegativeDepth,
      value: config.options.max_depth.to_string(),
      message: "Maximum scan depth cannot be negative.",
    })
  }
  if config.options.max_entries < 1 {
    issues.push(ScanConfigIssue::{
      kind: InvalidEntryLimit,
      value: config.options.max_entries.to_string(),
      message: "Maximum entry count must be at least one.",
    })
  }
  if config.options.max_concurrency < 1 || config.options.max_concurrency > 256 {
    issues.push(ScanConfigIssue::{
      kind: InvalidConcurrency,
      value: config.options.max_concurrency.to_string(),
      message: "Scan concurrency must be between 1 and 256.",
    })
  }
  let patterns : Map[String, Bool] = Map([])
  for pattern in config.options.ignore_patterns {
    let normalized = normalize_relative_path(pattern)
    if pattern.trim().is_empty() {
      issues.push(ScanConfigIssue::{
        kind: EmptyIgnorePattern,
        value: pattern,
        message: "Ignore patterns cannot be empty.",
      })
    } else if pattern.has_prefix("/") ||
      pattern.has_prefix("\\") ||
      pattern.contains(":") {
      issues.push(ScanConfigIssue::{
        kind: AbsoluteIgnorePattern,
        value: pattern,
        message: "Ignore patterns must be relative to the scan root.",
      })
    } else if patterns.get(normalized) is Some(_) {
      issues.push(ScanConfigIssue::{
        kind: DuplicateIgnorePattern,
        value: pattern,
        message: "This normalized ignore pattern appears more than once.",
      })
    } else {
      patterns[normalized] = true
    }
  }
  issues
}

///|
pub fn default_scan_config() -> ScanConfig {
  ScanConfig::{ format_version: 1, options: ScanOptions::default(), }
}

///|
pub fn developer_scan_config() -> ScanConfig {
  ScanConfig::{
    format_version: 1,
    options: {
      ..ScanOptions::default(),
      max_entries: 2000000,
      max_concurrency: 24,
      ignore_patterns: [
        ".git", ".git/*", ".moondisk-quarantine", ".moondisk-quarantine/*",
      ],
    },
  }
}

///|
pub fn quick_scan_config() -> ScanConfig {
  ScanConfig::{
    format_version: 1,
    options: {
      ..ScanOptions::default(),
      include_hidden: false,
      max_depth: 8,
      max_entries: 250000,
      max_concurrency: 32,
      ignore_patterns: [
        ".git", ".git/*", "_build", "_build/*", "*/_build", "*/_build/*", "node_modules",
        "*/node_modules", "*/node_modules/*",
      ],
    },
  }
}

///|
pub fn scan_config_for_profile(profile : String) -> ScanConfig raise {
  match profile.to_lower() {
    "default" => default_scan_config()
    "developer" => developer_scan_config()
    "quick" => quick_scan_config()
    name => raise Failure("Unknown scan profile: " + name)
  }
}

///|
pub fn scan_config_to_json(
  config : ScanConfig,
  pretty? : Bool = true,
) -> String {
  let json = ToJson::to_json(config)
  if pretty {
    json.stringify(indent=2)
  } else {
    json.stringify()
  }
}

///|
pub fn scan_config_from_json(source : String) -> ScanConfig raise {
  let config : ScanConfig = @json.from_json(@json.parse(source))
  if config.format_version != 1 {
    raise Failure("Unsupported scan configuration version")
  }
  let issues = validate_scan_config(config)
  if !issues.is_empty() {
    raise Failure("Invalid scan configuration: " + issues[0].message)
  }
  config
}

///|
pub async fn write_scan_config(path : String, config : ScanConfig) -> Unit {
  @fs.write_file(path, scan_config_to_json(config))
}

///|
pub async fn read_scan_config(path : String) -> ScanConfig {
  scan_config_from_json(@fs.read_file(path).text())
}