defmodule ZoeyscomputerWeb.GistLive.OgImage do
  @moduledoc """
  Generates OpenGraph preview images for Gists using SVG with syntax highlighting.
  """
  require Logger

  @external_resource "priv/static/fonts/WOFF2/Iosevka-Bold.woff2"
  @external_resource "priv/static/fonts/WOFF2/Iosevka-Regular.woff2"
  @font_bold File.read!("priv/static/fonts/WOFF2/Iosevka-Bold.woff2")
  @font_regular File.read!("priv/static/fonts/WOFF2/Iosevka-Regular.woff2")

  @line_height 36
  @code_start_y 210
  @code_start_x 80
  @font_size 24
  @char_width 12
  @space_width @char_width

  defp font_face_styles do
    """
    @font-face {
      font-family: 'Iosevka';
      src: url(data:font/woff2;charset=utf-8;base64,#{Base.encode64(@font_regular)}) format('woff2');
      font-weight: 400;
      font-style: normal;
    }

    @font-face {
      font-family: 'Iosevka';
      src: url(data:font/woff2;charset=utf-8;base64,#{Base.encode64(@font_bold)}) format('woff2');
      font-weight: 700;
      font-style: normal;
    }
    """
  end

  def generate_preview(gist) do
    code_lines =
      gist.code
      |> preview_code()
      |> highlight_code(gist.lang)
      |> render_code_lines()

    """
    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <svg width="1200" height="630" viewBox="0 0 1200 630" 
         xmlns="http://www.w3.org/2000/svg">
      <defs>
        <style type="text/css">
          #{font_face_styles()}

          * {
            font-family: 'Iosevka', monospace !important;
          }

          /* Catppuccin Macchiato Theme Colors */
          .background { fill: #24273a; }  /* base */
          .title { fill: #cad3f5; font-family: system-ui, -apple-system, sans-serif; }  /* text */
          .description { fill: #8087a2; font-family: system-ui, -apple-system, sans-serif; }  /* overlay1 */
          .code-background { fill: #1e2030; }  /* crust */
          
          /* Syntax Highlighting Colors - Catppuccin Macchiato */
          .c, .cm, .cp, .c1, .cs { fill: #8087a2; } /* Comment - overlay1 */
          .k, .kc, .kd, .kn, .kp, .kr, .kt { fill: #c6a0f6; } /* Keyword - mauve */
          .s, .sb, .sc, .sd, .s2, .se, .sh, .si, .sx, .sr, .s1, .ss { fill: #a6da95; } /* String - green */
          .n, .na, .nc, .no, .nd, .ni, .ne, .nf, .nl, .nn, .nx, .py, .nt, .nv, .vc, .vg, .vi { fill: #8aadf4; } /* Name - blue */
          .nb, .bp { fill: #f5a97f; } /* Built-in - peach */
          .m, .mf, .mh, .mi, .mo, .il { fill: #f5bde6; } /* Number - pink */
          .o, .ow { fill: #91d7e3; } /* Operator - sky */
          .p { fill: #cad3f5; } /* Punctuation - text */
          .w { fill: #cad3f5; } /* Whitespace - text */
          .l { fill: #8aadf4; } /* Literal - blue */
          .default { fill: #cad3f5; } /* Default text color - text */
        </style>

        <!-- Gradient definition -->
        <linearGradient id="fade" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stop-color="#1e2030" stop-opacity="0"/>
          <stop offset="50%" stop-color="#1e2030" stop-opacity="0"/>
          <stop offset="100%" stop-color="#181926" stop-opacity="1"/>
        </linearGradient>
      </defs>

      <!-- Background -->
      <rect class="background" width="1200" height="630" />
      
      <!-- Content -->
      <text x="60" y="80" class="title" font-size="40" font-weight="bold">
        #{escape_text(gist.title || "Gist: #{gist.id}")}
      </text>
      
      <text x="60" y="130" class="description" font-size="24">
        #{escape_text(gist.desc || "No description")}
      </text>
      
      <!-- Code Preview Background -->
      <rect x="60" y="160" width="1080" height="600" class="code-background" rx="8"/>

      <!-- Code Content -->
      <g font-family="Iosevka" font-size="#{@font_size}">
        #{code_lines}
      </g>

      <!-- Gradient Overlay -->
      <rect x="0" y="160" width="1200" height="600" fill="url(#fade)" rx="8"/>
    </svg>
    """
  end

  defp render_code_lines(highlighted_html) do
    highlighted_html
    |> String.split("\n")
    |> Enum.with_index()
    |> Enum.map_join("\n", fn {line, index} ->
      indent_level = calculate_indent_level(line)
      y_position = @code_start_y + index * @line_height
      render_line(String.trim_leading(line), y_position, indent_level)
    end)
  end

  defp calculate_indent_level(line) do
    leading_spaces =
      line
      |> String.replace(~r/^(\s+).*$/, "\\1")
      |> String.length()

    div(leading_spaces, 2)
  end

  defp render_line("", _y_position, _indent), do: ""

  defp render_line(line, y_position, indent_level) do
    spans = parse_spans(line)
    indent_offset = indent_level * 2 * @space_width

    spans
    |> Enum.reduce({[], indent_offset}, fn {content, class}, {spans, x_offset} ->
      unescaped_content = unescape_html(content)
      span = render_span(unescaped_content, class, x_offset, y_position)
      {[span | spans], x_offset + String.length(unescaped_content) * @char_width}
    end)
    |> elem(0)
    |> Enum.reverse()
    |> Enum.join("\n")
  end

  defp unescape_html(text) do
    text
    |> String.replace("&amp;", "&")
    |> String.replace("&lt;", "<")
    |> String.replace("&gt;", ">")
    |> String.replace("&quot;", "\"")
    |> String.replace("&#39;", "'")
    |> String.replace("&#x27;", "'")
    |> String.replace("&#x2F;", "/")
    |> String.replace("&#47;", "/")
    |> String.replace("&nbsp;", " ")
  end

  defp parse_spans(""), do: []

  defp parse_spans(line) do
    line
    |> String.split(~r{(<span.*?>|</span>)}, include_captures: true)
    |> Enum.reduce({[], nil}, fn
      <<"<span class=\"", rest::binary>>, {acc, nil} ->
        class = rest |> String.replace_suffix("\">", "")
        {acc, class}

      "</span>", {acc, _class} ->
        {acc, nil}

      content, {acc, class} when byte_size(content) > 0 ->
        {[{content, class || "default"} | acc], class}

      _, acc_class ->
        acc_class
    end)
    |> elem(0)
    |> Enum.reverse()
  end

  defp render_span(content, class, x_offset, y_position) when byte_size(content) > 0 do
    x = @code_start_x + x_offset
    # Re-escape the content for the SVG output
    safe_content = escape_text(content)
    "<text x=\"#{x}\" y=\"#{y_position}\" class=\"#{class}\">#{safe_content}</text>"
  end

  defp render_span(_, _, _, _), do: ""

  defp preview_code(content) do
    content
    |> String.split("\n")
    |> Enum.take(15)
    |> Enum.join("\n")
  end

  defp escape_text(text) do
    text
    |> String.replace("&", "&amp;")
    |> String.replace("<", "&lt;")
    |> String.replace(">", "&gt;")
  end

  defp highlight_code(code, language) when is_binary(language) do
    lexer = get_lexer_for_language(language)

    String.trim(
      code
      |> Makeup.highlight_inner_html(
        lexer: lexer,
        formatter_opts: [
          highlight_tag: "span",
          css_class: "makeup",
          highlight_css_class: "hll"
        ]
      )
      |> to_string()
    )
  end

  defp highlight_code(code, nil) do
    escape_text(code)
  end

  defp get_lexer_for_language(language) do
    case language do
      "elixir" -> Makeup.Lexers.ElixirLexer
      "erlang" -> Makeup.Lexers.ErlangLexer
      "html" -> Makeup.Lexers.HtmlLexer
      "javascript" -> Makeup.Lexers.JsLexer
      "python" -> Makeup.Lexers.PythonLexer
      "ruby" -> Makeup.Lexers.RubyLexer
      "rust" -> Makeup.Lexers.RustLexer
    end
  end

  def get_webp(gist) do
    svg = generate_preview(gist)
    tmp_svg_path = Path.join(System.tmp_dir(), "temp_#{:rand.uniform(999_999)}.svg")

    Logger.debug(tmp_svg_path)

    try do
      File.write!(tmp_svg_path, svg)

      Logger.debug("wrote svg")

      try do
        image =
          tmp_svg_path
          |> Mogrify.open()
          |> Mogrify.custom("define", "svg:include-fonts=true")
          |> Mogrify.format("webp")
          |> Mogrify.save()

        Logger.debug("wrote image #{image.path}")

        {:ok, image_binary} = File.read(image.path)

        Logger.debug("returning binary")

        {:ok, image_binary}
      rescue
        e in Mogrify.Error ->
          {
            :error,
            "Conversion failed: #{Exception.message(e)}"
          }
      end
    rescue
      e -> {:error, "File operation failed: #{Exception.message(e)}"}
    after
      File.rm(tmp_svg_path)
    end
  end
end
