defmodule Raxol.Style.Colors.Persistence do
  @moduledoc """
  Handles persistence of color themes and user preferences.

  This module provides functionality for:
  - Saving and loading themes
  - Managing user preferences
  - Handling theme file storage
  """

  alias Raxol.Style.Colors.Color

  @themes_dir "themes"
  @preferences_file "preferences.json"

  # Helper to get the configured base directory
  defp config_dir do
    # Default to current dir
    Application.get_env(:raxol, :config_dir, ".")
  end

  @doc """
  Saves a theme to a file.

  ## Parameters

  - `theme` - The theme to save

  ## Returns

  - `:ok` on success
  - `{:error, reason}` on failure
  """
  def save_theme(theme) do
    # Pre-process theme to convert tuple values to lists for JSON encoding
    theme = deep_convert_tuples_to_lists(theme)
    # Construct full path using config_dir
    full_themes_dir = Path.join(config_dir(), @themes_dir)
    # Ensure themes directory exists
    File.mkdir_p!(full_themes_dir)

    # Convert theme to JSON
    theme_json = Jason.encode!(theme, pretty: true)

    # Robustly get the theme name (prefer id over name for filename)
    theme_name = extract_theme_name(theme)

    # Save theme to file
    theme_path = Path.join(full_themes_dir, "#{theme_name}.json")
    File.write(theme_path, theme_json)
  end

  # Recursively convert tuple values to lists and all map keys to strings in maps and structs
  defp deep_convert_tuples_to_lists(%_struct{} = struct) do
    struct
    |> Map.from_struct()
    |> Enum.map(fn {k, v} ->
      {to_string_key(k), deep_convert_tuples_to_lists(v)}
    end)
    |> Enum.into(%{})
  end

  defp deep_convert_tuples_to_lists(map) when is_map(map) do
    map
    |> Enum.map(fn {k, v} ->
      {to_string_key(k), deep_convert_tuples_to_lists(v)}
    end)
    |> Enum.into(%{})
  end

  defp deep_convert_tuples_to_lists(list) when is_list(list) do
    Enum.map(list, &deep_convert_tuples_to_lists/1)
  end

  defp deep_convert_tuples_to_lists(tuple) when is_tuple(tuple) do
    Tuple.to_list(tuple)
  end

  defp deep_convert_tuples_to_lists(val), do: val

  defp to_string_key(k) when is_tuple(k),
    do: Enum.map_join(Tuple.to_list(k), ":", &to_string/1)

  defp to_string_key(k) when is_atom(k), do: Atom.to_string(k)
  defp to_string_key(k), do: to_string(k)

  @doc """
  Loads a theme from a file.

  ## Parameters

  - `theme_name` - The name of the theme to load

  ## Returns

  - `{:ok, theme}` on success
  - `{:error, reason}` on failure
  """
  def load_theme(theme_name) do
    theme_path =
      Path.join(Path.join(config_dir(), @themes_dir), "#{theme_name}.json")

    with {:ok, theme_json} <- File.read(theme_path),
         {:ok, theme_map} <- Jason.decode(theme_json) do
      theme_struct = map_to_theme_struct(theme_map)

      {:ok, theme_struct}
    else
      {:error, :enoent} -> {:error, :enoent}
      {:error, reason} -> {:error, reason}
    end
  end

  @doc """
  Loads the current theme from user preferences.

  ## Returns

  - `{:ok, theme}` on success
  - `{:error, reason}` on failure
  """
  def load_current_theme do
    case load_user_preferences() do
      {:ok, preferences} ->
        theme_name = Map.get(preferences, "theme", "Default")

        case load_theme(theme_name) do
          {:ok, theme} -> {:ok, theme}
          {:error, _reason} -> {:ok, Raxol.UI.Theming.Theme.default_theme()}
        end

      {:error, _reason} ->
        {:ok, Raxol.UI.Theming.Theme.default_theme()}
    end
  end

  @doc """
  Loads user preferences from file.

  ## Returns

  - `{:ok, preferences}` on success
  - `{:error, reason}` on failure
  """
  def load_user_preferences do
    prefs_path = Path.join(config_dir(), @preferences_file)

    case File.read(prefs_path) do
      {:ok, json} ->
        # Decode with default string keys
        case Jason.decode(json) do
          {:ok, preferences} -> {:ok, preferences}
          # Propagate decoding errors
          error -> error
        end

      {:error, :enoent} ->
        # File doesn't exist, return default preferences (use string key)
        {:ok, %{"theme" => "Default"}}

      error ->
        # Propagate other file read errors
        error
    end
  end

  @doc """
  Saves user preferences to file.

  ## Parameters

  - `preferences` - The preferences to save

  ## Returns

  - `:ok` on success
  - `{:error, reason}` on failure
  """
  def save_user_preferences(preferences) do
    prefs_path = Path.join(config_dir(), @preferences_file)

    case Jason.encode(preferences, pretty: true) do
      {:ok, json} -> File.write(prefs_path, json)
      error -> error
    end
  end

  @doc """
  Lists all available themes.

  ## Returns

  - A list of theme names
  """
  def list_themes do
    full_themes_dir = Path.join(config_dir(), @themes_dir)
    # Ensure themes directory exists
    File.mkdir_p!(full_themes_dir)

    # List all theme files
    full_themes_dir
    |> File.ls!()
    |> Enum.filter(&String.ends_with?(&1, ".json"))
    |> Enum.map(&String.replace(&1, ".json", ""))
  end

  @doc """
  Deletes a theme.

  ## Parameters

  - `theme_name` - The name of the theme to delete

  ## Returns

  - `:ok` on success
  - `{:error, reason}` on failure
  """
  def delete_theme(theme_name) do
    theme_path =
      Path.join(Path.join(config_dir(), @themes_dir), "#{theme_name}.json")

    File.rm(theme_path)
  end

  # --- Conversion helpers ---
  # Convert a loaded map (with string keys and list keys for variants) back to a Theme struct
  def map_to_theme_struct(%_struct{} = struct) do
    struct
    |> Map.from_struct()
    |> map_to_theme_struct()
  end

  def map_to_theme_struct(map) when is_map(map) do
    # Deeply atomize all keys before normalization
    atomized_map = deep_atomize_keys(map)

    attrs =
      atomized_map
      |> Enum.map(fn {k, v} -> {to_atom_key(k), v} end)
      |> Enum.into(%{})
      |> Map.update(:colors, %{}, &normalize_colors/1)
      |> Map.put_new(:variants, %{})
      |> Map.update(:variants, %{}, &normalize_variants/1)
      |> Map.update(:ui_mappings, %{}, &normalize_ui_mappings/1)

    Raxol.UI.Theming.Theme.new(attrs)
  end

  def map_to_theme_struct(other), do: other

  defp to_atom_key(k) when is_binary(k), do: String.to_atom(k)
  defp to_atom_key(k), do: k

  # Helper to extract theme name with preference for id over name
  defp extract_theme_name(theme) when is_map(theme) and is_map_key(theme, :id),
    do: theme[:id]

  defp extract_theme_name(theme) when is_map(theme) and is_map_key(theme, "id"),
    do: theme["id"]

  defp extract_theme_name(theme)
       when is_map(theme) and is_map_key(theme, :name),
       do: theme[:name]

  defp extract_theme_name(theme)
       when is_map(theme) and is_map_key(theme, "name"),
       do: theme["name"]

  defp extract_theme_name(_theme), do: raise("Theme missing id or name key")

  # Helper to normalize color values in both colors and variants
  defp normalize_color_value(v) when is_map(v) and is_map_key(v, :hex) do
    Color.from_hex(v["hex"] || v.hex)
  end

  defp normalize_color_value(v) when is_map(v) and is_map_key(v, "hex") do
    Color.from_hex(v["hex"])
  end

  defp normalize_color_value(v) when is_binary(v) do
    case String.starts_with?(v, "#") do
      true -> Color.from_hex(v)
      false -> v
    end
  end

  defp normalize_color_value(v) when is_struct(v, Color), do: v
  defp normalize_color_value(v), do: v

  # Convert color keys to atoms and values to hex or Color structs
  defp normalize_colors(colors) when is_map(colors) do
    result =
      Enum.into(colors, %{}, fn {k, v} ->
        key = if is_binary(k), do: String.to_atom(k), else: k
        value = normalize_color_value(v)
        {key, value}
      end)

    result
  end

  defp normalize_colors(other), do: other

  # Convert variant keys like "primary:high_contrast" or ["primary", "high_contrast"] to tuples
  defp normalize_variants(variants) when is_map(variants) do
    result =
      Enum.into(variants, %{}, fn {k, v} ->
        {normalize_variant_key(k), normalize_color_value(v)}
      end)

    result
  end

  defp normalize_variants(other), do: other

  defp normalize_variant_key(k) when is_binary(k) do
    case String.contains?(k, ":") do
      true ->
        k |> String.split(":") |> Enum.map(&String.to_atom/1) |> List.to_tuple()

      false ->
        String.to_atom(k)
    end
  end

  defp normalize_variant_key(k) when is_list(k),
    do: Enum.map(k, &to_atom_key/1) |> List.to_tuple()

  defp normalize_variant_key(k) when is_atom(k), do: k
  defp normalize_variant_key(k), do: k

  # Convert ui_mappings keys and values to atoms
  defp normalize_ui_mappings(mappings) when is_map(mappings) do
    Enum.into(mappings, %{}, fn {k, v} ->
      {to_atom_key(k), if(is_binary(v), do: String.to_atom(v), else: v)}
    end)
  end

  defp normalize_ui_mappings(other), do: other

  # Recursively convert all string keys in a map to atoms
  defp deep_atomize_keys(%{} = map) do
    map
    |> Enum.map(fn {k, v} ->
      {if(is_binary(k), do: String.to_atom(k), else: k), deep_atomize_keys(v)}
    end)
    |> Enum.into(%{})
  end

  defp deep_atomize_keys([head | tail]),
    do: [deep_atomize_keys(head) | deep_atomize_keys(tail)]

  defp deep_atomize_keys([]), do: []
  defp deep_atomize_keys(other), do: other
end
