///|
pub(all) struct Location {
  root : String
  invocation : String
  file : String?
  directory : @lander_config.FileTreePath
  configuration_files : Map[String, String]
}

///|
fn configuration_platform() -> @lander_config.Platform {
  match @platform.system_type() {
    "windows" => Windows
    "macos" => MacOs
    _ => Linux
  }
}

///|
#cfg(target="wasm")
fn configuration_cwd() -> @lander_config.FileTreePath raise {
  @lander_config.FileTreePath::parse("/__morphir_wasi")
}

///|
#cfg(not(target="wasm"))
fn configuration_cwd() -> @lander_config.FileTreePath raise {
  @lander_config.FileTreePath::from_native(
    @platform.cwd(),
    configuration_platform(),
  )
}

///|
#cfg(target="wasm")
fn configuration_native(
  path : @lander_config.FileTreePath,
  machine : Bool,
) -> String? {
  let text = path.to_string()
  let base = "/__morphir_wasi"
  if text == base {
    return Some(".")
  }
  if text.has_prefix(base + "/") {
    return Some(text[base.length() + 1:].to_owned())
  }
  if machine {
    // Conventions own filenames. WASI maps machine requests into host mounts.
    let directory = if text == "/etc/morphir" ||
      text.has_prefix("/etc/morphir/") {
      ".morphir-host/system"
    } else {
      ".morphir-host/user"
    }
    return Some(
      if text.has_suffix("/morphir") {
        directory
      } else {
        directory + "/" + path.name().unwrap_or("")
      },
    )
  }
  None
}

///|
#cfg(not(target="wasm"))
fn configuration_native(
  path : @lander_config.FileTreePath,
  _machine : Bool,
) -> String? {
  Some(path.to_native(configuration_platform()))
}

///|
fn read_configuration_requests(
  initial : @lander_config.FileTree,
  requests : Array[@lander_config.FileRequest],
  files : Map[String, String],
  machine~ : Bool = false,
) -> @lander_config.FileTree raise {
  let mut tree = initial
  for request in requests {
    match request {
      ListDir(path) => {
        guard configuration_native(path, machine) is Some(native) else {
          tree = tree.with_missing(path)
          continue
        }
        let kind = @platform.kind(native)
        // A file or a link cannot contain automatically discovered children.
        // Explicit files still pass through the regular-file ReadFile boundary.
        if kind == 0 || kind == 1 || kind == 3 {
          tree = tree.with_missing(path)
          continue
        }
        if kind != 2 {
          raise @config.Invalid(
            "Cannot list configuration directory: " + native,
          )
        }
        let entries = configuration_entries(
          native,
          @platform.list(native),
          @platform.kind,
        )
        tree = tree.with_listing(path, entries)
      }
      ReadFile(path) => {
        guard configuration_native(path, machine) is Some(native) else {
          tree = tree.with_missing(path)
          continue
        }
        match @platform.kind(native) {
          0 => tree = tree.with_missing(path)
          1 => {
            let text = @platform.read(native)
            files[path.to_string()] = text
            tree = tree.with_file(path, text)
          }
          _ =>
            raise @config.Invalid(
              "Configuration must be a readable regular file: " + native,
            )
        }
      }
    }
  }
  tree
}

///|
pub fn Location::at_root(root : String) -> Location raise {
  let cwd = configuration_cwd()
  let directory = if root == @platform.cwd() {
    cwd
  } else {
    let parsed = @lander_config.FileTreePath::from_native(
      root,
      configuration_platform(),
    )
    if parsed.is_absolute() {
      parsed
    } else {
      cwd.resolve(parsed)
    }
  }
  { root, invocation: "", file: None, directory, configuration_files: Map([]), }
}

///|
/// The published document-aware policy owns the entire root walk.
pub fn locate(path : String, boundary? : String) -> Location raise {
  let requested = @workspace.normalize(path)
  let cwd = configuration_cwd()
  let full = if requested == "" { cwd } else { cwd.join(requested) }
  let native = configuration_native(full, false).unwrap_or(path)
  let kind = @platform.kind(native)
  if kind != 1 && kind != 2 {
    raise @config.Invalid("Input does not exist or is unsupported: " + path)
  }
  let start = if kind == 1 { full.parent().unwrap_or(full) } else { full }
  let boundary = boundary.map(path => {
    let path = @lander_config.FileTreePath::from_native(
      path,
      configuration_platform(),
    )
    if path.is_absolute() {
      path.to_string()
    } else {
      cwd.resolve(path).to_string()
    }
  })
  let conventions = @workspace.configuration_conventions(boundary?)
  let files = Map([])
  let mut tree = @lander_config.FileTree::new()
  let formats = @workspace.configuration_formats()
  while true {
    let context : @lander_config.LoadContext = {
      tree,
      platform: Some(configuration_platform()),
      env_vars: Map([]),
      working_dir: Some(start),
      formats,
    }
    let step = conventions.roots(context) catch {
      @lander_config.Ambiguous(candidates~, ..) =>
        raise @config.Invalid(
          "Ambiguous configuration: " + candidates.join(", "),
        )
      error => raise @config.Invalid(error.message())
    }
    match step {
      Need(requests) =>
        tree = read_configuration_requests(tree, requests, files)
      Done(evidence) => {
        let root = evidence
          .workspace()
          .unwrap_or(evidence.nearest_project().unwrap_or(start))
        let prefix = root.to_string() + "/"
        let invocation = if start == root {
          ""
        } else {
          start.to_string()[prefix.length():].to_owned()
        }
        let file = if kind == 1 {
          Some(full.to_string()[prefix.length():].to_owned())
        } else {
          None
        }
        return {
          root: configuration_native(root, false).unwrap(),
          invocation,
          file,
          directory: root,
          configuration_files: files,
        }
      }
    }
  } nobreak {
    abort("root discovery ended without a result")
  }
}

