///|
pub(all) struct Mount {
  kind : (String) -> Int32 raise
  read : (String) -> String raise
  list : (String) -> Array[String] raise
}

///|
pub(all) struct Publication {
  stage : (@engine.Artifact) -> Unit raise
  commit : (String) -> Unit raise
  abort : () -> Unit raise
}

///|
pub(all) enum PublicationGuarantee {
  AtomicTask
  RecoverableTask
  HostManaged
}

///|
pub(all) struct Publisher {
  guarantee : PublicationGuarantee
  begin : (Array[String]) -> Publication raise
}

///|
pub(all) struct Limits {
  max_files : Int32
  max_bytes : Int32
  max_depth : Int32
}

///|
pub fn Limits::default() -> Limits {
  { max_files: 10000, max_bytes: 67108864, max_depth: 64, }
}

///|
pub(all) enum Event {
  Phase(String)
  Diagnostic(@engine.Diagnostic)
  Generated(@engine.LibraryProject)
  Validated(ValidatedProject)
  Published(String)
}

///|
pub(all) struct Host {
  input : Mount
  publisher : Publisher?
  toolchain : @toolchain.Provider?
  library_build : LibraryBuildProvider?
  metadata_sessions : MetadataSessions
  environment : Map[String, String]
  system : @config.Document
  global : @config.Document
  cancelled : () -> Bool
  event : (Event) -> Unit
  limits : Limits
}

///|
pub fn Host::new(
  input : Mount,
  publisher~ : Publisher? = None,
  toolchain~ : @toolchain.Provider? = None,
  library_build~ : LibraryBuildProvider? = None,
  environment~ : Map[String, String] = Map([]),
  system~ : @config.Document = @config.empty(),
  global~ : @config.Document = @config.empty(),
  cancelled~ : () -> Bool = fn() { false },
  event~ : (Event) -> Unit = fn(_) { () },
  limits~ : Limits = Limits::default(),
) -> Host {
  {
    input,
    publisher,
    toolchain,
    library_build,
    metadata_sessions: MetadataSessions::new(),
    environment,
    system,
    global,
    cancelled,
    event,
    limits,
  }
}

///|
/// Host-owned tooling for pipelines. Acquisition and embedding remain provider capabilities.
pub fn Host::toolchain_request(self : Host, request : Json) -> Json raise {
  if (self.cancelled)() {
    raise @platform.Interrupted
  }
  let provider = match self.toolchain {
    Some(provider) => provider
    None => raise @config.Invalid("Host does not provide a toolchain")
  }
  (self.event)(Phase("toolchain"))
  let result = (provider.request)(request)
  if (self.cancelled)() {
    raise @platform.Interrupted
  }
  result
}

///|
pub(all) struct Snapshot {
  tree : @workspace.Tree
  paths : Array[String]
}

///|
fn configuration_path(path : String) -> Bool {
  let name = path.split("/").last().unwrap_or("").to_owned()
  name == "morphir.config.scm" ||
  (
    ["morphir", "config"].any(stem => name.has_prefix(stem + ".")) &&
    [".toml", ".json", ".json5", ".yaml", ".yml"].any(extension => {
      name.has_suffix(extension)
    })
  ) ||
  name == "morphir-workspace.json"
}

