///|
fn type_name_is_qualified(name : String) -> Bool {
  name.find(".") is Some(_)
}

///|
fn optional_type_name_is_qualified(name : String?) -> Bool {
  match name {
    Some(value) => type_name_is_qualified(value)
    None => false
  }
}

///|
fn expr_uses_qualified_type_name(expr : Expr) -> Bool {
  match expr {
    TypedObjectLiteral(type_name, members) =>
      type_name_is_qualified(type_name) ||
      object_members_use_qualified_type_name(members)
    ObjectLiteral(members) => object_members_use_qualified_type_name(members)
    ListingLiteral(elements) => {
      for element in elements {
        if expr_uses_qualified_type_name(element) {
          return true
        }
      }
      false
    }
    MappingLiteral(entries) => {
      for entry in entries {
        if expr_uses_qualified_type_name(entry.key) ||
          expr_uses_qualified_type_name(entry.value) {
          return true
        }
      }
      false
    }
    MemberAccess(target, _) | SafeMemberAccess(target, _) =>
      expr_uses_qualified_type_name(target)
    SubscriptAccess(target, key) =>
      expr_uses_qualified_type_name(target) ||
      expr_uses_qualified_type_name(key)
    AmendExpr(base, members) =>
      expr_uses_qualified_type_name(base) ||
      object_members_use_qualified_type_name(members)
    CallExpr(callee, arguments) => {
      if expr_uses_qualified_type_name(callee) {
        return true
      }
      for argument in arguments {
        if expr_uses_qualified_type_name(argument) {
          return true
        }
      }
      false
    }
    LambdaExpr(parameters, body, return_type_name) => {
      if optional_type_name_is_qualified(return_type_name) ||
        function_parameters_use_qualified_type_name(parameters) {
        return true
      }
      expr_uses_qualified_type_name(body)
    }
    NonNullExpr(inner) | UnaryExpr(_, inner) =>
      expr_uses_qualified_type_name(inner)
    BinaryExpr(_, left, right) =>
      expr_uses_qualified_type_name(left) ||
      expr_uses_qualified_type_name(right)
    ConditionalExpr(condition, then_expr, else_expr) =>
      expr_uses_qualified_type_name(condition) ||
      expr_uses_qualified_type_name(then_expr) ||
      expr_uses_qualified_type_name(else_expr)
    _ => false
  }
}

///|
fn object_members_use_qualified_type_name(
  members : Array[ObjectMember],
) -> Bool {
  for object_member in members {
    if optional_type_name_is_qualified(object_member.type_name) ||
      expr_uses_qualified_type_name(object_member.value) {
      return true
    }
  }
  false
}

///|
fn function_parameters_use_qualified_type_name(
  parameters : Array[FunctionParameter],
) -> Bool {
  for parameter in parameters {
    if optional_type_name_is_qualified(parameter.type_name) {
      return true
    }
  }
  false
}

///|
fn function_decl_uses_qualified_type_name(function_decl : FunctionDecl) -> Bool {
  if optional_type_name_is_qualified(function_decl.return_type_name) ||
    function_parameters_use_qualified_type_name(function_decl.parameters) {
    return true
  }
  match function_decl.body {
    Some(body) => expr_uses_qualified_type_name(body)
    None => false
  }
}

///|
fn class_properties_use_qualified_type_name(
  properties : Array[ClassProperty],
) -> Bool {
  for property in properties {
    if optional_type_name_is_qualified(property.type_name) {
      return true
    }
    match property.value {
      Some(value) => if expr_uses_qualified_type_name(value) { return true }
      None => ()
    }
  }
  false
}

///|
fn class_methods_use_qualified_type_name(methods : Array[FunctionDecl]) -> Bool {
  for class_method in methods {
    if function_decl_uses_qualified_type_name(class_method) {
      return true
    }
  }
  false
}

