///|
pub(all) enum Style {
  Xml
  Markdown
} derive(Eq, Debug)

///|
pub(all) enum Action {
  Pack
  Init
  Help
  Version
} derive(Eq, Debug)

///|
/// Absent scalars stay None; lists are additive, not replacement values.
pub(all) struct Patch {
  mut output : String?
  mut style : Style?
  mut include_patterns : Array[String]
  mut ignore : Array[String]
  mut use_gitignore : Bool?
  mut use_dot_ignore : Bool?
  mut use_default_patterns : Bool?
  mut security_check : Bool?
  mut encoding : String?
  mut token_budget : Int32?
  mut max_file_size : Int32?
  mut tokenizer_path : String?
} derive(Debug)

///|
pub fn Patch::empty() -> Patch {
  {
    output: None,
    style: None,
    include_patterns: [],
    ignore: [],
    use_gitignore: None,
    use_dot_ignore: None,
    use_default_patterns: None,
    security_check: None,
    encoding: None,
    token_budget: None,
    max_file_size: None,
    tokenizer_path: None,
  }
}

///|
/// All paths are unmodified user paths; this package performs no I/O.
pub(all) struct Cli {
  directory : String
  config_path : String?
  action : Action
  stdout : Bool
  quiet : Bool
  verbose : Bool
  patch : Patch
} derive(Debug)

///|
pub(all) struct Options {
  directory : String
  output : String
  stdout : Bool
  style : Style
  include_patterns : Array[String]
  ignore : Array[String]
  use_gitignore : Bool
  use_dot_ignore : Bool
  use_default_patterns : Bool
  security_check : Bool
  encoding : String
  token_budget : Int32?
  max_file_size : Int32
  tokenizer_path : String?
  quiet : Bool
  verbose : Bool
} derive(Debug)

///|
/// Error payloads contain field/option names, never source JSON or values.
pub(all) suberror ConfigError {
  InvalidJson
  UnknownField(String)
  InvalidValue(String)
  UnknownOption(String)
  MissingValue(String)
  Conflict(String)
  MultipleDirectories
} derive(Eq, Debug)

///|
pub extend Style with Eq::{equal, not_equal}

///|
pub extend Style with Debug::{to_repr}

///|
pub extend Action with Eq::{equal, not_equal}

///|
pub extend Action with Debug::{to_repr}

///|
pub extend Patch with Debug::{to_repr}

///|
pub extend Cli with Debug::{to_repr}

///|
pub extend Options with Debug::{to_repr}

///|
pub extend ConfigError with Eq::{equal, not_equal}

///|
pub extend ConfigError with Debug::{to_repr}

///|
fn parse_style(value : String) -> Style raise ConfigError {
  match value {
    "xml" => Xml
    "markdown" => Markdown
    _ => raise InvalidValue("output.style")
  }
}

///|
fn validate_pattern(pattern : String) -> Unit raise ConfigError {
  if pattern.is_empty() ||
    pattern.has_prefix("!") ||
    pattern.has_prefix("/") ||
    pattern.contains("\\") ||
    pattern.contains(":") ||
    pattern.contains("{") ||
    pattern.contains("}") {
    raise InvalidValue("glob")
  }
  for part in pattern.split("/") {
    if part == ".." {
      raise InvalidValue("glob")
    }
  }
}

///|
fn validate_patch(patch : Patch) -> Unit raise ConfigError {
  for p in patch.include_patterns {
    validate_pattern(p)
  }
  for p in patch.ignore {
    validate_pattern(p)
  }
  if patch.encoding is Some(value) && value != "o200k_base" {
    raise InvalidValue("tokenCount.encoding")
  }
  if patch.token_budget is Some(value) && value <= 0 {
    raise InvalidValue("output.tokenBudget")
  }
  if patch.max_file_size is Some(value) && value <= 0 {
    raise InvalidValue("input.maxFileSize")
  }
  if patch.output is Some(value) {
    validate_path(value, "output.filePath")
  }
  if patch.tokenizer_path is Some(value) {
    validate_path(value, "tokenizerPath")
  }
}

///|
fn validate_path(value : String, field : String) -> Unit raise ConfigError {
  if value.is_empty() || value.contains("\u0000") {
    raise InvalidValue(field)
  }
}

///|
fn append_unique(
  first : Array[String],
  second : Array[String],
) -> Array[String] {
  let result : Array[String] = []
  let seen : Map[String, Unit] = Map([])
  for items in [first, second] {
    for item in items {
      if !seen.contains(item) {
        seen[item] = ()
        result.push(item)
      }
    }
  }
  result
}

///|
/// Defaults -> selected JSON -> explicit CLI. No paths are resolved here.
pub fn resolve(cli : Cli, json : Patch?) -> Options raise ConfigError {
  let file = json.unwrap_or(Patch::empty())
  validate_patch(file)
  validate_patch(cli.patch)
  validate_path(cli.directory, "directory")
  if cli.config_path is Some(path) {
    validate_path(path, "config")
  }
  if cli.quiet && cli.verbose {
    raise Conflict("quiet/verbose")
  }
  // Only an explicitly supplied CLI --output conflicts with --stdout.
  if cli.stdout && cli.patch.output is Some(_) {
    raise Conflict("output/stdout")
  }
  let patch = cli.patch
  let style = patch.style.unwrap_or(file.style.unwrap_or(Xml))
  {
    directory: cli.directory,
    output: patch.output.unwrap_or(
      file.output.unwrap_or(
        match style {
          Xml => "repomix-output.xml"
          Markdown => "repomix-output.md"
        },
      ),
    ),
    stdout: cli.stdout,
    style,
    include_patterns: append_unique(
      file.include_patterns,
      patch.include_patterns,
    ),
    ignore: append_unique(file.ignore, patch.ignore),
    use_gitignore: patch.use_gitignore.unwrap_or(
      file.use_gitignore.unwrap_or(true),
    ),
    use_dot_ignore: patch.use_dot_ignore.unwrap_or(
      file.use_dot_ignore.unwrap_or(true),
    ),
    use_default_patterns: patch.use_default_patterns.unwrap_or(
      file.use_default_patterns.unwrap_or(true),
    ),
    security_check: patch.security_check.unwrap_or(
      file.security_check.unwrap_or(true),
    ),
    encoding: patch.encoding.unwrap_or(file.encoding.unwrap_or("o200k_base")),
    token_budget: match patch.token_budget {
      Some(_) as value => value
      None => file.token_budget
    },
    max_file_size: patch.max_file_size.unwrap_or(
      file.max_file_size.unwrap_or(52428800),
    ),
    tokenizer_path: match patch.tokenizer_path {
      Some(_) as value => value
      None => file.tokenizer_path
    },
    quiet: cli.quiet,
    verbose: cli.verbose,
  }
}