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

module Lattice
  module Inspections
    # Shells out to the `ameba` binary (if present on PATH) to run Crystal's
    # community linter and maps its JSON output into LSP::Diagnostic objects.
    #
    # The following rules are disabled while the user is actively typing to
    # avoid distracting noise, matching the behaviour of ameba-ls:
    #   - Layout/TrailingBlankLines
    #   - Layout/TrailingWhitespace
    #   - Lint/Formatting
    #
    # Severity mapping:
    #   Ameba Error      → LSP Error
    #   Ameba Warning    → LSP Warning
    #   Ameba Convention → LSP Information
    class AmebaInspection < Inspection
      DISABLED_WHILE_TYPING = %w[
        Layout/TrailingBlankLines
        Layout/TrailingWhitespace
        Lint/Formatting
      ]

      # Seconds before the ameba subprocess is considered hung.
      TIMEOUT_SECONDS = 10

      def run(
        uri : String,
        source : String,
        lines : Array(String),
        ast : ::Crystal::ASTNode,
      ) : Array(::LSP::Diagnostic)
        return [] of ::LSP::Diagnostic unless ameba_available?

        run_ameba(uri, source)
      end

      # Exposed so the code-action provider can query which issues are
      # correctable without re-running the full inspection pipeline.
      def correctable_issues(uri : String, source : String) : Array(AmebaIssue)
        return [] of AmebaIssue unless ameba_available?

        json_output = run_ameba_raw(uri, source)
        return [] of AmebaIssue if json_output.nil? || json_output.empty?

        parse_issues(json_output).select(&.correctable)
      end

      private def ameba_available? : Bool
        Process.find_executable("ameba") != nil
      end

      private def run_ameba(uri : String, source : String) : Array(::LSP::Diagnostic)
        json_output = run_ameba_raw(uri, source)
        return [] of ::LSP::Diagnostic if json_output.nil? || json_output.empty?

        issues = parse_issues(json_output)
        issues.map { |issue| issue_to_diagnostic(issue) }.compact
      end

      private def run_ameba_raw(uri : String, source : String) : String?
        # Write source to a temp file so ameba can read it.
        tmp = File.tempname("lattice-ameba-", ".cr")
        File.write(tmp, source)

        output_io = IO::Memory.new
        error_io  = IO::Memory.new

        args = ["--format", "json", "--no-color"]

        # Disable noisy-while-typing rules.
        DISABLED_WHILE_TYPING.each do |rule|
          args << "--except" << rule
        end

        args << tmp

        status = nil
        begin
          # Use a watchdog channel to enforce the hard timeout.
          done_ch = Channel(Nil).new

          spawn do
            status = Process.run(
              "ameba",
              args,
              output: output_io,
              error: error_io,
            )
            done_ch.send(nil)
          end

          select
          when done_ch.receive
            # Completed normally.
          when timeout(TIMEOUT_SECONDS.seconds)
            STDERR.puts "[lattice] ameba timed out — killing subprocess"
            return nil
          end
        rescue ex
          STDERR.puts "[lattice] ameba error: #{ex.message}"
          return nil
        ensure
          File.delete(tmp) if File.exists?(tmp)
        end

        output_io.to_s
      end

      # Parse ameba's JSON output format into internal structs.
      private def parse_issues(json : String) : Array(AmebaIssue)
        issues = [] of AmebaIssue
        begin
          root = JSON.parse(json)
          sources = root["sources"]?.try(&.as_a?) || [] of JSON::Any
          sources.each do |src|
            (src["issues"]?.try(&.as_a?) || [] of JSON::Any).each do |issue|
              rule      = issue["rule_name"]?.try(&.as_s?) || "unknown"
              message   = issue["message"]?.try(&.as_s?) || ""
              severity  = issue["severity"]?.try(&.as_s?) || "convention"
              correctable = issue["correctable"]?.try(&.as_bool?) || false

              loc     = issue["location"]?
              end_loc = issue["end_location"]?

              start_line   = ((loc.try(&.["line"]?.try(&.as_i?)) || 1) - 1).clamp(0, Int32::MAX)
              start_col    = ((loc.try(&.["column"]?.try(&.as_i?)) || 1) - 1).clamp(0, Int32::MAX)
              finish_line  = ((end_loc.try(&.["line"]?.try(&.as_i?)) || start_line + 1) - 1).clamp(0, Int32::MAX)
              finish_col   = (end_loc.try(&.["column"]?.try(&.as_i?)) || start_col + 1).clamp(0, Int32::MAX)

              issues << AmebaIssue.new(
                rule:        rule,
                message:     message,
                severity:    severity,
                correctable: correctable,
                start_line:  start_line,
                start_col:   start_col,
                end_line:    finish_line,
                end_col:     finish_col,
              )
            end
          end
        rescue ex
          STDERR.puts "[lattice] Failed to parse ameba JSON output: #{ex.message}"
        end
        issues
      end

      private def issue_to_diagnostic(issue : AmebaIssue) : ::LSP::Diagnostic
        severity = case issue.severity.downcase
                   when "error"      then ::LSP::DiagnosticSeverity::Error
                   when "warning"    then ::LSP::DiagnosticSeverity::Warning
                   else                   ::LSP::DiagnosticSeverity::Information
                   end

        range = ::LSP::Range.new(
          ::LSP::Position.new(issue.start_line, issue.start_col),
          ::LSP::Position.new(issue.end_line, issue.end_col),
        )

        ::LSP::Diagnostic.new(
          range,
          "[#{issue.rule}] #{issue.message}",
          severity,
          "ameba",
        )
      end
    end

    # Value object representing a single ameba issue.
    record AmebaIssue,
      rule : String,
      message : String,
      severity : String,
      correctable : Bool,
      start_line : Int32,
      start_col : Int32,
      end_line : Int32,
      end_col : Int32
  end
end