///|
fn declaration_uses_qualified_type_name(declaration : Declaration) -> Bool {
  match declaration {
    ClassDeclaration(class_decl) => {
      if optional_type_name_is_qualified(class_decl.parent_name) {
        return true
      }
      class_properties_use_qualified_type_name(class_decl.properties) ||
      class_methods_use_qualified_type_name(class_decl.methods)
    }
    FunctionDeclaration(function_decl) =>
      function_decl_uses_qualified_type_name(function_decl)
    TypeAliasDeclaration(type_alias) =>
      type_name_is_qualified(type_alias.target)
  }
}

///|
fn program_uses_qualified_type_name(program : Program) -> Bool {
  for declaration in program.declarations {
    if declaration_uses_qualified_type_name(declaration) {
      return true
    }
  }
  for binding in program.bindings {
    if optional_type_name_is_qualified(binding.type_name) ||
      expr_uses_qualified_type_name(binding.value) {
      return true
    }
  }
  match program.body {
    Some(body) => expr_uses_qualified_type_name(body)
    None => false
  }
}

///|
fn narrow_positive_is_guard_type(original : Type, guard_type : Type) -> Type? {
  match original {
    UnionType(options) => {
      let narrowed : Array[Type] = []
      for option in options {
        if type_accepts(guard_type, option) {
          push_unique_type(narrowed, option)
        }
      }
      if narrowed.length() == 0 {
        None
      } else {
        Some(make_union_type(narrowed))
      }
    }
    NullableType(inner) =>
      if type_accepts(guard_type, inner) {
        Some(inner)
      } else if guard_type == NullType {
        Some(NullType)
      } else {
        None
      }
    _ =>
      if type_accepts(guard_type, original) {
        Some(original)
      } else if type_accepts(original, guard_type) {
        Some(guard_type)
      } else {
        None
      }
  }
}

///|
fn narrow_negative_is_guard_type(original : Type, guard_type : Type) -> Type? {
  match original {
    UnionType(options) => {
      let narrowed : Array[Type] = []
      let mut changed = false
      for option in options {
        if type_accepts(guard_type, option) {
          changed = true
        } else {
          push_unique_type(narrowed, option)
        }
      }
      if changed && narrowed.length() > 0 {
        Some(make_union_type(narrowed))
      } else {
        None
      }
    }
    NullableType(inner) =>
      if type_accepts(guard_type, inner) {
        Some(NullType)
      } else if guard_type == NullType {
        Some(inner)
      } else {
        None
      }
    _ => None
  }
}

///|
fn push_narrowed_type_binding(
  name : String,
  guard_type : Type,
  positive : Bool,
  bindings : Array[Binding],
  env : Array[TypeBinding],
  type_env : Array[TypeBinding],
  source_cache : Array[TypeBinding],
  target_cache : Array[TypeBinding],
  stack : Array[String],
  diagnostics : Array[Diagnostic],
  resolve_import : (String) -> TypecheckResult?,
) -> Unit {
  match
    resolve_binding_type(
      name, bindings, env, type_env, source_cache, stack, diagnostics, resolve_import,
    ) {
    Some(original_type) => {
      let narrowed = if positive {
        narrow_positive_is_guard_type(original_type, guard_type)
      } else {
        narrow_negative_is_guard_type(original_type, guard_type)
      }
      match narrowed {
        Some(typ) =>
          target_cache.push({ name, typ, alias_decl: None, bound: None })
        None => ()
      }
    }
    None => ()
  }
}

