defmodule MDEx.DeltaConverter do
  @moduledoc false

  alias MDEx.Document

  @typedoc "Delta operation map"
  @type delta_op :: %{
          required(:insert) => String.t(),
          optional(:attributes) => map()
        }

  @typedoc "Complete Delta document"
  @type delta :: %{ops: [delta_op()]}

  @typedoc "Conversion options"
  @type options :: keyword()

  @doc """
  Convert an MDEx document to Quill Delta format.

  ## Examples

      iex> doc = %MDEx.Document{nodes: [%MDEx.Text{literal: "Hello"}]}
      iex> MDEx.DeltaConverter.convert(doc, %{})
      {:ok, [%{"insert" => "Hello"}]}

  """
  @spec convert(Document.t(), options()) :: {:ok, [delta_op()]} | {:error, term()}
  def convert(%Document{nodes: nodes}, options) do
    try do
      # Initialize list_depth if not provided
      options = Keyword.put_new(options, :list_depth, 0)
      ops = convert_nodes(nodes, [], options)
      {:ok, ops}
    rescue
      error -> {:error, error}
    catch
      error -> {:error, error}
    end
  end

  # Convert a list of nodes, accumulating operations and current formatting state
  @spec convert_nodes([term()], [map()], options()) :: [delta_op()]
  defp convert_nodes([], _current_attrs, _options), do: []

  defp convert_nodes([node | rest], current_attrs, options) do
    node_ops = convert_node(node, current_attrs, options)
    rest_ops = convert_nodes(rest, current_attrs, options)

    # Add extra newline between consecutive paragraphs only
    case {node, rest} do
      {%MDEx.Paragraph{}, [%MDEx.Paragraph{} | _]} ->
        # Paragraph followed by another paragraph - add extra newline for visual separation
        node_ops ++ [%{"insert" => "\n"}] ++ rest_ops

      _ ->
        node_ops ++ rest_ops
    end
  end

  # Convert individual nodes to delta operations
  @spec convert_node(term(), [map()], options()) :: [delta_op()]
  defp convert_node(node, current_attrs, options) do
    # Check for custom converters first
    custom_converters = Keyword.get(options, :custom_converters, %{})

    case Map.get(custom_converters, node.__struct__) do
      converter when is_function(converter, 2) ->
        case converter.(node, options) do
          :skip -> []
          {:error, reason} -> throw({:custom_converter_error, reason})
          ops when is_list(ops) -> ops
          other -> throw({:custom_converter_error, "Custom converter returned invalid value: #{inspect(other)}"})
        end

      nil ->
        # Use default conversion
        default_convert_node(node, current_attrs, options)
    end
  end

  # Default conversion for all node types
  @spec default_convert_node(term(), [map()], options()) :: [delta_op()]
  defp default_convert_node(node, current_attrs, options)

  # Document - process child nodes
  defp default_convert_node(%MDEx.Document{nodes: nodes}, current_attrs, options) do
    convert_nodes(nodes, current_attrs, options)
  end

  # Paragraph - process children, add paragraph break at end
  defp default_convert_node(%MDEx.Paragraph{nodes: nodes}, current_attrs, options) do
    child_ops = convert_nodes(nodes, current_attrs, options)
    child_ops ++ [%{"insert" => "\n"}]
  end

  # Text - insert literal content
  defp default_convert_node(%MDEx.Text{literal: text}, current_attrs, _options) do
    attrs = merge_attributes(current_attrs)

    if map_size(attrs) == 0 do
      [%{"insert" => text}]
    else
      [%{"insert" => text, "attributes" => attrs}]
    end
  end

  # Strong (bold) - add bold attribute to children
  defp default_convert_node(%MDEx.Strong{nodes: nodes}, current_attrs, options) do
    new_attrs = [%{"bold" => true} | current_attrs]
    convert_nodes(nodes, new_attrs, options)
  end

  # Emph (italic) - add italic attribute to children
  defp default_convert_node(%MDEx.Emph{nodes: nodes}, current_attrs, options) do
    new_attrs = [%{"italic" => true} | current_attrs]
    convert_nodes(nodes, new_attrs, options)
  end

  # Code (inline) - add code attribute to children
  defp default_convert_node(%MDEx.Code{literal: code}, current_attrs, _options) do
    attrs = merge_attributes([%{"code" => true} | current_attrs])
    [%{"insert" => code, "attributes" => attrs}]
  end

  # Headings - process children with header attribute on line break
  defp default_convert_node(%MDEx.Heading{nodes: nodes, level: level}, current_attrs, options) do
    child_ops = convert_nodes(nodes, current_attrs, options)
    header_attrs = merge_attributes([%{"header" => level} | current_attrs])
    child_ops ++ [%{"insert" => "\n", "attributes" => header_attrs}]
  end

  # SoftBreak - insert space (Quill doesn't distinguish soft/hard breaks in plain text)
  defp default_convert_node(%MDEx.SoftBreak{}, _current_attrs, _options) do
    [%{"insert" => " "}]
  end

  # LineBreak - insert explicit line break
  defp default_convert_node(%MDEx.LineBreak{}, _current_attrs, _options) do
    [%{"insert" => "\n"}]
  end

  # Phase 2: Exotic Nodes with Custom Attributes

  # Strikethrough - add strike attribute to children
  defp default_convert_node(%MDEx.Strikethrough{nodes: nodes}, current_attrs, options) do
    new_attrs = [%{"strike" => true} | current_attrs]
    convert_nodes(nodes, new_attrs, options)
  end

  # Underline - add underline attribute to children
  defp default_convert_node(%MDEx.Underline{nodes: nodes}, current_attrs, options) do
    new_attrs = [%{"underline" => true} | current_attrs]
    convert_nodes(nodes, new_attrs, options)
  end

  # Link - add link attribute to children
  defp default_convert_node(%MDEx.Link{url: url, nodes: nodes}, current_attrs, options) do
    new_attrs = [%{"link" => url} | current_attrs]
    convert_nodes(nodes, new_attrs, options)
  end

  # Image - use custom insert object
  defp default_convert_node(%MDEx.Image{url: url, title: title}, _current_attrs, _options) do
    image_data = %{"image" => url}
    image_data = if title, do: Map.put(image_data, "alt", title), else: image_data
    [%{"insert" => image_data}]
  end

  # BlockQuote - apply blockquote attribute to newlines
  defp default_convert_node(%MDEx.BlockQuote{nodes: nodes}, current_attrs, options) do
    child_ops = convert_nodes(nodes, current_attrs, options)
    apply_block_format(child_ops, %{"blockquote" => true})
  end

  # CodeBlock - insert code with code-block attribute
  defp default_convert_node(%MDEx.CodeBlock{literal: code, info: info}, _current_attrs, _options) do
    attrs = %{"code-block" => true}
    attrs = if info && info != "", do: Map.put(attrs, "code-block-lang", info), else: attrs

    [
      %{"insert" => code},
      %{"insert" => "\n", "attributes" => attrs}
    ]
  end

  # ThematicBreak (HR) - convert to text representation
  defp default_convert_node(%MDEx.ThematicBreak{}, _current_attrs, _options) do
    [%{"insert" => "***\n"}]
  end

  # Lists - support for bullet and ordered
  defp default_convert_node(%MDEx.List{nodes: items}, current_attrs, options) do
    convert_nodes(items, current_attrs, options)
  end

  defp default_convert_node(%MDEx.ListItem{list_type: list_type, nodes: children}, current_attrs, options) do
    current_depth = Keyword.get(options, :list_depth, 0)

    # Separate paragraph content from nested lists
    {paragraph_content, nested_lists} =
      Enum.split_with(children, fn node ->
        not match?(%MDEx.List{}, node)
      end)

    # Process paragraph content first
    paragraph_ops =
      Enum.flat_map(paragraph_content, fn
        %MDEx.Paragraph{nodes: paragraph_children} ->
          convert_nodes(paragraph_children, current_attrs, options)

        node ->
          convert_node(node, current_attrs, options)
      end)

    # Create list attributes with proper indent
    list_attr =
      case list_type do
        :bullet -> "bullet"
        :ordered -> "ordered"
        _ -> "bullet"
      end

    list_attrs = %{"list" => list_attr}

    list_attrs =
      if current_depth > 0 do
        Map.put(list_attrs, "indent", current_depth)
      else
        list_attrs
      end

    # Add the list item newline
    list_item_ops = paragraph_ops ++ [%{"insert" => "\n", "attributes" => list_attrs}]

    # Process nested lists after the current item with increased depth
    nested_ops =
      Enum.flat_map(nested_lists, fn nested_list ->
        nested_options = Keyword.put(options, :list_depth, current_depth + 1)
        convert_node(nested_list, current_attrs, nested_options)
      end)

    list_item_ops ++ nested_ops
  end

  # TaskItem - block-level attribute on newline
  defp default_convert_node(%MDEx.TaskItem{checked: checked, nodes: children}, current_attrs, options) do
    # Extract content from children (usually paragraphs)
    child_ops =
      Enum.flat_map(children, fn
        %MDEx.Paragraph{nodes: paragraph_children} ->
          convert_nodes(paragraph_children, current_attrs, options)

        node ->
          convert_node(node, current_attrs, options)
      end)

    # Apply task attribute as block-level on the newline
    task_attrs = %{"list" => "bullet", "task" => checked}
    child_ops ++ [%{"insert" => "\n", "attributes" => task_attrs}]
  end

  # FootnoteReference - inline attribute
  defp default_convert_node(%MDEx.FootnoteReference{name: name}, current_attrs, _options) do
    attrs = merge_attributes([%{"footnote_ref" => name} | current_attrs])
    [%{"insert" => "[^#{name}]", "attributes" => attrs}]
  end

  # FootnoteDefinition - block-level attribute on newline
  defp default_convert_node(%MDEx.FootnoteDefinition{name: name, nodes: children}, current_attrs, options) do
    child_ops = convert_nodes(children, current_attrs, options)

    # Apply footnote definition to the final newline
    case List.last(child_ops) do
      %{"insert" => "\n", "attributes" => attrs} ->
        List.replace_at(child_ops, -1, %{
          "insert" => "\n",
          "attributes" => Map.put(attrs, "footnote_definition", name)
        })

      %{"insert" => "\n"} ->
        List.replace_at(child_ops, -1, %{
          "insert" => "\n",
          "attributes" => %{"footnote_definition" => name}
        })

      _ ->
        # No trailing newline, add one with the attribute
        child_ops ++ [%{"insert" => "\n", "attributes" => %{"footnote_definition" => name}}]
    end
  end

  # Subscript and superscript - inline attributes
  defp default_convert_node(%MDEx.Subscript{nodes: children}, current_attrs, options) do
    new_attrs = [%{"subscript" => true} | current_attrs]
    convert_nodes(children, new_attrs, options)
  end

  defp default_convert_node(%MDEx.Superscript{nodes: children}, current_attrs, options) do
    new_attrs = [%{"superscript" => true} | current_attrs]
    convert_nodes(children, new_attrs, options)
  end

  # Math - inline attribute with type
  defp default_convert_node(%MDEx.Math{literal: math, display_math: display?}, current_attrs, _options) do
    math_type = if display?, do: "display", else: "inline"
    attrs = merge_attributes([%{"math" => math_type} | current_attrs])
    [%{"insert" => math, "attributes" => attrs}]
  end

  # Alerts - custom attributes for type and styling
  defp default_convert_node(%MDEx.Alert{alert_type: type, title: title, nodes: children}, current_attrs, options) do
    child_ops = convert_nodes(children, current_attrs, options)

    alert_attrs = %{"alert" => Atom.to_string(type)}
    alert_attrs = if title, do: Map.put(alert_attrs, "alert_title", title), else: alert_attrs

    apply_block_format(child_ops, alert_attrs)
  end

  # SpoileredText - inline attribute
  defp default_convert_node(%MDEx.SpoileredText{nodes: children}, current_attrs, options) do
    new_attrs = [%{"spoiler" => true} | current_attrs]
    convert_nodes(children, new_attrs, options)
  end

  # WikiLinks - link with wikilink attribute
  defp default_convert_node(%MDEx.WikiLink{url: url, nodes: children}, current_attrs, options) do
    new_attrs = [%{"link" => url, "wikilink" => true} | current_attrs]
    convert_nodes(children, new_attrs, options)
  end

  # Tables - standard Quill Delta format with table-cell-line, table-row, and table attributes
  defp default_convert_node(%MDEx.Table{nodes: rows}, current_attrs, options) do
    # Process all rows and collect their ops
    row_ops =
      rows
      |> Enum.with_index()
      |> Enum.flat_map(fn {row, row_index} ->
        row_id = "row-#{row_index + 1}"
        convert_table_row(row, row_id, current_attrs, options)
      end)

    # Add final table newline
    row_ops ++ [%{"insert" => "\n", "attributes" => %{"table" => true}}]
  end

  # ShortCode - already processed to emoji, treat as text
  defp default_convert_node(%MDEx.ShortCode{emoji: emoji}, current_attrs, _options) do
    attrs = merge_attributes(current_attrs)

    if map_size(attrs) == 0 do
      [%{"insert" => emoji}]
    else
      [%{"insert" => emoji, "attributes" => attrs}]
    end
  end

  # Raw - pass through as-is
  defp default_convert_node(%MDEx.Raw{literal: content}, current_attrs, _options) do
    attrs = merge_attributes(current_attrs)

    if map_size(attrs) == 0 do
      [%{"insert" => content}]
    else
      [%{"insert" => content, "attributes" => attrs}]
    end
  end

  # FrontMatter - block-level attribute
  defp default_convert_node(%MDEx.FrontMatter{literal: content}, _current_attrs, _options) do
    [
      %{"insert" => content},
      %{"insert" => "\n", "attributes" => %{"front_matter" => true}}
    ]
  end

  # HTML nodes - block vs inline attributes
  defp default_convert_node(%MDEx.HtmlBlock{literal: html}, _current_attrs, _options) do
    [
      %{"insert" => html},
      %{"insert" => "\n", "attributes" => %{"html" => "block"}}
    ]
  end

  defp default_convert_node(%MDEx.HtmlInline{literal: html}, current_attrs, _options) do
    attrs = merge_attributes([%{"html" => "inline"} | current_attrs])
    [%{"insert" => html, "attributes" => attrs}]
  end

  # Fallback for unknown node types
  defp default_convert_node(node, current_attrs, options) do
    # Unknown node type - try to extract text content or skip
    extract_text_content(node, current_attrs, options)
  end

  # Helper to extract text content from unknown nodes
  defp extract_text_content(%{literal: text}, current_attrs, _options) when is_binary(text) do
    attrs = merge_attributes(current_attrs)

    if map_size(attrs) == 0 do
      [%{"insert" => text}]
    else
      [%{"insert" => text, "attributes" => attrs}]
    end
  end

  defp extract_text_content(%{nodes: nodes}, current_attrs, options) when is_list(nodes) do
    # Try to process child nodes for containers we don't explicitly handle
    convert_nodes(nodes, current_attrs, options)
  end

  defp extract_text_content(_node, _current_attrs, _options) do
    # Unknown node structure - skip it
    []
  end

  # Helper function to convert table rows with proper IDs
  defp convert_table_row(%MDEx.TableRow{header: _is_header, nodes: cells}, row_id, current_attrs, options) do
    # Process all cells in the row
    cell_ops =
      cells
      |> Enum.with_index()
      |> Enum.flat_map(fn {cell, cell_index} ->
        cell_id = "cell-#{cell_index + 1}"
        convert_table_cell(cell, row_id, cell_id, current_attrs, options)
      end)

    # Add row-ending newline
    cell_ops ++ [%{"insert" => "\n", "attributes" => %{"table-row" => row_id}}]
  end

  # Helper function to convert table cells with proper attributes
  defp convert_table_cell(%MDEx.TableCell{nodes: children}, row_id, cell_id, current_attrs, options) do
    # Process cell content recursively for formatting
    cell_content = convert_nodes(children, current_attrs, options)

    # Add table-cell-line attribute to the cell content newline
    # If the cell content ends with a newline (from paragraph), modify it
    case List.last(cell_content) do
      %{"insert" => "\n", "attributes" => attrs} ->
        List.replace_at(cell_content, -1, %{
          "insert" => "\n",
          "attributes" => Map.put(attrs, "table-cell-line", %{"row" => row_id, "cell" => cell_id})
        })

      %{"insert" => "\n"} ->
        List.replace_at(cell_content, -1, %{
          "insert" => "\n",
          "attributes" => %{"table-cell-line" => %{"row" => row_id, "cell" => cell_id}}
        })

      _ ->
        # No trailing newline, add one with table-cell-line attribute
        cell_content ++
          [
            %{
              "insert" => "\n",
              "attributes" => %{"table-cell-line" => %{"row" => row_id, "cell" => cell_id}}
            }
          ]
    end
  end

  # Merge attribute maps from the attribute stack
  @spec merge_attributes([map()]) :: map()
  defp merge_attributes(attr_list) do
    Enum.reduce(attr_list, %{}, fn attrs, acc ->
      Map.merge(acc, attrs)
    end)
  end

  # Apply block formatting to newline characters
  @spec apply_block_format([delta_op()], map()) :: [delta_op()]
  defp apply_block_format(ops, block_attributes) do
    Enum.map(ops, fn op ->
      case op do
        %{"insert" => "\n", "attributes" => attrs} ->
          %{"insert" => "\n", "attributes" => Map.merge(attrs, block_attributes)}

        %{"insert" => "\n"} ->
          %{"insert" => "\n", "attributes" => block_attributes}

        other ->
          other
      end
    end)
  end
end
