require "../../lsp/protocol"
require "../workspace_analyzer"

module Lattice
  # IndexingManager drives background re-indexing of workspace files using a
  # Crystal `Channel`-based work queue processed on a dedicated `Fiber`.
  # Callers enqueue URIs (or the sentinel `SCAN_ALL`) and the manager pulls
  # tasks off the channel, runs `WorkspaceAnalyzer#force_scan` or a
  # per-file update, then emits LSP `$/progress` work-done notifications so
  # the editor can display indexing status in its status bar.
  #
  # Progress tokens are registered with the client via
  # `window/workDoneProgress/create` before the first `$/progress begin`
  # notification is sent, which is required for Zed (and any LSP 3.16-compliant
  # client) to actually render the spinner.
  class IndexingManager
    # Sentinel value: enqueue this URI string to trigger a full workspace scan.
    SCAN_ALL = "__LATTICE_SCAN_ALL__"

    # Maximum number of pending work items before the channel blocks callers.
    QUEUE_CAPACITY = 128

    @work_queue : Channel(String)
    @workspace_analyzer : WorkspaceAnalyzer
    @send_notification : Proc(String, JSON::Any?, Nil)
    @send_request : Proc(String | Int32, String, JSON::Any?, Nil)
    @running = false
    @progress_token : Int32 = 0

    # *workspace_analyzer* – the shared `WorkspaceAnalyzer` instance.
    # *send_notification* – callback that forwards a JSON-RPC notification to
    # the client; matches the signature of `JsonRpcHandler#send_notification`.
    # *send_request* – callback that sends a server-initiated JSON-RPC request
    # to the client (used for `window/workDoneProgress/create`).
    def initialize(
      @workspace_analyzer : WorkspaceAnalyzer,
      @send_notification : Proc(String, JSON::Any?, Nil),
      @send_request : Proc(String | Int32, String, JSON::Any?, Nil),
    )
      @work_queue = Channel(String).new(QUEUE_CAPACITY)
    end

    # Start the background indexing fiber. Safe to call once; subsequent calls
    # are no-ops.
    def start
      return if @running
      @running = true

      spawn do
        loop do
          uri = @work_queue.receive? || break
          process(uri)
        end
      end

      STDERR.puts "(Lattice) IndexingManager started."
    end

    # Enqueue a single file URI for re-indexing.  Non-blocking: if the queue
    # is full the item is silently dropped (the file will be caught by the next
    # full scan).
    def enqueue(uri : String)
      @work_queue.send(uri) unless @work_queue.closed?
    rescue Channel::ClosedError
      # Server is shutting down; ignore.
    end

    # Enqueue a full workspace scan.
    def enqueue_full_scan
      enqueue(SCAN_ALL)
    end

    # Stop the background fiber by closing the work channel.
    def stop
      @work_queue.close unless @work_queue.closed?
      @running = false
    end

    # ---------------------------------------------------------------------------
    # Private helpers
    # ---------------------------------------------------------------------------

    private def process(uri : String)
      if uri == SCAN_ALL
        run_full_scan
      else
        run_file_scan(uri)
      end
    end

    private def run_full_scan
      token = next_token
      create_progress(token)
      begin_progress(token, "Lattice: Indexing workspace…")
      begin
        @workspace_analyzer.force_scan
        report_progress(token, "Done")
      rescue ex
        STDERR.puts "(Lattice) IndexingManager full-scan error: #{ex.message}"
      ensure
        end_progress(token)
      end
    end

    private def run_file_scan(uri : String)
      token = next_token
      create_progress(token)
      begin_progress(token, "Lattice: Indexing #{File.basename(uri_to_path(uri))}…")
      begin
        if source = read_source(uri)
          @workspace_analyzer.update_source(uri, source)
        end
        report_progress(token, "Done")
      rescue ex
        STDERR.puts "(Lattice) IndexingManager file-scan error for #{uri}: #{ex.message}"
      ensure
        end_progress(token)
      end
    end

    # ---------------------------------------------------------------------------
    # Progress notification helpers (LSP §3.16 – work done progress)
    # ---------------------------------------------------------------------------

    # Send `window/workDoneProgress/create` to register *token* with the client
    # before emitting any `$/progress` notifications for it.  Per the LSP spec,
    # a server-initiated progress token MUST be registered this way first.
    private def create_progress(token : Int32)
      params = JSON::Any.new({
        "token" => JSON::Any.new(token.to_i64),
      })
      @send_request.call("lattice/progress/#{token}", "window/workDoneProgress/create", params)
    rescue ex
      STDERR.puts "(Lattice) IndexingManager progress create error: #{ex.message}"
    end

    # Emit `$/progress` begin for *token*.
    private def begin_progress(token : Int32, title : String)
      value = {
        "kind"        => JSON::Any.new("begin"),
        "title"       => JSON::Any.new(title),
        "cancellable" => JSON::Any.new(false),
      }
      send_progress(token, value)
    end

    # Emit `$/progress` report for *token*.
    private def report_progress(token : Int32, message : String)
      value = {
        "kind"    => JSON::Any.new("report"),
        "message" => JSON::Any.new(message),
      }
      send_progress(token, value)
    end

    # Emit `$/progress` end for *token*.
    private def end_progress(token : Int32)
      value = {
        "kind" => JSON::Any.new("end"),
      }
      send_progress(token, value)
    end

    private def send_progress(token : Int32, value : Hash(String, JSON::Any))
      params = JSON::Any.new({
        "token" => JSON::Any.new(token.to_i64),
        "value" => JSON::Any.new(value),
      })
      @send_notification.call("$/progress", params)
    rescue ex
      STDERR.puts "(Lattice) IndexingManager progress send error: #{ex.message}"
    end

    private def next_token : Int32
      @progress_token += 1
    end

    # ---------------------------------------------------------------------------
    # Utility
    # ---------------------------------------------------------------------------

    private def uri_to_path(uri : String) : String
      return uri unless uri.starts_with?("file://")
      path = uri.sub(/^file:\/\//, "")
      path = URI.decode(path)
      if path =~ /^\/([a-zA-Z]):/
        path = path[1..]
      end
      path.gsub('/', File::SEPARATOR)
    end

    private def read_source(uri : String) : String?
      path = uri_to_path(uri)
      return unless File.exists?(path)
      File.read(path)
    rescue
      nil
    end
  end
end