///|
fn apply_positive_is_guard_expr(
  expr : Expr,
  bindings : Array[Binding],
  env : Array[TypeBinding],
  type_env : Array[TypeBinding],
  source_cache : Array[TypeBinding],
  target_cache : Array[TypeBinding],
  stack : Array[String],
  diagnostics : Array[Diagnostic],
  resolve_import : (String) -> TypecheckResult?,
) -> Unit {
  match expr {
    BinaryExpr(Is, Identifier(name), Identifier(type_name)) =>
      match type_from_annotation(type_name, type_env) {
        Some(guard_type) =>
          push_narrowed_type_binding(
            name, guard_type, true, bindings, env, type_env, source_cache, target_cache,
            stack, diagnostics, resolve_import,
          )
        None => ()
      }
    BinaryExpr(Equal, Identifier(name), NullLiteral) =>
      push_narrowed_type_binding(
        name,
        NullType,
        true,
        bindings,
        env,
        type_env,
        source_cache,
        target_cache,
        stack,
        diagnostics,
        resolve_import,
      )
    BinaryExpr(Equal, NullLiteral, Identifier(name)) =>
      push_narrowed_type_binding(
        name,
        NullType,
        true,
        bindings,
        env,
        type_env,
        source_cache,
        target_cache,
        stack,
        diagnostics,
        resolve_import,
      )
    BinaryExpr(NotEqual, Identifier(name), NullLiteral) =>
      push_narrowed_type_binding(
        name,
        NullType,
        false,
        bindings,
        env,
        type_env,
        source_cache,
        target_cache,
        stack,
        diagnostics,
        resolve_import,
      )
    BinaryExpr(NotEqual, NullLiteral, Identifier(name)) =>
      push_narrowed_type_binding(
        name,
        NullType,
        false,
        bindings,
        env,
        type_env,
        source_cache,
        target_cache,
        stack,
        diagnostics,
        resolve_import,
      )
    BinaryExpr(And, left, right) => {
      apply_positive_is_guard_expr(
        left, bindings, env, type_env, source_cache, target_cache, stack, diagnostics,
        resolve_import,
      )
      apply_positive_is_guard_expr(
        right, bindings, env, type_env, target_cache, target_cache, stack, diagnostics,
        resolve_import,
      )
    }
    _ => ()
  }
}

///|
fn apply_negative_is_guard_expr(
  expr : Expr,
  bindings : Array[Binding],
  env : Array[TypeBinding],
  type_env : Array[TypeBinding],
  source_cache : Array[TypeBinding],
  target_cache : Array[TypeBinding],
  stack : Array[String],
  diagnostics : Array[Diagnostic],
  resolve_import : (String) -> TypecheckResult?,
) -> Unit {
  match expr {
    BinaryExpr(Is, Identifier(name), Identifier(type_name)) =>
      match type_from_annotation(type_name, type_env) {
        Some(guard_type) =>
          push_narrowed_type_binding(
            name, guard_type, false, bindings, env, type_env, source_cache, target_cache,
            stack, diagnostics, resolve_import,
          )
        None => ()
      }
    BinaryExpr(Equal, Identifier(name), NullLiteral) =>
      push_narrowed_type_binding(
        name,
        NullType,
        false,
        bindings,
        env,
        type_env,
        source_cache,
        target_cache,
        stack,
        diagnostics,
        resolve_import,
      )
    BinaryExpr(Equal, NullLiteral, Identifier(name)) =>
      push_narrowed_type_binding(
        name,
        NullType,
        false,
        bindings,
        env,
        type_env,
        source_cache,
        target_cache,
        stack,
        diagnostics,
        resolve_import,
      )
    BinaryExpr(NotEqual, Identifier(name), NullLiteral) =>
      push_narrowed_type_binding(
        name,
        NullType,
        true,
        bindings,
        env,
        type_env,
        source_cache,
        target_cache,
        stack,
        diagnostics,
        resolve_import,
      )
    BinaryExpr(NotEqual, NullLiteral, Identifier(name)) =>
      push_narrowed_type_binding(
        name,
        NullType,
        true,
        bindings,
        env,
        type_env,
        source_cache,
        target_cache,
        stack,
        diagnostics,
        resolve_import,
      )
    _ => ()
  }
}

