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

module Lattice
  module Inspections
    # Reports local variables that are declared (assigned) but never read.
    #
    # Strategy: perform a two-pass PSI walk per method body.
    #   Pass 1 — collect every assignment site for local (non-instance) vars.
    #   Pass 2 — collect every read site.
    # Any variable that has an assignment site but zero read sites is flagged.
    #
    # Variables whose names begin with `_` are intentionally ignored (Crystal
    # convention for intentionally-unused bindings).
    class UnusedVariableInspection < 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))
          @method_stack = [] of MethodScope
        end

        def visit(node : ::Crystal::Def) : Bool
          scope = MethodScope.new
          @method_stack.push(scope)
          true
        end

        def end_visit(node : ::Crystal::Def) : Nil
          scope = @method_stack.pop
          scope.assignments.each do |name, assign_loc|
            next if name.starts_with?('_')
            unless scope.reads.includes?(name)
              line = (assign_loc.line_number - 1).clamp(0, Int32::MAX)
              col  = (assign_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 + name.size),
                ),
                "Unused variable '#{name}'",
                ::LSP::DiagnosticSeverity::Warning,
                "lattice",
              )
            end
          end
        end

        def visit(node : ::Crystal::Assign) : Bool
          if (scope = @method_stack.last?) && node.target.is_a?(::Crystal::Var)
            var = node.target.as(::Crystal::Var)
            if loc = node.location
              scope.assignments[var.name] ||= loc
            end
            # Visit the value subtree manually so we record reads there,
            # but skip visiting the target Var (it is a write, not a read).
            node.value.accept(self)
            return false
          end
          true
        end

        def visit(node : ::Crystal::Var) : Bool
          # A Var node that is NOT the target of an Assign is a read.
          if scope = @method_stack.last?
            scope.reads.add(node.name)
          end
          true
        end

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

      private class MethodScope
        property assignments = Hash(String, ::Crystal::Location).new
        property reads       = Set(String).new
      end
    end
  end
end
