defmodule SimpleshopThemeWeb.ImageController do
  use SimpleshopThemeWeb, :controller

  alias SimpleshopTheme.Media
  alias SimpleshopTheme.Media.SVGRecolorer

  @doc """
  Serves an image from the database by ID.

  Images are served with aggressive caching headers since they are
  immutable once uploaded.
  """
  def show(conn, %{"id" => id}) do
    case Media.get_image(id) do
      nil ->
        send_resp(conn, 404, "Image not found")

      image ->
        conn
        |> put_resp_content_type(image.content_type)
        |> put_resp_header("cache-control", "public, max-age=31536000, immutable")
        |> put_resp_header("etag", ~s("#{image.id}"))
        |> send_resp(200, image.data)
    end
  end

  @doc """
  Serves a thumbnail of an image from the disk cache.

  Thumbnails are generated by the background image optimization pipeline.
  If the thumbnail doesn't exist on disk yet (still processing), generates
  it on-demand and saves it for future requests.
  """
  def thumbnail(conn, %{"id" => id}) do
    thumb_path = Media.get_thumbnail_path(id)

    if File.exists?(thumb_path) do
      # Serve from disk cache
      conn
      |> put_resp_content_type("image/jpeg")
      |> put_resp_header("cache-control", "public, max-age=31536000, immutable")
      |> put_resp_header("etag", ~s("#{id}-thumb"))
      |> send_file(200, thumb_path)
    else
      # Thumbnail not yet generated - generate on-demand
      case Media.get_image(id) do
        nil ->
          send_resp(conn, 404, "Image not found")

        %{data: data} when is_binary(data) ->
          case generate_thumbnail_on_demand(data, thumb_path) do
            {:ok, binary} ->
              conn
              |> put_resp_content_type("image/jpeg")
              |> put_resp_header("cache-control", "public, max-age=31536000, immutable")
              |> put_resp_header("etag", ~s("#{id}-thumb"))
              |> send_resp(200, binary)

            {:error, _} ->
              # Fallback to full image if thumbnail generation fails
              conn
              |> put_resp_content_type("image/webp")
              |> put_resp_header("cache-control", "public, max-age=31536000, immutable")
              |> send_resp(200, data)
          end

        %{data: nil} ->
          send_resp(conn, 404, "Image data not available")
      end
    end
  end

  defp generate_thumbnail_on_demand(image_data, thumb_path) do
    with {:ok, image} <- Image.from_binary(image_data),
         {:ok, thumb} <- Image.thumbnail(image, 200),
         {:ok, binary} <- Image.write(thumb, :memory, suffix: ".jpg", quality: 80) do
      # Ensure cache directory exists and save for future requests
      File.mkdir_p!(Path.dirname(thumb_path))
      File.write!(thumb_path, binary)
      {:ok, binary}
    end
  rescue
    _ -> {:error, :thumbnail_generation_failed}
  end

  @supported_formats %{"avif" => :avif, "webp" => :webp, "jpg" => :jpg}

  @doc """
  Serves an image variant at the specified width and format.

  Supports AVIF, WebP, and JPEG formats. If the variant doesn't exist on disk
  (e.g., cache was deleted), it will be generated on-demand and cached.

  JPEG variants are never pre-generated (to save disk space), so they are
  always generated on first request.

  ## URL format

      /images/:id/variant/:width.:format

  Examples:
  - `/images/abc123/variant/800.avif`
  - `/images/abc123/variant/400.webp`
  - `/images/abc123/variant/1200.jpg`
  """
  def variant(conn, %{"id" => id, "width" => width_with_ext}) do
    alias SimpleshopTheme.Images.Optimizer

    with {width, format} <- parse_width_and_format(width_with_ext),
         true <- width in Optimizer.all_widths(),
         %{data: data} when is_binary(data) <- Media.get_image(id) do
      case Optimizer.generate_variant_on_demand(data, id, width, format) do
        {:ok, path} ->
          conn
          |> put_resp_content_type(format_content_type(format))
          |> put_resp_header("cache-control", "public, max-age=31536000, immutable")
          |> put_resp_header("etag", ~s("#{id}-#{width}.#{format}"))
          |> send_file(200, path)

        {:error, _reason} ->
          send_resp(conn, 500, "Failed to generate variant")
      end
    else
      :error ->
        send_resp(conn, 400, "Invalid width or format")

      false ->
        send_resp(conn, 400, "Width not supported")

      nil ->
        send_resp(conn, 404, "Image not found")

      %{data: nil} ->
        send_resp(conn, 404, "Image data not available")
    end
  end

  defp parse_width_and_format(width_with_ext) do
    case String.split(width_with_ext, ".") do
      [width_str, ext] when is_map_key(@supported_formats, ext) ->
        case Integer.parse(width_str) do
          {width, ""} -> {width, @supported_formats[ext]}
          _ -> :error
        end

      _ ->
        :error
    end
  end

  defp format_content_type(:avif), do: "image/avif"
  defp format_content_type(:webp), do: "image/webp"
  defp format_content_type(:jpg), do: "image/jpeg"

  @doc """
  Serves an SVG image recolored with the specified color.

  The color should be a hex color code (with or without the leading #).
  Only works with SVG images.
  """
  def recolored_svg(conn, %{"id" => id, "color" => color}) do
    clean_color = normalize_color(color)

    with true <- SVGRecolorer.valid_hex_color?(clean_color),
         %{is_svg: true, svg_content: svg} when not is_nil(svg) <- Media.get_image(id) do
      recolored = SVGRecolorer.recolor(svg, clean_color)

      conn
      |> put_resp_content_type("image/svg+xml")
      |> put_resp_header("cache-control", "public, max-age=3600")
      |> put_resp_header("etag", ~s("#{id}-#{clean_color}"))
      |> send_resp(200, recolored)
    else
      false ->
        send_resp(conn, 400, "Invalid color format")

      nil ->
        send_resp(conn, 404, "Image not found")

      %{is_svg: false} ->
        send_resp(conn, 400, "Image is not an SVG")

      %{svg_content: nil} ->
        send_resp(conn, 400, "SVG content not available")
    end
  end

  defp normalize_color(color) do
    color = String.trim(color)

    if String.starts_with?(color, "#") do
      color
    else
      "#" <> color
    end
  end
end
