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

///|
pub(all) enum PathStyle {
  TargetRelative
  CwdRelative
  Absolute
} derive(Eq, Debug)

///|
pub(all) enum SplitOutput {
  Disabled
  Enabled(Int32)
} derive(Eq, Debug)

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

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

///|
fn parse_path_style(value : String) -> PathStyle raise ConfigError {
  match value {
    "target-relative" => TargetRelative
    "cwd-relative" => CwdRelative
    "absolute" => Absolute
    _ => raise InvalidValue("output.filePathStyle")
  }
}

///|
/// CLI reporting only; thresholds use the sum of accepted file content tokens.
pub(all) enum TokenCountTree {
  Disabled
  Enabled(Int32)
} derive(Eq, Debug)

///|
/// Explicit CLI file-list input. Configuration files never enable stdin reads.
pub(all) enum StdinMode {
  Off
  Lines
  Nul
} derive(Eq, Debug)

///|
pub(all) enum Action {
  Pack
  Init
  Help
  HelpAll
  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 split_output : SplitOutput?
  mut compress : Bool?
  mut keep_full_patterns : Array[String]
  mut header_text : String?
  mut instruction_file_path : String?
  mut full_metadata : Bool?
  mut file_summary : Bool?
  mut directory_structure : Bool?
  mut include_files : Bool?
  mut git_sections : Bool?
  mut metadata : Bool?
  mut show_line_numbers : Bool?
  mut output_file_path_style : PathStyle?
  mut include_diffs : Bool?
  mut include_logs : Bool?
  mut include_logs_count : Int32?
  mut remote : String?
  mut remote_branch : String?
  mut max_git_bytes : Int32?
  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 top_files_length : Int32?
  mut token_count_tree : TokenCountTree?
  mut max_file_size : Int32?
  mut max_files : Int32?
  mut max_total_size : Int64?
  mut tokenizer_path : String?
} derive(Debug)

