///|
pub(all) enum OperationScopeKind {
  PathOperation
  ShellOperation
  UrlOperation
} derive(Debug, Eq)

///|
pub fn OperationScopeKind::name(self : OperationScopeKind) -> String {
  match self {
    PathOperation => "path"
    ShellOperation => "shell"
    UrlOperation => "url"
  }
}

///|
pub struct OperationScope {
  kind : OperationScopeKind
  value : String
  writable : Bool
} derive(Debug, Eq)

///|
pub fn OperationScope::path(
  root~ : String,
  writable? : Bool = false,
) -> OperationScope {
  { kind: PathOperation, value: root, writable }
}

///|
pub fn OperationScope::shell(command : String) -> OperationScope {
  { kind: ShellOperation, value: command, writable: false }
}

///|
pub fn OperationScope::url(origin : String) -> OperationScope {
  { kind: UrlOperation, value: origin, writable: false }
}

///|
pub fn OperationScope::kind(self : OperationScope) -> OperationScopeKind {
  self.kind
}

///|
pub fn OperationScope::value(self : OperationScope) -> String {
  self.value
}

///|
pub fn OperationScope::root(self : OperationScope) -> String {
  if self.kind is PathOperation {
    self.value
  } else {
    ""
  }
}

///|
pub fn OperationScope::command(self : OperationScope) -> String {
  if self.kind is ShellOperation {
    self.value
  } else {
    ""
  }
}

///|
pub fn OperationScope::origin(self : OperationScope) -> String {
  if self.kind is UrlOperation {
    self.value
  } else {
    ""
  }
}

///|
pub fn OperationScope::writable(self : OperationScope) -> Bool {
  self.writable
}

///|
pub fn OperationScope::allows(
  self : OperationScope,
  requested : OperationScope,
) -> Bool {
  if self.kind != requested.kind {
    return false
  }
  match self.kind {
    PathOperation =>
      (!requested.writable || self.writable) &&
      path_inside_scope(self.value, requested.value)
    ShellOperation => self.value == requested.value
    UrlOperation => self.value == requested.value
  }
}

///|
pub fn OperationScope::validate(self : OperationScope) -> Array[String] {
  let problems : Array[String] = []
  if self.value == "" {
    problems.push("\{self.kind.name()} operation scope value is required")
  }
  match self.kind {
    PathOperation =>
      if self.value.contains("\u{0000}") {
        problems.push("path operation scope must not contain null bytes")
      }
    ShellOperation =>
      if contains_ascii_space(self.value) ||
        self.value.contains("\n") ||
        self.value.contains("\r") {
        problems.push(
          "shell operation scope command must be a single executable",
        )
      }
    UrlOperation =>
      if !valid_url_origin(self.value) {
        problems.push(
          "url operation scope origin must be http(s): \{self.value}",
        )
      }
  }
  problems
}

///|
pub fn OperationScope::to_json(self : OperationScope) -> String {
  let field = match self.kind {
    PathOperation => "root"
    ShellOperation => "command"
    UrlOperation => "origin"
  }
  let writable = if self.kind is PathOperation {
    ",\"writable\":\{self.writable.json_bool()}"
  } else {
    ""
  }
  [
    "{",
    "\"kind\":\{self.kind.name().json_string()},",
    "\"\{field}\":\{self.value.json_string()}",
    writable,
    "}",
  ].join("")
}

///|
fn OperationScope::fingerprint(self : OperationScope) -> String {
  "\{self.kind.name()}:\{self.value}:\{self.writable}"
}

///|
fn path_inside_scope(root : String, target : String) -> Bool {
  let root = normalize_scope_path(root)
  let target = normalize_scope_path(target)
  target == root ||
  target.has_prefix(root + "/") ||
  target.has_prefix(root + "\\")
}

///|
fn normalize_scope_path(path : String) -> String {
  let trimmed = path.trim_end(chars="/\\").to_owned()
  if trimmed == "" {
    path
  } else {
    trimmed
  }
}

///|
fn contains_ascii_space(value : String) -> Bool {
  value.contains(" ") || value.contains("\t")
}

///|
fn valid_url_origin(origin : String) -> Bool {
  if !(origin.has_prefix("https://") || origin.has_prefix("http://")) {
    return false
  }
  let without_scheme = if origin.has_prefix("https://") {
    origin[8:]
  } else {
    origin[7:]
  }
  without_scheme != "" &&
  !without_scheme.contains("/") &&
  !without_scheme.contains("?") &&
  !without_scheme.contains("#")
}