///|
priv struct ConfigPayload {
  source : String
  destination : String
  location : String?
} derive(FromJson)

///|
priv struct ConfigDocumentType {
  name : String
  extensions : Array[String]
  role : String?
} derive(FromJson)

///|
priv struct PackageConfig {
  schema_version : Int
  executable : String?
  product_name : String?
  identifier : String?
  version : String?
  formats : Array[String]?
  output : String?
  payloads : Array[ConfigPayload]?
  icons : Array[String]?
  url_schemes : Array[String]?
  document_types : Array[ConfigDocumentType]?
  sign : Bool?
  notarize : Bool?
} derive(FromJson, Default)

///|
priv suberror PackageCommandError {
  InvalidConfig(path~ : String, detail~ : String)
  UnsupportedSchema(version~ : Int)
  MissingOption(name~ : String)
} derive(Debug)

///|
impl Show for PackageCommandError with fn output(self, logger) {
  logger.write_string(
    match self {
      InvalidConfig(path~, detail~) =>
        "invalid package config " + path + ": " + detail
      UnsupportedSchema(version~) =>
        "unsupported package config schema version " + version.to_string()
      MissingOption(name~) => "missing required option --" + name
    },
  )
}

///|
fn decode_package_config(
  source : String,
  path : String,
) -> PackageConfig raise PackageCommandError {
  let config : PackageConfig = @json.from_json(@json.parse(source)) catch {
    error =>
      raise PackageCommandError::InvalidConfig(path~, detail=error.to_string())
  }
  guard config.schema_version == 1 else {
    raise PackageCommandError::UnsupportedSchema(version=config.schema_version)
  }
  config
}

///|
async fn load_package_config(path : String) -> (PackageConfig, String) {
  let absolute = @path.Path(path).resolve()
  let source = @async_fs.read_file(absolute.to_string()).text() catch {
    error =>
      raise PackageCommandError::InvalidConfig(
        path=absolute.to_string(),
        detail=error.to_string(),
      )
  }
  (
    decode_package_config(source, absolute.to_string()),
    absolute.dirname().to_string(),
  )
}

///|
fn PackageConfig::payload_specs(
  self : PackageConfig,
  base : String,
) -> Array[@package.Payload] raise @package.PackagePlanError {
  self.payloads
  .unwrap_or([])
  .map(payload => {
    let location = match payload.location {
      Some(value) => @package.PayloadLocation::parse(value)
      None => @package.PayloadLocation::Resources
    }
    @package.Payload(
      resolve_path(base, payload.source),
      payload.destination,
      location~,
    )
  })
}

///|
fn PackageConfig::document_type_specs(
  self : PackageConfig,
) -> Array[@package.DocumentType] {
  self.document_types
  .unwrap_or([])
  .map(document_type => {
    @package.DocumentType(
      document_type.name,
      document_type.extensions,
      role=document_type.role.unwrap_or("Viewer"),
    )
  })
}

///|
fn resolve_path(base : String, value : String) -> String {
  let path : @path.Path = value
  if path.is_absolute() {
    path.resolve().to_string()
  } else {
    @path.Path(base).join(path).resolve().to_string()
  }
}