///|
fn all_program_bindings(program : Program) -> Array[Binding] {
  let bindings : Array[Binding] = []
  for decl in program.declarations {
    match decl {
      FunctionDeclaration(function_decl) =>
        match function_decl.body {
          Some(body) =>
            bindings.push({
              name: function_decl.name,
              type_name: None,
              value: LambdaExpr(
                function_decl.parameters,
                body,
                function_decl.return_type_name,
              ),
              exported: false,
              is_const: true,
              annotations: function_decl.annotations,
              abstract_slot: false,
              sibling_slot: false,
            })
          None => ()
        }
      _ => ()
    }
  }
  for binding in program.bindings {
    bindings.push(binding)
  }
  bindings
}

///|
fn class_properties_to_members(
  properties : Array[ClassProperty],
  type_env : Array[TypeBinding],
  diagnostics : Array[Diagnostic],
) -> Array[TypeMember] {
  let members : Array[TypeMember] = []
  for property in properties {
    let typ = match property.type_name {
      Some(type_name) =>
        match type_from_annotation(type_name, type_env) {
          Some(resolved) => resolved
          None => {
            diagnostics.push(diag("Cannot find type `\{type_name}`."))
            UnknownType
          }
        }
      None =>
        match property.value {
          Some(value) =>
            infer_expr_with_bindings(
              value,
              [],
              [],
              type_env,
              [],
              [],
              diagnostics,
              fn(_) { None },
            )
          None => UnknownType
        }
    }
    let member_type = match property.value {
      Some(_) => DefaultedType(typ)
      None => typ
    }
    members.push({ name: property.name, typ: member_type })
  }
  members
}

///|
fn class_methods_to_members(
  methods : Array[FunctionDecl],
  type_env : Array[TypeBinding],
  diagnostics : Array[Diagnostic],
) -> Array[TypeMember] {
  let members : Array[TypeMember] = []
  for class_method in methods {
    members.push({
      name: class_method.name,
      typ: DefaultedType(
        function_signature_type(class_method, type_env, diagnostics),
      ),
    })
  }
  members
}

///|
fn push_class_method_receiver_bindings(
  cache : Array[TypeBinding],
  class_name : String,
  members : Array[TypeMember],
) -> Unit {
  for type_member in members {
    cache.push({
      name: type_member.name,
      typ: member_contract_type(type_member.typ),
      alias_decl: None,
      bound: None,
    })
  }
  cache.push({
    name: "this",
    typ: ClassType(class_name, members),
    alias_decl: None,
    bound: None,
  })
}

///|
fn validate_class_method_body(
  class_name : String,
  members : Array[TypeMember],
  function_decl : FunctionDecl,
  type_env : Array[TypeBinding],
  diagnostics : Array[Diagnostic],
) -> Unit {
  match function_decl.body {
    Some(body) => {
      let method_cache : Array[TypeBinding] = []
      push_class_method_receiver_bindings(method_cache, class_name, members)
      let parameter_types = function_parameter_types(
        function_decl.parameters,
        type_env,
        diagnostics,
      )
      for i = 0; i < function_decl.parameters.length(); i = i + 1 {
        method_cache.push({
          name: function_decl.parameters[i].name,
          typ: parameter_types[i],
          alias_decl: None,
          bound: None,
        })
      }
      let inferred_return = infer_expr_with_bindings(
        body,
        [],
        [],
        type_env,
        method_cache,
        [],
        diagnostics,
        fn(_) { None },
      )
      match function_decl.return_type_name {
        Some(type_name) =>
          match type_from_annotation(type_name, type_env) {
            Some(expected) =>
              if !type_accepts(expected, inferred_return) {
                diagnostics.push(
                  diag(
                    "method \{class_name}.\{function_decl.name} return annotation \{type_name} does not accept \{render_type(inferred_return)}",
                  ),
                )
              }
            None => diagnostics.push(diag("Cannot find type `\{type_name}`."))
          }
        None => ()
      }
    }
    None => ()
  }
}

///|
fn validate_class_method_bodies(
  class_name : String,
  members : Array[TypeMember],
  methods : Array[FunctionDecl],
  type_env : Array[TypeBinding],
  diagnostics : Array[Diagnostic],
) -> Unit {
  for class_method in methods {
    validate_class_method_body(
      class_name, members, class_method, type_env, diagnostics,
    )
  }
}

