defmodule BerrypodWeb.SiteEditorState do
  @moduledoc """
  Encapsulates Site tab editor state with automatic dirty detection.

  All Site tab state lives in a single struct. Comparing current state against
  original state automatically determines if there are unsaved changes.

  ## Usage

      # Load initial state
      state = SiteEditorState.load()

      # Update a field (returns {new_state, dirty?})
      {state, dirty?} = SiteEditorState.put(state, :announcement_text, "New text")

      # Update a list item
      {state, dirty?} = SiteEditorState.update_social_link(state, id, %{url: "..."})

      # Check if dirty
      SiteEditorState.dirty?(state)

      # Save to database
      state = SiteEditorState.save(state)

      # Revert to original
      state = SiteEditorState.revert(state)
  """

  alias Berrypod.Site
  alias Berrypod.Site.{NavItem, SocialLink}

  defstruct [
    # Simple fields (Settings table)
    :announcement_text,
    :announcement_link,
    :announcement_style,
    :footer_about,
    :footer_copyright,
    :show_newsletter,
    # List fields (separate tables)
    :header_nav,
    :footer_nav,
    :social_links,
    # Original state for dirty detection and revert
    :_original
  ]

  @type t :: %__MODULE__{
          announcement_text: String.t(),
          announcement_link: String.t(),
          announcement_style: String.t(),
          footer_about: String.t(),
          footer_copyright: String.t(),
          show_newsletter: boolean(),
          header_nav: [NavItem.t()],
          footer_nav: [NavItem.t()],
          social_links: [SocialLink.t()],
          _original: map() | nil
        }

  @doc """
  Load Site editor state from database.
  """
  @spec load() :: t()
  def load do
    settings = Site.get_settings()
    header_nav = Site.list_nav_items(:header)
    footer_nav = Site.list_nav_items(:footer)
    social_links = Site.list_social_links()

    state = %__MODULE__{
      announcement_text: settings.announcement_text,
      announcement_link: settings.announcement_link,
      announcement_style: settings.announcement_style,
      footer_about: settings.footer_about,
      footer_copyright: settings.footer_copyright,
      show_newsletter: settings.show_newsletter,
      header_nav: header_nav,
      footer_nav: footer_nav,
      social_links: social_links
    }

    # Store snapshot for dirty detection
    %{state | _original: snapshot(state)}
  end

  @doc """
  Check if state has unsaved changes.
  """
  @spec dirty?(t()) :: boolean()
  def dirty?(%__MODULE__{_original: nil}), do: false

  def dirty?(%__MODULE__{} = state) do
    snapshot(state) != state._original
  end

  @doc """
  Update a simple field. Returns updated state.
  """
  @spec put(t(), atom(), any()) :: t()
  def put(%__MODULE__{} = state, field, value) when is_atom(field) do
    Map.put(state, field, value)
  end

  @doc """
  Add a new social link. Returns updated state.
  """
  @spec add_social_link(t()) :: t()
  def add_social_link(%__MODULE__{social_links: links} = state) do
    new_link = %SocialLink{
      id: Ecto.UUID.generate(),
      platform: "custom",
      url: "",
      position: length(links)
    }

    %{state | social_links: links ++ [new_link]}
  end

  @doc """
  Update a social link by ID. Returns updated state.
  """
  @spec update_social_link(t(), String.t(), map()) :: t()
  def update_social_link(%__MODULE__{social_links: links} = state, id, attrs) do
    updated_links =
      Enum.map(links, fn link ->
        if link.id == id, do: struct(link, attrs), else: link
      end)

    %{state | social_links: updated_links}
  end

  @doc """
  Remove a social link by ID. Returns updated state.
  """
  @spec remove_social_link(t(), String.t()) :: t()
  def remove_social_link(%__MODULE__{social_links: links} = state, id) do
    %{state | social_links: Enum.reject(links, &(&1.id == id))}
  end

  @doc """
  Move a social link up or down. Returns updated state.
  """
  @spec move_social_link(t(), String.t(), :up | :down) :: t()
  def move_social_link(%__MODULE__{social_links: links} = state, id, direction) do
    index = Enum.find_index(links, &(&1.id == id))

    new_index =
      case direction do
        :up -> max(0, index - 1)
        :down -> min(length(links) - 1, index + 1)
      end

    if index != new_index do
      item = Enum.at(links, index)
      other = Enum.at(links, new_index)

      reordered =
        links
        |> List.replace_at(index, other)
        |> List.replace_at(new_index, item)

      %{state | social_links: reordered}
    else
      state
    end
  end

  @doc """
  Add a new nav item for the given location. Returns updated state.
  """
  @spec add_nav_item(t(), String.t()) :: t()
  def add_nav_item(%__MODULE__{} = state, location) do
    items = get_nav_items(state, location)

    new_item = %NavItem{
      id: Ecto.UUID.generate(),
      location: location,
      label: "New link",
      url: "",
      position: length(items)
    }

    put_nav_items(state, location, items ++ [new_item])
  end

  @doc """
  Update a nav item by ID. Returns updated state.
  """
  @spec update_nav_item(t(), String.t(), String.t(), map()) :: t()
  def update_nav_item(%__MODULE__{} = state, location, id, attrs) do
    items = get_nav_items(state, location)

    updated_items =
      Enum.map(items, fn item ->
        if item.id == id, do: struct(item, attrs), else: item
      end)

    put_nav_items(state, location, updated_items)
  end

  @doc """
  Remove a nav item by ID. Returns updated state.
  """
  @spec remove_nav_item(t(), String.t(), String.t()) :: t()
  def remove_nav_item(%__MODULE__{} = state, location, id) do
    items = get_nav_items(state, location)
    put_nav_items(state, location, Enum.reject(items, &(&1.id == id)))
  end

  @doc """
  Move a nav item up or down. Returns updated state.
  """
  @spec move_nav_item(t(), String.t(), String.t(), :up | :down) :: t()
  def move_nav_item(%__MODULE__{} = state, location, id, direction) do
    items = get_nav_items(state, location)
    index = Enum.find_index(items, &(&1.id == id))

    new_index =
      case direction do
        :up -> max(0, index - 1)
        :down -> min(length(items) - 1, index + 1)
      end

    if index != new_index do
      item = Enum.at(items, index)
      other = Enum.at(items, new_index)

      reordered =
        items
        |> List.replace_at(index, other)
        |> List.replace_at(new_index, item)

      put_nav_items(state, location, reordered)
    else
      state
    end
  end

  @doc """
  Save all changes to the database. Returns updated state with new original.
  """
  @spec save(t()) :: t()
  def save(%__MODULE__{} = state) do
    # Save simple settings
    Site.put_announcement(
      state.announcement_text,
      state.announcement_link,
      state.announcement_style
    )

    Site.put_footer_content(
      state.footer_about,
      state.footer_copyright,
      state.show_newsletter
    )

    # Sync nav items and social links
    header_nav = sync_nav_items(state.header_nav, state._original.header_nav)
    footer_nav = sync_nav_items(state.footer_nav, state._original.footer_nav)
    social_links = sync_social_links(state.social_links, state._original.social_links)

    # Update state with database-assigned IDs and new original
    state = %{state | header_nav: header_nav, footer_nav: footer_nav, social_links: social_links}
    %{state | _original: snapshot(state)}
  end

  @doc """
  Revert to original state. Returns reset state.
  """
  @spec revert(t()) :: t()
  def revert(%__MODULE__{_original: original} = state) when not is_nil(original) do
    %{
      state
      | announcement_text: original.announcement_text,
        announcement_link: original.announcement_link,
        announcement_style: original.announcement_style,
        footer_about: original.footer_about,
        footer_copyright: original.footer_copyright,
        show_newsletter: original.show_newsletter,
        header_nav: original.header_nav,
        footer_nav: original.footer_nav,
        social_links: original.social_links
    }
  end

  # ── Private helpers ──────────────────────────────────────────────────

  defp get_nav_items(%__MODULE__{header_nav: items}, "header"), do: items
  defp get_nav_items(%__MODULE__{footer_nav: items}, "footer"), do: items

  defp put_nav_items(state, "header", items), do: %{state | header_nav: items}
  defp put_nav_items(state, "footer", items), do: %{state | footer_nav: items}

  # Create a comparable snapshot of the state (excludes _original)
  defp snapshot(%__MODULE__{} = state) do
    %{
      announcement_text: state.announcement_text,
      announcement_link: state.announcement_link,
      announcement_style: state.announcement_style,
      footer_about: state.footer_about,
      footer_copyright: state.footer_copyright,
      show_newsletter: state.show_newsletter,
      header_nav: Enum.map(state.header_nav, &nav_item_snapshot/1),
      footer_nav: Enum.map(state.footer_nav, &nav_item_snapshot/1),
      social_links: Enum.map(state.social_links, &social_link_snapshot/1)
    }
  end

  # Only compare fields that matter for dirty detection
  defp nav_item_snapshot(%NavItem{} = item) do
    %{id: item.id, label: item.label, url: item.url}
  end

  defp social_link_snapshot(%SocialLink{} = link) do
    %{id: link.id, platform: link.platform, url: link.url}
  end

  # Sync nav items - create new, update changed, delete removed
  defp sync_nav_items(current, original) do
    original_ids = MapSet.new(Enum.map(original, & &1.id))
    current_ids = MapSet.new(Enum.map(current, & &1.id))

    # Delete items that were removed
    deleted_ids = MapSet.difference(original_ids, current_ids)

    for id <- deleted_ids do
      item = Enum.find(original, &(&1.id == id))
      if item, do: Site.delete_nav_item(item)
    end

    # Create or update items
    Enum.map(current, fn item ->
      if MapSet.member?(original_ids, item.id) do
        # Existing item - update if changed
        orig = Enum.find(original, &(&1.id == item.id))

        if orig.label != item.label or orig.url != item.url do
          {:ok, updated} = Site.update_nav_item(orig, %{label: item.label, url: item.url})
          updated
        else
          orig
        end
      else
        # New item - create
        {:ok, created} =
          Site.create_nav_item(%{
            location: item.location,
            label: item.label,
            url: item.url,
            position: item.position
          })

        created
      end
    end)
  end

  # Sync social links - create new, update changed, delete removed
  defp sync_social_links(current, original) do
    original_ids = MapSet.new(Enum.map(original, & &1.id))
    current_ids = MapSet.new(Enum.map(current, & &1.id))

    # Delete links that were removed
    deleted_ids = MapSet.difference(original_ids, current_ids)

    for id <- deleted_ids do
      link = Enum.find(original, &(&1.id == id))
      if link, do: Site.delete_social_link(link)
    end

    # Create or update links
    Enum.map(current, fn link ->
      if MapSet.member?(original_ids, link.id) do
        # Existing link - update if changed
        orig = Enum.find(original, &(&1.id == link.id))

        if orig.platform != link.platform or orig.url != link.url do
          {:ok, updated} =
            Site.update_social_link(orig, %{platform: link.platform, url: link.url})

          updated
        else
          orig
        end
      else
        # New link - create
        {:ok, created} =
          Site.create_social_link(%{
            platform: link.platform,
            url: link.url,
            position: link.position
          })

        created
      end
    end)
  end
end
