///|
/// Invalid extended-document metadata or an AST target absent from its registry.
/// These errors never alter the AST or substitute a standard target.
pub(all) suberror ExtendedError {
  UnregisteredTarget(Target)
  InvalidIdentity(String)
} derive(Eq, Debug)

///|
/// A parsed AST and its declared dialect, profile identities, and target registry.
/// This document contains data only. Decoding never imports profile code.
/// Use `Extended` to validate the references before encoding.
pub struct Extended {
  parsed : Parsed
  dialect : String
  profiles : ArrayView[String]
  targets : @target.Registry
} derive(Eq, Debug)

///|
/// Create an extended document. Every AST target must occur in `targets`.
/// `dialect` must be nonempty. Profile identities must be nonempty and unique;
/// use module-qualified identities with a version for published profiles.
/// The profile list is copied. Registration does not implement a backend.
pub fn Extended::Extended(
  parsed : Parsed,
  dialect~ : String,
  targets~ : @target.Registry,
  profiles~ : ArrayView[String] = [][:],
) -> Extended raise ExtendedError {
  if dialect.is_empty() {
    raise InvalidIdentity(dialect)
  }
  let names : Array[String] = []
  for profile in profiles {
    if profile.is_empty() || names.contains(profile) {
      raise InvalidIdentity(profile)
    }
    names.push(profile)
  }
  names.sort_by(byte_order)
  let document = { parsed, dialect, targets, profiles: names[:], }
  document.validate()
  document
}

///|
fn Extended::validate(self : Extended) -> Unit raise ExtendedError {
  fn check(target : Target) -> Unit raise ExtendedError {
    if self.targets.resolve(target.name()) != Some(target) {
      raise UnregisteredTarget(target)
    }
  }
  for definition in self.parsed.module_.definitions {
    if definition.target is Some(target) {
      check(target)
    }
    for binding in definition.definition.external_bindings().all() {
      check(binding.target)
    }
  }
  for warning in self.parsed.warnings {
    if warning is DeprecatedTargetShorthand(target~, ..) {
      check(target)
    }
  }
}

///|
/// Return a definition's external bindings. Definitions without an external
/// annotation grammar return an empty collection.
pub fn Definition::external_bindings(self : Definition) -> ExternalBindings {
  match self {
    Function(f) => f.externals
    CustomType(t) => t.externals
    _ => ExternalBindings::empty()
  }
}

///|
fn extended_definition(definition : TargetedDefinition) -> Json {
  // Reuse the stack-safe node codec. Replace only the compatibility fields at
  // the definition boundary; nested expression/type codecs stay identical.
  let payload = run(enc_definition(definition.definition, j => Done(j)))
  let fields = match payload {
    Object(fields) => fields
    _ => Map([])
  }
  match definition.definition {
    Function(_) | CustomType(_) => {
      fields.remove("external_erlang")
      fields.remove("external_javascript")
      fields.set(
        "externals",
        arr(
          definition.definition
          .external_bindings()
          .all()
          .map(binding => {
            obj([
              ("target", str(binding.target.name())),
              ("module", str(binding.external.module_)),
              ("function", str(binding.external.function)),
              ("location", encode_span(binding.external.location)),
            ])
          }),
        ),
      )
    }
    _ => ()
  }
  obj([
    ("definition", Json::object(fields)),
    ("target", opt(definition.target, t => str(t.name()))),
  ])
}

///|
/// Encode version 1 of the extended JSON format. Use `canonical` for
/// deterministic bytes. Unlike compiler-compatible export, this preserves
/// every registered target and generic external binding. Revalidate references
/// on each call in case the caller changed an AST array after construction.
///
/// ```mbt check
/// test {
///   let parsed : @ast.Parsed = {
///     module_: { definitions: [][:], },
///     extra: @ast.ModuleExtra::empty(),
///     warnings: [][:],
///   }
///   let document = @ast.Extended(
///     parsed,
///     dialect="gleam-1.19",
///     targets=@target.Registry::standard(),
///   )
///   assert_eq(@ast.decode_extended(@ast.encode_extended(document)), document)
/// }
/// ```
pub fn encode_extended(document : Extended) -> Json raise ExtendedError {
  document.validate()
  let targets = document.targets.all().map(t => t.name())
  targets.sort_by(byte_order)
  obj([
    ("format", str("gleam-parser-extended")),
    ("version", num(1)),
    ("dialect", str(document.dialect)),
    ("profiles", list(document.profiles, str)),
    ("targets", arr(targets.map(str))),
    (
      "parsed",
      obj([
        (
          "module",
          obj([
            (
              "definitions",
              list(document.parsed.module_.definitions, extended_definition),
            ),
          ]),
        ),
        ("extra", encode_extra(document.parsed.extra)),
        ("warnings", list(document.parsed.warnings, warning_json)),
      ]),
    ),
  ])
}

