defmodule FirehoseWeb.MicroprintsLive do
  use FirehoseWeb, :live_view

  alias Microprints.MicroprintCache
  alias Microprints.MicroprintComponent

  @source_dirs ["app", "blogex"]

  @impl true
  def mount(params, _session, socket) do
    files = scan_source_files_with_dir()

    microprints =
      files
      |> Enum.map(&process_file/1)

    {:ok,
     socket
     |> assign(:page_title, "Microprints")
     |> assign(:microprints, microprints)
     |> restore_state_from_params(params)}
  rescue
    e ->
      {:ok,
       socket
       |> assign(:page_title, "Microprints")
       |> assign(:microprints, [])
       |> assign(:expanded_path, nil)
       |> assign(:source_lines, nil)
       |> assign(:highlighted_path, nil)
       |> assign(:highlighted_line, nil)
       |> put_flash(:error, "Error loading microprints: #{inspect(e)}")}
  end

  @impl true
  def handle_params(params, _uri, socket) do
    {:noreply, restore_state_from_params(socket, params)}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div class="max-w-4xl mx-auto">
      <h1 class="text-2xl font-bold mb-6">Microprints</h1>
      <p class="text-sm text-base-content/70 mb-4">
        Visual fingerprints of source code files. Click a line to highlight it.
        Click a card to expand and view the source.
      </p>

      <.microprint_legend />

      <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-6">
        <%= for %{path: path, microprint: microprint, source_dir: _source_dir} = item <- @microprints do %>
          {error = item[:error]}
          <div class="card bg-base-100 shadow-sm border border-zinc-200">
            <div class="card-body p-4">
              <h3 class="text-sm font-mono font-medium truncate" title={path}>
                {path}
              </h3>

              <%= if microprint do %>
                <button
                  phx-click="toggle_expand"
                  phx-value-path={path}
                  class="btn btn-xs btn-ghost w-full"
                >
                  <%= if @expanded_path == path do %>
                    Collapse
                  <% else %>
                    Expand
                  <% end %>
                </button>

                <.microprint
                  microprint={microprint}
                  width={200}
                  max_height={100}
                  clickable={true}
                  file_path={path}
                  highlighted_line={@highlighted_line}
                />

                <%= if @expanded_path == path and @source_lines do %>
                  <.source_viewer
                    source_lines={@source_lines}
                    highlighted_line={@highlighted_line}
                    language="elixir"
                    file_path={path}
                  />
                <% end %>
              <% else %>
                <div class="text-xs text-red-500 mt-1">
                  Error: {inspect(error)}
                </div>
              <% end %>
            </div>
          </div>
        <% end %>
      </div>
    </div>
    """
  end

  @impl true
  def handle_event("highlight_line", %{"line" => line, "path" => path}, socket) do
    highlighted =
      case socket.assigns.highlighted_path do
        ^path -> nil
        _ -> String.to_integer(line)
      end

    # Collapse any currently expanded file when highlighting a different file
    expanded =
      case socket.assigns.expanded_path do
        ^path -> socket.assigns.expanded_path
        _ -> nil
      end

    socket =
      socket
      |> assign(:highlighted_path, path)
      |> assign(:highlighted_line, highlighted)
      |> assign(:expanded_path, expanded)
      |> assign(:source_lines, nil)

    {:noreply, push_patch(socket, to: build_params(socket))}
  end

  @impl true
  def handle_event("toggle_expand", %{"path" => path}, socket) do
    expanded =
      case socket.assigns.expanded_path do
        ^path -> nil
        _ -> path
      end

    source_lines =
      case expanded do
        nil ->
          nil

        ^path ->
          item = Enum.find(socket.assigns.microprints, &(&1.path == path))

          if item && item.source_dir do
            abs_path = resolve_source_path(item.source_dir, path)
            read_source(abs_path)
          else
            nil
          end
      end

    socket = socket |> assign(:expanded_path, expanded) |> assign(:source_lines, source_lines)

    {:noreply, push_patch(socket, to: build_params(socket))}
  end

  # Private helpers

  defp restore_state_from_params(socket, params) do
    expanded_path = params["expanded"]
    highlighted_path = params["highlighted"]
    highlighted_line = if params["line"], do: String.to_integer(params["line"]), else: nil

    source_lines =
      if expanded_path do
        item = Enum.find(socket.assigns.microprints, &(&1.path == expanded_path))

        if item && item.source_dir do
          abs_path = resolve_source_path(item.source_dir, expanded_path)
          read_source(abs_path)
        end
      end

    socket
    |> assign(:expanded_path, expanded_path)
    |> assign(:source_lines, source_lines)
    |> assign(:highlighted_path, highlighted_path)
    |> assign(:highlighted_line, highlighted_line)
  end

  defp build_params(socket) do
    params = %{}

    params =
      if socket.assigns.expanded_path do
        Map.put(params, "expanded", socket.assigns.expanded_path)
      else
        params
      end

    params =
      if socket.assigns.highlighted_path do
        Map.put(params, "highlighted", socket.assigns.highlighted_path)
      else
        params
      end

    params =
      if socket.assigns.highlighted_line do
        params |> Map.put("line", socket.assigns.highlighted_line)
      else
        params
      end

    "/microprints" <> if params != %{}, do: "?" <> URI.encode_query(params), else: ""
  end

  defp sort_by_mtime(files) do
    app_root = Mix.Project.project_file() |> Path.dirname()

    files
    |> Enum.map(fn file ->
      abs_path = Path.join(app_root, file)
      {file, File.stat!(abs_path).mtime}
    end)
    |> Enum.sort_by(fn {_file, mtime} -> mtime end, :desc)
    |> Enum.map(fn {file, _mtime} -> file end)
  end

  defp sort_by_mtime_with_dir(tuples) do
    app_root = Mix.Project.project_file() |> Path.dirname()

    tuples
    |> Enum.map(fn {path, dir} ->
      abs_path = Path.join(app_root, path)
      {{path, dir}, File.stat!(abs_path).mtime}
    end)
    |> Enum.sort_by(fn {{_path, _dir}, mtime} -> mtime end, :desc)
    |> Enum.map(fn {{path, dir}, _mtime} -> {path, dir} end)
  end

  @doc false
  def scan_source_files do
    @source_dirs
    |> Enum.flat_map(&collect_elixir_files/1)
    |> Enum.uniq_by(&elem(&1, 0))
    |> Enum.map(&elem(&1, 0))
    |> Enum.sort()
    |> sort_by_mtime()
    |> Enum.take(2)
  end

  @doc false
  def scan_source_files_with_dir do
    @source_dirs
    |> Enum.flat_map(&collect_elixir_files/1)
    |> Enum.uniq_by(&elem(&1, 0))
    |> Enum.sort_by(&elem(&1, 0))
    |> sort_by_mtime_with_dir()
    |> Enum.take(2)
  end

  defp resolve_absolute_paths(files) do
    app_root = Mix.Project.project_file() |> Path.dirname()

    Enum.map(files, fn path ->
      Path.expand(Path.join(app_root, path))
    end)
  end

  defp excluded_path?(path) do
    path =~ "/_build/" or
      path =~ "/deps/" or
      path =~ "/examples/" or
      path =~ "/test/" or
      path =~ "/lib_dev/"
  end

  defp collect_elixir_files(dir) do
    app_root = Mix.Project.project_file() |> Path.dirname()
    monorepo_root = app_root |> Path.dirname()
    base = Path.join(monorepo_root, dir)

    case File.dir?(base) do
      true ->
        base
        |> Path.join("**/*.ex")
        |> Path.wildcard()
        |> Enum.filter(&File.regular?(&1))
        |> Enum.reject(&excluded_path?/1)
        |> Enum.map(&format_path_and_dir(&1, monorepo_root, dir))

      false ->
        []
    end
  end

  defp format_path_and_dir(path, monorepo_root, dir) do
    relative = Path.relative_to(path, monorepo_root)

    formatted =
      case dir do
        "app" -> String.replace_prefix(relative, "app/", "")
        _ -> "../" <> relative
      end

    {formatted, dir}
  end

  defp process_file({rel_path, source_dir}) do
    abs_path = resolve_absolute_paths([rel_path]) |> List.first()

    case MicroprintCache.get_microprint(abs_path) do
      {:ok, microprint} ->
        # Add line numbers to each line for highlighting
        lines_with_numbers =
          microprint.lines
          |> Enum.with_index(1)
          |> Enum.map(fn {line, num} -> Map.put(line, :line_number, num) end)

        %{
          path: rel_path,
          source_dir: source_dir,
          microprint: Map.put(microprint, :lines, lines_with_numbers)
        }

      {:error, reason} ->
        %{path: rel_path, microprint: nil, error: reason}
    end
  end

  defp resolve_source_path(source_dir, rel_path) do
    app_root = Mix.Project.project_file() |> Path.dirname()
    monorepo_root = app_root |> Path.dirname()
    base = Path.join(monorepo_root, source_dir)
    Path.join(base, rel_path)
  end

  defp read_source(abs_path) do
    case File.read(abs_path) do
      {:ok, content} -> String.split(content, "\n")
      {:error, _} -> nil
    end
  end

  # Delegate to MicroprintComponent
  defdelegate microprint(assigns), to: MicroprintComponent
  defdelegate microprint_legend(assigns), to: MicroprintComponent

  # Custom source_viewer with unique DOM IDs per file to prevent LiveView
  # DOM patching bugs when switching expanded files.
  def source_viewer(assigns) do
    assigns =
      assign(
        assigns,
        :viewer_id,
        "source-viewer-" <> Integer.to_string(:erlang.phash2(assigns.file_path, 1_000_000))
      )

    ~H"""
    <div
      id={@viewer_id}
      class="source-viewer mt-2 max-h-96 overflow-auto rounded font-mono text-xs"
      phx-hook="SourceViewer"
      data-highlighted-line={@highlighted_line}
      data-language={@language}
      style="background: var(--sv-bg); color: var(--sv-text);"
    >
      <div class="sv-lines p-2">
        <%= for {line, num} <- Enum.with_index(@source_lines || [], 1) do %>
          <div
            id={"line-#{num}"}
            class={"sv-line" <> if @highlighted_line == num, do: " sv-line-highlighted", else: ""}
          >
            <span class="sv-line-number">{num}</span>
            <span class="sv-line-content">{line}</span>
          </div>
        <% end %>
      </div>
    </div>
    """
  end
end
