# Copyright 2024 Adobe. All rights reserved.
# Copyright 2025 SmartRent. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.

defmodule Quokka.Style.Configs do
  @moduledoc """
  Orders `Config.config/2,3` stanzas in configuration files.

  - ordering is done only within immediate-sibling config statements
  - assignments are moved above the configuration blocks
  - any non `config/2,3` or assignment (`=/2`) calls mark the end of a sorting block.
    this is support having conditional blocks (`if/case/cond`) and `import_config` stanzas between blocks

  ### Breakages

  If you configure the same values multiple times, Quokka may swap their orders

  **Before**

    line 04: config :foo, bar: :zab
    line 40: config :foo, bar: :baz

    # Application.fetch_env!(:foo)[:bar] => :baz

  **After**

    line 04: config :foo, bar: :baz
    line 05: config :foo, bar: :zab

    # Application.fetch_env!(:foo)[:bar] => :zab

  **Fix**

  The reason Quokka sorts configuration is to help you noticed these duplicated configuration stanzas.
  Delete the duplicative/erroneous stanza and life will be good.
  """

  alias Quokka.Style

  def run({{:import, _, [{:__aliases__, _, [:Config]}]}, _} = zipper, %{config?: true} = ctx) do
    {:skip, zipper, Map.put(ctx, :mix_config?, true)}
  end

  def run({{:config, cfm, [_, _ | _]} = config, zm}, %{mix_config?: true, comments: comments} = ctx) do
    # all of these list are reversed due to the reduce
    {configs, assignments, rest} = accumulate(zm.r, [], [])
    # @TODO
    # okay so comments between nodes that we moved.......
    # lets just push them out of the way (???). so
    # 1. figure out first/last possible lines we're talking about here
    # 2. only pass comments in that range off
    # 3. split those comments into "moved, didn't move"
    # 4. for any "didn't move" comments... move them to the top?
    #
    # also, should i just do a scan of the configs ++ assignments, and see if any of them have lines out of order,
    # and decide from there whether or not i want to do set_lines

    configs =
      [config | configs]
      |> Enum.group_by(fn
        {:config, _, [{:__block__, _, [app]} | _]} -> app
        {:config, _, [arg | _]} -> Style.without_meta(arg)
      end)
      |> Enum.sort()
      |> Enum.flat_map(fn {_app, configs} ->
        configs
        |> Enum.sort_by(&Style.without_meta/1)
        |> Style.reset_newlines()
      end)

    nodes =
      assignments
      |> Enum.reverse()
      |> Style.reset_newlines()
      |> Enum.concat(configs)

    {nodes, comments} =
      if changed?(nodes) do
        # after running, this block should take up the same # of lines that it did before
        # the first node of `rest` is greater than the highest line in configs, assignments
        # config line is the first line to be used as part of this block
        {node_comments, _} = Style.comments_for_node(config, comments)
        first_line = min(List.last(node_comments)[:line] || cfm[:line], cfm[:line])
        Style.order_line_meta_and_comments(nodes, comments, first_line)
      else
        {nodes, comments}
      end

    [config | left_siblings] = Enum.reverse(nodes, zm.l)

    {:skip, {config, %{zm | l: left_siblings, r: rest}}, %{ctx | comments: comments}}
  end

  def run(zipper, %{config?: true} = ctx) do
    {:cont, zipper, ctx}
  end

  def run(zipper, %{file: file} = ctx) do
    if file =~ ~r|config/.*\.exs| or file =~ ~r|rel/overlays/.*\.exs| do
      # @TODO have this run forward to `import Config`, then run forward from there until we find `config` itself. no need for multi function head
      {:cont, zipper, Map.put(ctx, :config?, true)}
    else
      {:halt, zipper, ctx}
    end
  end

  defp changed?([{_, am, _}, {_, bm, _} = b | tail]) do
    if am[:line] > bm[:line], do: true, else: changed?([b | tail])
  end

  defp changed?(_), do: false

  defp accumulate([{:config, _, [_, _ | _]} = c | siblings], cs, as), do: accumulate(siblings, [c | cs], as)

  defp accumulate([{:=, _, [_lhs, _rhs]} = a | siblings], cs, as), do: accumulate(siblings, cs, [a | as])

  defp accumulate(rest, configs, assignments), do: {configs, assignments, rest}
end