///|
pub fn Patch::empty() -> Patch {
  {
    output: None,
    style: None,
    split_output: None,
    compress: None,
    keep_full_patterns: [],
    header_text: None,
    instruction_file_path: None,
    full_metadata: None,
    file_summary: None,
    directory_structure: None,
    include_files: None,
    git_sections: None,
    metadata: None,
    show_line_numbers: None,
    output_file_path_style: None,
    include_diffs: None,
    include_logs: None,
    include_logs_count: None,
    remote: None,
    remote_branch: None,
    max_git_bytes: None,
    include_patterns: [],
    ignore: [],
    use_gitignore: None,
    use_dot_ignore: None,
    use_default_patterns: None,
    security_check: None,
    encoding: None,
    token_budget: None,
    top_files_length: None,
    token_count_tree: None,
    max_file_size: None,
    max_files: None,
    max_total_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
  stdin_mode : StdinMode
  explain_paths : Array[String]
  report_json : Bool
  mcp : Bool
  webui : Bool
  webui_port : Int32
  mcp_max_tokens : Int32
  mcp_max_output_bytes : Int32
  mcp_max_volumes : Int32
  quiet : Bool
  verbose : Bool
  patch : Patch
} derive(Debug)

///|
pub(all) struct Options {
  directory : String
  output : String
  stdout : Bool
  style : Style
  split_output : SplitOutput
  compress : Bool
  keep_full_patterns : Array[String]
  header_text : String
  instruction_file_path : String?
  full_metadata : Bool
  file_summary : Bool
  directory_structure : Bool
  include_files : Bool
  git_sections : Bool
  metadata : Bool
  show_line_numbers : Bool
  output_file_path_style : PathStyle
  include_diffs : Bool
  include_logs : Bool
  include_logs_count : Int32
  remote : String?
  remote_branch : String?
  max_git_bytes : Int32
  include_patterns : Array[String]
  file_paths : Array[String]?
  ignore : Array[String]
  use_gitignore : Bool
  use_dot_ignore : Bool
  use_default_patterns : Bool
  security_check : Bool
  encoding : String
  token_budget : Int32?
  top_files_length : Int32
  token_count_tree : TokenCountTree
  max_file_size : Int32
  max_files : Int32
  max_total_size : Int64
  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 PathStyle with Eq::{equal, not_equal}

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

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

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

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

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

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

///|
pub extend StdinMode 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
    "json" => Json
    "plain" => Plain
    _ => 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("\u0000") {
    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)
  }
  for p in patch.keep_full_patterns {
    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.split_output is Some(SplitOutput::Enabled(value)) && value <= 0 {
    raise InvalidValue("output.splitOutput")
  }
  if patch.top_files_length is Some(value) && value < 0 {
    raise InvalidValue("output.topFilesLength")
  }
  if patch.token_count_tree is Some(Enabled(value)) && value < 0 {
    raise InvalidValue("output.tokenCountTree")
  }
  if patch.max_file_size is Some(value) && value <= 0 {
    raise InvalidValue("input.maxFileSize")
  }
  if patch.max_files is Some(value) && value < 0 {
    raise InvalidValue("input.maxFiles")
  }
  if patch.max_total_size is Some(value) &&
    (value < 0 || value > 9007199254740991L) {
    raise InvalidValue("input.maxTotalSize")
  }
  if patch.output is Some(value) {
    validate_path(value, "output.filePath")
  }
  if patch.instruction_file_path is Some(value) && !value.is_empty() {
    validate_path(value, "output.instructionFilePath")
  }
  if patch.tokenizer_path is Some(value) {
    validate_path(value, "tokenizerPath")
  }
  if patch.include_logs_count is Some(value) && (value <= 0 || value > 10000) {
    raise InvalidValue("output.git.includeLogsCount")
  }
  if patch.max_git_bytes is Some(value) && value <= 0 {
    raise InvalidValue("input.maxGitBytes")
  }
  if patch.remote is Some(value) {
    validate_path(value, "remote.url")
  }
  if patch.remote_branch is Some(value) {
    validate_path(value, "remote.branch")
  }
}

///|
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 {
  if cli.webui && json is Some(_) {
    raise Conflict("webui.config")
  }
  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")
  }
  // '-' agrees with --stdout; an explicit regular filename conflicts.
  if cli.stdout && cli.patch.output is Some(path) && path != "-" {
    raise Conflict("output/stdout")
  }
  let patch = cli.patch
  let style = patch.style.unwrap_or(file.style.unwrap_or(Xml))
  let split_output = patch.split_output.unwrap_or(
    file.split_output.unwrap_or(SplitOutput::Disabled),
  )
  let default_output = if split_output is SplitOutput::Enabled(_) {
    "moon-repomix-output"
  } else {
    match style {
      Xml => "repomix-output.xml"
      Markdown => "repomix-output.md"
      Json => "repomix-output.json"
      Plain => "repomix-output.txt"
    }
  }
  let output = patch.output.unwrap_or(file.output.unwrap_or(default_output))
  let token_budget = match patch.token_budget {
    Some(_) as value => value
    None => file.token_budget
  }
  if split_output is SplitOutput::Enabled(_) &&
    (cli.stdout || output == "-" || token_budget is Some(_)) {
    raise Conflict("splitOutput/stdout/tokenBudget")
  }
  let instruction_file_path = match patch.instruction_file_path {
    Some(_) as explicit => explicit
    None => file.instruction_file_path
  }
  let remote = match patch.remote {
    Some(_) as value => value
    None => file.remote
  }
  let remote_branch = match patch.remote_branch {
    Some(_) as value => value
    None => file.remote_branch
  }
  if remote_branch is Some(_) && remote is None {
    raise Conflict("remote.branch/remote.url")
  }
  if remote is Some(_) && cli.directory != "." {
    raise Conflict("remote/directory")
  }
  {
    directory: cli.directory,
    output,
    stdout: cli.stdout || output == "-",
    style,
    split_output,
    compress: patch.compress.unwrap_or(file.compress.unwrap_or(false)),
    keep_full_patterns: append_unique(
      file.keep_full_patterns,
      patch.keep_full_patterns,
    ),
    header_text: patch.header_text.unwrap_or(file.header_text.unwrap_or("")),
    instruction_file_path: match instruction_file_path {
      Some("") => None
      other => other
    },
    full_metadata: patch.full_metadata.unwrap_or(
      file.full_metadata.unwrap_or(false),
    ),
    file_summary: patch.file_summary.unwrap_or(
      file.file_summary.unwrap_or(true),
    ),
    directory_structure: patch.directory_structure.unwrap_or(
      file.directory_structure.unwrap_or(true),
    ),
    include_files: patch.include_files.unwrap_or(
      file.include_files.unwrap_or(true),
    ),
    git_sections: patch.git_sections.unwrap_or(
      file.git_sections.unwrap_or(true),
    ),
    metadata: patch.metadata.unwrap_or(file.metadata.unwrap_or(true)),
    show_line_numbers: patch.show_line_numbers.unwrap_or(
      file.show_line_numbers.unwrap_or(false),
    ),
    output_file_path_style: patch.output_file_path_style.unwrap_or(
      file.output_file_path_style.unwrap_or(TargetRelative),
    ),
    include_diffs: patch.include_diffs.unwrap_or(
      file.include_diffs.unwrap_or(false),
    ),
    include_logs: patch.include_logs.unwrap_or(
      file.include_logs.unwrap_or(false),
    ),
    include_logs_count: patch.include_logs_count.unwrap_or(
      file.include_logs_count.unwrap_or(50),
    ),
    remote,
    remote_branch,
    max_git_bytes: patch.max_git_bytes.unwrap_or(
      file.max_git_bytes.unwrap_or(8388608),
    ),
    include_patterns: append_unique(
      file.include_patterns,
      patch.include_patterns,
    ),
    file_paths: None,
    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,
    top_files_length: patch.top_files_length.unwrap_or(
      file.top_files_length.unwrap_or(5),
    ),
    token_count_tree: patch.token_count_tree.unwrap_or(
      file.token_count_tree.unwrap_or(Disabled),
    ),
    max_file_size: patch.max_file_size.unwrap_or(
      file.max_file_size.unwrap_or(52428800),
    ),
    max_files: patch.max_files.unwrap_or(file.max_files.unwrap_or(20000)),
    max_total_size: patch.max_total_size.unwrap_or(
      file.max_total_size.unwrap_or(209715200L),
    ),
    tokenizer_path: match patch.tokenizer_path {
      Some(_) as value => value
      None => file.tokenizer_path
    },
    quiet: cli.quiet,
    verbose: cli.verbose,
  }
}