///|
pub fn Host::snapshot(self : Host) -> Snapshot raise {
  (self.event)(Phase("snapshot"))
  let files = Map([])
  let paths = []
  let directories = []
  let mut bytes = 0
  fn walk(path : String, depth : Int32) -> Unit raise {
    if (self.cancelled)() {
      raise @platform.Interrupted
    }
    if depth > self.limits.max_depth {
      raise @config.Invalid("Directory depth limit exceeded")
    }
    directories.push(path)
    if paths.length() + directories.length() > self.limits.max_files {
      raise @config.Invalid("Entry limit exceeded")
    }
    let children = (self.input.list)(path)
    children.sort()
    for name in children {
      if name.contains("/") || name == "" || name == "." || name == ".." {
        raise @config.Invalid("Invalid directory entry: " + name)
      }
      if [".git", ".beads", ".mooncakes", "node_modules", "_build"].contains(
          name,
        ) {
        continue
      }
      let child = @workspace.join(path, name)
      match (self.input.kind)(child) {
        1 => {
          paths.push(child)
          if paths.length() + directories.length() > self.limits.max_files {
            raise @config.Invalid("Entry limit exceeded")
          }
          if configuration_path(child) {
            let content = (self.input.read)(child)
            bytes = bytes + @utf8.encode(content[:]).length()
            if bytes > self.limits.max_bytes {
              raise @config.Invalid("Configuration byte limit exceeded")
            }
            files[child] = content
          }
        }
        2 => walk(child, depth + 1)
        3 => raise @config.Invalid("Unresolved symbolic link: " + child)
        _ => raise @config.Invalid("Unreadable or unsupported entry: " + child)
      }
    }
  }
  walk("", 0)
  { tree: { files, directories, }, paths, }
}

///|
pub fn Snapshot::engine_host(self : Snapshot) -> @engine.Host {
  {
    read: fn(path) raise {
      match self.tree.files.get(path) {
        Some(s) => s
        None =>
          raise @config.Invalid(
            "Content not in configuration snapshot: " + path,
          )
      }
    },
    exists: fn(path) { self.tree.files.contains(path) },
    list: fn(root) { self.paths.filter(p => @workspace.within(root, p)) },
  }
}

///|
pub(all) struct Result {
  report : @engine.Report
  committed : Array[String]
  publication_error : String?
  cancelled : Bool
}

///|
pub fn Result::successful(self : Result) -> Bool {
  self.report.successful() && self.publication_error is None && !self.cancelled
}

///|
pub fn Host::run(
  self : Host,
  engine : @engine.Engine,
  plan : @engine.Plan,
) -> Result raise {
  let publisher = match self.publisher {
    Some(publisher) => publisher
    None => raise @config.Invalid("Host does not provide publication")
  }
  let result = self.run_content(engine, plan, ContentPublisher::text(publisher))
  {
    report: result.report.text_report(),
    committed: result.committed,
    publication_error: result.publication_error,
    cancelled: result.cancelled,
  }
}

///|
pub fn Mount::memory(files : Map[String, String]) -> Mount raise {
  let data = Map([])
  let dirs = Map::from_array([("", true)])
  for path, value in files {
    let path = @workspace.normalize(path)
    if path == "" || data.contains(path) {
      raise @config.Invalid("Invalid or duplicate memory path: " + path)
    }
    data[path] = value
    let mut parent = @workspace.parent(path)
    while parent != "" {
      dirs[parent] = true
      parent = @workspace.parent(parent)
    }
  }
  {
    kind: fn(path) {
      if data.contains(path) {
        1
      } else if dirs.contains(path) {
        2
      } else {
        0
      }
    },
    read: fn(path) raise {
      match data.get(path) {
        Some(s) => s
        None => raise @config.Invalid("Missing memory file: " + path)
      }
    },
    list: fn(root) {
      let names = Map([])
      for path in data.keys().concat(dirs.keys()) {
        if path != root && @workspace.parent(path) == root {
          names[path.split("/").last().unwrap_or("").to_owned()] = true
        }
      }
      names.keys().collect()
    },
  }
}

///|
pub fn Publisher::memory(outputs : Map[String, String]) -> Publisher {
  {
    guarantee: AtomicTask,
    begin: fn(destinations) {
      let staged : Map[String, String] = Map([])
      {
        stage: fn(artifact) raise {
          if !destinations.any(d => @workspace.within(d, artifact.path)) {
            raise @config.Invalid("Artifact outside publication")
          }
          staged[artifact.path] = artifact.content
        },
        commit: fn(destination) {
          for path in outputs.keys().collect() {
            if @workspace.within(destination, path) {
              outputs.remove(path)
            }
          }
          for path, value in staged {
            if @workspace.within(destination, path) {
              outputs[path] = value
            }
          }
        },
        abort: fn() { staged.clear() },
      }
    },
  }
}