require "./inspection"
require "compiler/crystal/syntax"

module Lattice
  module Inspections
    # Warns when a method declares a non-Nil return type annotation but one or
    # more code paths reach the end of the body without an explicit `return`
    # expression.
    #
    # Crystal's compiler catches true type errors at compile time, but this
    # inspection surfaces the issue in the editor before a build, following the
    # same pattern as velvet's `missing_return` inspection.
    #
    # Heuristic approach (no full type-flow graph):
    #   - Parse the explicit return-type annotation from the `def` signature.
    #   - If the annotation is absent or is `Nil` / `Void`, skip.
    #   - Walk the top-level statements of the method body.
    #   - If the last statement is NOT a `Return`, `Raise`, or a conditional
    #     whose every branch ends with one of those, emit a warning.
    class MissingReturnInspection < Inspection
      def run(
        uri : String,
        source : String,
        lines : Array(String),
        ast : ::Crystal::ASTNode,
      ) : Array(::LSP::Diagnostic)
        diagnostics = [] of ::LSP::Diagnostic
        visitor = Visitor.new(diagnostics)
        ast.accept(visitor)
        diagnostics
      end

      private class Visitor < ::Crystal::Visitor
        def initialize(@diagnostics : Array(::LSP::Diagnostic))
        end

        def visit(node : ::Crystal::Def) : Bool
          # Only inspect methods that carry an explicit non-nil return type.
          return_type = node.return_type
          return false unless return_type
          type_name = return_type.to_s.strip
          return false if type_name.empty? || type_name == "Nil" || type_name == "Void"

          body = node.body
          return false if always_returns?(body)

          if loc = node.location
            line = (loc.line_number - 1).clamp(0, Int32::MAX)
            col  = (loc.column_number - 1).clamp(0, Int32::MAX)
            @diagnostics << ::LSP::Diagnostic.new(
              ::LSP::Range.new(
                ::LSP::Position.new(line, col),
                ::LSP::Position.new(line, col + "def".size + 1 + node.name.size),
              ),
              "Method '#{node.name}' declares return type '#{type_name}' but may not return a value on all paths",
              ::LSP::DiagnosticSeverity::Warning,
              "lattice",
            )
          end

          # Do not descend — nested defs are handled by their own visit calls.
          false
        end

        def visit(node : ::Crystal::ASTNode) : Bool
          true
        end

        # Returns true if *node* unconditionally terminates (explicit return,
        # raise, or a conditional where every branch terminates).
        private def always_returns?(node : ::Crystal::ASTNode) : Bool
          case node
          when ::Crystal::Return
            true
          when ::Crystal::Call
            node.name == "raise"
          when ::Crystal::Expressions
            node.expressions.any? { |e| always_returns?(e) }
          when ::Crystal::If
            always_returns?(node.then) && always_returns?(node.else)
          when ::Crystal::Case
            all_whens = node.whens.all? { |w| always_returns?(w.body) }
            else_ok   = node.else ? always_returns?(node.else.not_nil!) : false
            all_whens && else_ok
          else
            false
          end
        end
      end
    end
  end
end