///|
fn class_decl_to_members(
  class_decl : ClassDecl,
  type_env : Array[TypeBinding],
  diagnostics : Array[Diagnostic],
) -> Array[TypeMember] {
  // PKL-089 introduced the scoped type environment so `class Box`
  // body uses of T resolve. PKL-110 sharpens the binding from
  // UnknownType to TypeVariable(name) so the call-site substitution
  // pass can later rewrite each T against the concrete argument type.
  let scoped_env = if class_decl.type_parameters.length() == 0 {
    type_env
  } else {
    let next : Array[TypeBinding] = []
    // PKL-116: thread declared bounds through the class-scoped type_env
    // so methods and properties resolved against `T` participate in the
    // same bound check as standalone functions. Bounds resolve against
    // the outer `type_env` (the class binding itself is not yet in
    // scope, matching PKL-110's TypeVariable injection order).
    for i = 0; i < class_decl.type_parameters.length(); i = i + 1 {
      let parameter = class_decl.type_parameters[i]
      let bound_text = if i < class_decl.type_parameter_bounds.length() {
        class_decl.type_parameter_bounds[i]
      } else {
        None
      }
      let bound = match bound_text {
        Some(text) => type_from_annotation(text, type_env)
        None => None
      }
      next.push({
        name: parameter,
        typ: TypeVariable(parameter),
        alias_decl: None,
        bound,
      })
    }
    for binding in type_env {
      next.push(binding)
    }
    next
  }
  let own_members = merge_type_members(
    class_properties_to_members(class_decl.properties, scoped_env, diagnostics),
    class_methods_to_members(class_decl.methods, scoped_env, diagnostics),
  )
  let members = match class_decl.parent_name {
    Some(parent_name) =>
      match lookup_type(scoped_env, parent_name) {
        Some(ClassType(_, base_members)) =>
          merge_type_members(base_members, own_members)
        Some(_) => {
          diagnostics.push(
            diag("class \{class_decl.name} extends non-class \{parent_name}"),
          )
          own_members
        }
        None => {
          diagnostics.push(diag("Cannot find type `\{parent_name}`."))
          own_members
        }
      }
    None => own_members
  }
  validate_class_method_bodies(
    class_decl.name,
    members,
    class_decl.methods,
    scoped_env,
    diagnostics,
  )
  members
}

///|
fn collect_declared_types(
  declarations : Array[Declaration],
  diagnostics : Array[Diagnostic],
) -> Array[TypeBinding] {
  collect_declared_types_with_imports(declarations, diagnostics, [])
}

