defmodule MvWeb.MemberLive.Index.FieldSelection do
  @moduledoc """
  Handles user-specific field selection persistence and URL parameter parsing.

  This module manages:
  - Reading/writing field selection from cookies (persistent storage)
  - Reading/writing field selection from session (temporary storage)
  - Parsing field selection from URL parameters
  - Merging multiple sources with priority: URL > Session > Cookie

  ## Data Format

  Field selection is stored as a map:
  ```elixir
  %{
    "first_name" => true,
    "email" => true,
    "street" => false,
    "custom_field_abc-123" => true
  }
  ```

  ## Cookie/Session Format

  Stored as JSON string: `{"first_name":true,"email":true}`

  ## URL Format

  Comma-separated list: `?fields=first_name,email,custom_field_abc-123`
  """

  @cookie_name "member_field_selection"
  @cookie_max_age 365 * 24 * 60 * 60
  @session_key "member_field_selection"

  @doc """
  Reads field selection from session.

  Returns a map of field names (strings) to boolean visibility values.
  Returns empty map if no selection is stored.
  """
  @spec get_from_session(map()) :: %{String.t() => boolean()}
  def get_from_session(session) when is_map(session) do
    case Map.get(session, @session_key) do
      nil -> %{}
      json_string when is_binary(json_string) -> parse_json(json_string)
      _ -> %{}
    end
  end

  def get_from_session(_), do: %{}

  @doc """
  Saves field selection to session.

  Converts the map to JSON string and stores it in the session.
  """
  @spec save_to_session(map(), %{String.t() => boolean()}) :: map()
  def save_to_session(session, selection) when is_map(selection) do
    json_string = Jason.encode!(selection)
    Map.put(session, @session_key, json_string)
  end

  def save_to_session(session, _), do: session

  @doc """
  Reads field selection from cookie.

  Returns a map of field names (strings) to boolean visibility values.
  Returns empty map if no cookie is present.

  Note: This function parses the raw Cookie header. In LiveView, cookies
  are typically accessed via get_connect_info.
  """
  @spec get_from_cookie(Plug.Conn.t()) :: %{String.t() => boolean()}
  def get_from_cookie(conn) do
    # get_req_header always returns a list ([] if no header, [value] if present)
    case Plug.Conn.get_req_header(conn, "cookie") do
      [] ->
        %{}

      [cookie_header | _rest] ->
        cookies = parse_cookie_header(cookie_header)

        case Map.get(cookies, @cookie_name) do
          nil -> %{}
          json_string when is_binary(json_string) -> parse_json(json_string)
          _ -> %{}
        end
    end
  end

  # Parses cookie header string into a map
  defp parse_cookie_header(cookie_header) when is_binary(cookie_header) do
    cookie_header
    |> String.split(";")
    |> Enum.map(&String.trim/1)
    |> Enum.map(&String.split(&1, "=", parts: 2))
    |> Enum.reduce(%{}, fn
      [key, value], acc -> Map.put(acc, key, URI.decode(value))
      [key], acc -> Map.put(acc, key, "")
      _, acc -> acc
    end)
  end

  defp parse_cookie_header(_), do: %{}

  @doc """
  Saves field selection to cookie.

  Sets a persistent cookie with the field selection as JSON.
  """
  @spec save_to_cookie(Plug.Conn.t(), %{String.t() => boolean()}) :: Plug.Conn.t()
  def save_to_cookie(conn, selection) when is_map(selection) do
    json_string = Jason.encode!(selection)
    secure = Application.get_env(:mv, :use_secure_cookies, false)

    Plug.Conn.put_resp_cookie(conn, @cookie_name, json_string,
      max_age: @cookie_max_age,
      same_site: "Lax",
      http_only: true,
      secure: secure
    )
  end

  def save_to_cookie(conn, _), do: conn

  @doc """
  Parses field selection from URL parameters.

  Expects a comma-separated list of field names in the `fields` parameter.
  All fields in the list are set to `true` (visible).

  ## Examples

      iex> parse_from_url(%{"fields" => "first_name,email"})
      %{"first_name" => true, "email" => true}

      iex> parse_from_url(%{"fields" => "custom_field_abc-123"})
      %{"custom_field_abc-123" => true}

      iex> parse_from_url(%{})
      %{}
  """
  @spec parse_from_url(map()) :: %{String.t() => boolean()}
  def parse_from_url(params) when is_map(params) do
    case Map.get(params, "fields") do
      nil -> %{}
      "" -> %{}
      fields_string when is_binary(fields_string) -> parse_fields_string(fields_string)
      _ -> %{}
    end
  end

  def parse_from_url(_), do: %{}

  @doc """
  Merges multiple field selection sources with priority.

  Priority order (highest to lowest):
  1. URL parameters
  2. Session
  3. Cookie

  Later sources override earlier ones for the same field.

  ## Examples

      iex> merge_sources(%{"first_name" => true}, %{"email" => true}, %{"street" => true})
      %{"first_name" => true, "email" => true, "street" => true}

      iex> merge_sources(%{"first_name" => false}, %{"first_name" => true}, %{})
      %{"first_name" => false}  # URL has priority
  """
  @spec merge_sources(
          %{String.t() => boolean()},
          %{String.t() => boolean()},
          %{String.t() => boolean()}
        ) :: %{String.t() => boolean()}
  def merge_sources(url_selection, session_selection, cookie_selection) do
    %{}
    |> Map.merge(cookie_selection)
    |> Map.merge(session_selection)
    |> Map.merge(url_selection)
  end

  @doc """
  Converts field selection map to URL parameter string.

  Returns a comma-separated string of visible fields (where value is `true`).

  ## Examples

      iex> to_url_param(%{"first_name" => true, "email" => true, "street" => false})
      "first_name,email"
  """
  @spec to_url_param(%{String.t() => boolean()}) :: String.t()
  def to_url_param(selection) when is_map(selection) do
    selection
    |> Enum.filter(fn {_field, visible} -> visible end)
    |> Enum.map_join(",", fn {field, _visible} -> field end)
  end

  def to_url_param(_), do: ""

  # Parses a JSON string into a map, handling errors gracefully
  defp parse_json(json_string) when is_binary(json_string) do
    case Jason.decode(json_string) do
      {:ok, decoded} when is_map(decoded) ->
        # Ensure all values are booleans
        Enum.reduce(decoded, %{}, fn
          {key, value}, acc when is_boolean(value) -> Map.put(acc, key, value)
          {key, _value}, acc -> Map.put(acc, key, true)
        end)

      _ ->
        %{}
    end
  end

  defp parse_json(_), do: %{}

  # Parses a comma-separated string of field names
  defp parse_fields_string(fields_string) do
    fields_string
    |> String.split(",")
    |> Enum.map(&String.trim/1)
    |> Enum.filter(&(&1 != ""))
    |> Enum.reduce(%{}, fn field, acc -> Map.put(acc, field, true) end)
  end
end