///|
fn registered_target(
  json : Json,
  path : Path,
  registry : @target.Registry,
) -> Target raise DecodeError {
  let name = dec_string(json, path)
  match registry.resolve(name) {
    Some(target) => target
    None => fail(path, "unregistered target \"\{name}\"")
  }
}

///|
fn dec_extended_definition(
  json : Json,
  path : Path,
  registry : @target.Registry,
) -> TargetedDefinition raise DecodeError {
  let target = dec_field(json, path, "target", (value, path) => {
    dec_nullable(value, path, (value, path) => {
      registered_target(value, path, registry)
    })
  })
  let definition = dec_field(json, path, "definition", (value, path) => {
    let fields = dec_object(value, path).copy()
    let kind = dec_kind_of(value, path)
    let mut externals = ExternalBindings::empty()
    if kind == "function" || kind == "custom_type" {
      if fields.contains("external_erlang") ||
        fields.contains("external_javascript") {
        fail(path, "legacy external fields are not part of the extended format")
      }
      let bindings = dec_field(value, path, "externals", (value, path) => {
        dec_array(value, path, (value, path) => {
          let target = dec_field(value, path, "target", (value, path) => {
            registered_target(value, path, registry)
          })
          let external = dec_external(value, path)
          (target, external, path)
        })
      })
      for binding in bindings {
        let (target, external, binding_path) = binding
        externals = externals.with_binding(target, external) catch {
          _ =>
            fail(
              binding_path,
              "duplicate external binding for \"\{target.name()}\"",
            )
        }
      }
      fields.remove("externals")
      fields.set("external_erlang", Json::null())
      fields.set("external_javascript", Json::null())
    } else if fields.contains("externals") {
      fail(
        Key(path, "externals"),
        "external bindings are not allowed on this definition",
      )
    }
    let definition = run_decode(
      dec_definition(Json::object(fields), path, v => Decoded(v)),
    )
    match definition {
      Function(f) => Function({ ..f, externals, })
      CustomType(t) => CustomType({ ..t, externals, })
      _ => definition
    }
  })
  { definition, target, }
}

///|
/// Decode version 1 of the extended JSON format without loading profile code.
/// Validate the embedded registry, rejecting invalid names, aliases, duplicates,
/// missing standard identities, unknown references, and duplicate bindings.
/// Raise `DecodeError` with the JSON path for malformed or unsupported input.
/// Profile identities are descriptive data; an application must supply matching
/// profile code explicitly if it wants to parse or format new source.
pub fn decode_extended(json : Json) -> Extended raise DecodeError {
  let format = dec_field(json, Root, "format", dec_string)
  if format != "gleam-parser-extended" {
    fail(Key(Root, "format"), "unknown format")
  }
  let version = dec_field(json, Root, "version", dec_int)
  if version != 1 {
    fail(Key(Root, "version"), "unsupported extended format version")
  }
  let dialect = dec_field(json, Root, "dialect", dec_string)
  let profiles = dec_field(json, Root, "profiles", (v, p) => {
    dec_array(v, p, dec_string)
  })
  let names = dec_field(json, Root, "targets", (v, p) => {
    dec_array(v, p, dec_string)
  })
  let mut registry = @target.Registry::standard()
  let seen : Array[String] = []
  for i, name in names {
    let path = Index(Key(Root, "targets"), i)
    if seen.contains(name) {
      fail(path, "duplicate target registration \"\{name}\"")
    }
    seen.push(name)
    if Target::of(name) is None {
      registry = registry.register(Target::extension(name)) catch {
        _ => fail(path, "invalid target registration \"\{name}\"")
      }
    }
  }
  if !seen.contains("erlang") || !seen.contains("javascript") {
    fail(Key(Root, "targets"), "missing standard target registration")
  }
  let parsed = dec_field(json, Root, "parsed", (value, path) => {
    let extra = dec_field(value, path, "extra", dec_extra)
    let warnings = dec_field(value, path, "warnings", (v, p) => {
      dec_array(v, p, (v, p) => {
        if dec_kind_of(v, p) == "deprecated_target_shorthand" {
          DeprecatedTargetShorthand(
            target=dec_field(v, p, "target", (v, p) => {
              registered_target(v, p, registry)
            }),
            location=dec_field(v, p, "location", dec_span),
          )
        } else {
          dec_warning(v, p)
        }
      })
    })
    let definitions = dec_field(value, path, "module", (v, p) => {
      dec_field(v, p, "definitions", (v, p) => {
        dec_array(v, p, (v, p) => dec_extended_definition(v, p, registry))
      })
    })
    Parsed::{ module_: { definitions, }, extra, warnings, }
  })
  Extended(parsed, dialect~, targets=registry, profiles~) catch {
    InvalidIdentity(name) =>
      fail(Root, "invalid or repeated dialect/profile identity \"\{name}\"")
    UnregisteredTarget(target) =>
      fail(Root, "unregistered target \"\{target.name()}\"")
  }
}