///|
fn collect_declared_types_with_imports(
  declarations : Array[Declaration],
  diagnostics : Array[Diagnostic],
  imported_types : Array[TypeBinding],
) -> Array[TypeBinding] {
  let type_env = copy_type_bindings(imported_types)
  for decl in declarations {
    match decl {
      ClassDeclaration(class_decl) =>
        type_env.push({
          name: class_decl.name,
          typ: ClassType(class_decl.name, []),
          alias_decl: None,
          bound: None,
        })
      TypeAliasDeclaration(type_alias) =>
        // PKL-137: fall back to `type_from_annotation` so union /
        // constrained / nullable / generic alias targets resolve too.
        // `builtin_type_from_annotation` alone misses
        // `("draft" | "review" | "approved")` and similar; the richer
        // resolver participates in the partially built `type_env`
        // (later aliases can reference earlier ones).
        match
          (
            builtin_type_from_annotation(type_alias.target),
            type_from_annotation(type_alias.target, type_env),
          ) {
          (Some(typ), _) | (None, Some(typ)) =>
            type_env.push({
              name: type_alias.name,
              typ,
              alias_decl: if type_alias.type_parameters.length() > 0 {
                Some(type_alias)
              } else {
                None
              },
              bound: None,
            })
          (None, None) => ()
        }
      // PKL-090 flattened function-level type parameters into the
      // module-level type_env so annotations like `(x: T): T` resolve.
      // PKL-110 binds each parameter to TypeVariable(name) instead of
      // UnknownType: now signatures carry a marker that the call-site
      // substitution pass can match on (`identity(7)` binds T = Int and
      // rewrites the return type before it propagates outward).
      FunctionDeclaration(function_decl) =>
        // PKL-116: resolve each parameter's optional bound text in the
        // current type_env so the binding carries a fully resolved
        // bound Type. `unify_for_substitution` consults `bound` to
        // reject call sites whose argument does not flow through the
        // bound. Unbounded parameters keep `bound = None`.
        for i = 0; i < function_decl.type_parameters.length(); i = i + 1 {
          let parameter = function_decl.type_parameters[i]
          let bound_text = if i < function_decl.type_parameter_bounds.length() {
            function_decl.type_parameter_bounds[i]
          } else {
            None
          }
          let bound = match bound_text {
            Some(text) => type_from_annotation(text, type_env)
            None => None
          }
          type_env.push({
            name: parameter,
            typ: TypeVariable(parameter),
            alias_decl: None,
            bound,
          })
        }
    }
  }
  for decl in declarations {
    match decl {
      ClassDeclaration(class_decl) =>
        type_env.push({
          name: class_decl.name,
          typ: ClassType(
            class_decl.name,
            class_decl_to_members(class_decl, type_env, diagnostics),
          ),
          alias_decl: None,
          bound: None,
        })
      TypeAliasDeclaration(type_alias) => {
        // PKL-115: capture the original decl on the binding when the
        // typealias is generic so use-site substitution can rewrite
        // the target text. For non-generic aliases the previous shape
        // is preserved by leaving `alias_decl` as `None`. Inject the
        // declared type parameters as scoped TypeVariables so the
        // target text can resolve at definition time even before any
        // instantiation site is seen.
        let scoped_type_env = copy_type_bindings(type_env)
        for parameter in type_alias.type_parameters {
          scoped_type_env.push({
            name: parameter,
            typ: TypeVariable(parameter),
            alias_decl: None,
            bound: None,
          })
        }
        match
          type_from_alias_target_annotation(type_alias.target, scoped_type_env) {
          Some(typ) =>
            type_env.push({
              name: type_alias.name,
              typ,
              alias_decl: if type_alias.type_parameters.length() > 0 {
                Some(type_alias)
              } else {
                None
              },
              bound: None,
            })
          None =>
            diagnostics.push(diag("Cannot find type `\{type_alias.target}`."))
        }
      }
      FunctionDeclaration(_) => ()
    }
  }
  type_env
}

///|
fn type_exports_from_parse_result(parsed : ParseResult) -> Array[TypeExport] {
  let diagnostics : Array[Diagnostic] = []
  let type_env = collect_declared_types(
    parsed.program.declarations,
    diagnostics,
  )
  let exports : Array[TypeExport] = []
  for declaration in parsed.program.declarations {
    match declaration {
      ClassDeclaration(class_decl) =>
        match lookup_type(type_env, class_decl.name) {
          Some(typ) => exports.push({ name: class_decl.name, typ })
          None => ()
        }
      // PKL-137: typealiases participate in the importing / amending
      // module's type scope the same way classes do. pkspec's
      // `Spec.pkl amends Test.pkl` references `Id`, `ReviewStatus`,
      // `Severity`, `IsoDate`, etc. — all typealiases declared in
      // Test.pkl. Export their resolved types so the relation /
      // import lookup populates the child's type env with them.
      TypeAliasDeclaration(type_alias) =>
        match lookup_type(type_env, type_alias.name) {
          Some(typ) => exports.push({ name: type_alias.name, typ })
          None => ()
        }
      FunctionDeclaration(_) => ()
    }
  }
  exports
}