///|
pub fn configuration_options(
  location : Location,
  environment : Map[String, String],
  selected? : String,
  profiles~ : @lander_config.ProfileSelection = Unspecified,
  isolated~ : Bool = false,
  suppress~ : Array[String] = [],
) -> @workspace.ConfigurationOptions raise {
  let files = location.configuration_files.copy()
  if !isolated {
    let formats = @workspace.configuration_formats()
    let conventions = machine_conventions(
      @workspace.configuration_conventions(),
    )
    let builder = formats.fold(
        init=@lander_config.builder()
          |> @lander_config.working_dir(location.directory.to_string())
          |> @lander_config.platform(configuration_platform())
          |> @lander_config.env_vars(environment),
        (builder, format) => builder |> @lander_config.format(format),
      )
      |> @conventions.add(
        conventions,
        options=@conventions.DiscoveryOptions::new(config=Skip),
      )
    let mut tree = @lander_config.FileTree::new()
    while true {
      let step = @lander_config.load(builder, tree) catch {
        @lander_config.Ambiguous(candidates~, ..) =>
          raise @config.Invalid(
            "Ambiguous configuration: " + candidates.join(", "),
          )
        error => raise @config.Invalid(error.message())
      }
      match step {
        Need(requests) =>
          tree = read_configuration_requests(
            tree,
            requests,
            files,
            machine=true,
          )
        Done(_) => break
      }
    }
  }
  let selected = selected.map(path => {
    if path.has_suffix(".scm") {
      raise @config.Invalid(
        "Use --config-script for Scheme configuration scripts: " + path,
      )
    }
    let parsed = configuration_selected_path(path)
    let native = configuration_native(parsed, false).unwrap_or(path)
    if @platform.kind(native) != 1 {
      raise @config.Invalid(
        "Configuration file not found or unreadable: " + path,
      )
    }
    files[parsed.to_string()] = @platform.read(native)
    parsed.to_string()
  })
  @workspace.ConfigurationOptions::new(
    directory=location.directory,
    selected?,
    profiles~,
    isolated~,
    suppress~,
    external_files=files,
    platform=configuration_platform(),
  )
}

///|
#cfg(target="wasm")
fn configuration_selected_path(
  path : String,
) -> @lander_config.FileTreePath raise {
  // WASI grants the development directory through a preopen. Its selected
  // files use the same confined relative paths as the other WASI inputs.
  configuration_cwd().join(@workspace.normalize(path))
}

///|
#cfg(not(target="wasm"))
fn configuration_selected_path(
  path : String,
) -> @lander_config.FileTreePath raise {
  let resolved = @metadata_grants.host_path(
    path,
    @platform.cwd(),
    @platform.system_type(),
  )
  let parsed = @lander_config.FileTreePath::from_native(
    resolved,
    configuration_platform(),
  )
  if parsed.is_absolute() {
    parsed
  } else {
    configuration_cwd().resolve(parsed)
  }
}

///|
#cfg(target="wasm")
fn machine_conventions(
  conventions : @conventions.Conventions,
) -> @conventions.Conventions {
  conventions |> @conventions.without(AppHome)
}

///|
#cfg(not(target="wasm"))
fn machine_conventions(
  conventions : @conventions.Conventions,
) -> @conventions.Conventions {
  conventions
}

///|
/// Listing a parent must retain inaccessible entries without inspecting them as
/// candidates. A selected configuration file still crosses ReadFile explicitly.
fn configuration_entries(
  directory : String,
  names : Array[String],
  inspect : (String) -> Int32 raise,
) -> Array[@lander_config.DirEntry] {
  names.map(name => {
    let kind : @lander_config.EntryKind = try {
      match inspect(directory + "/" + name) {
        1 => File
        2 => Directory
        _ => Inaccessible
      }
    } catch {
      _ => Inaccessible
    }
    @lander_config.DirEntry::{ name, kind, }
  })
}