defmodule Mix.Tasks.Venomous.Structs do
  @moduledoc false
  use Mix.Task

  @default_imports """
  from typing import Any
  from dataclasses import dataclass
  from erlport.erlterms import Map, Atom
  from venomous import VenomousTrait
  """

  @impl Mix.Task
  def run(args) do
    Mix.Task.run("app.start")

    case args do
      [] ->
        Mix.raise("""
        Simple utility to create python elixir compatible classes.

        VenomousTrait class provides 2 functions: 
          - def from_dict(cls, erl_map: Map | Dict) -> cls
            # converts Erlport Map or a Dict into the object class
          - def into_erl(self) -> Map
            # returns erlang compatible struct from self

           
        To create basic python classes based on structs: 
            - mix venomous.structs MyModule.MyStruct MyModule.MoreStructs ...

        To create extended classes depending on existing python class: 
            - mix venomous.structs MyModule.MyStruct:PythonClassName ...

        To create for all available structs inside an application
            - mix venomous.structs all my_application
            
        """)

      ["all", module] ->
        all_from_module(module)

      modules ->
        both_way_modules(modules) |> IO.puts()
    end
  end

  defp sanitize_modulename(input) do
    input
    |> String.replace_prefix("Elixir.", "")
    |> String.replace(".", "")
    |> Kernel.<>("Struct")
  end

  defp generate_inherited_class(name, class_name) do
    """
    @dataclass
    class #{class_name}Struct(VenomousTrait, #{class_name}):
        __struct__: Atom = Atom(b"#{name}")

    """
  end

  defp generate_python_class({name, args}) do
    remove_prefix = sanitize_modulename(name)

    attributes =
      args
      |> Enum.map(fn attr_atom ->
        attr_atom |> to_string |> Kernel.<>(": Any = None")
      end)

    """
    @dataclass
    class #{remove_prefix}(VenomousTrait):
        #{Enum.join(attributes, "\n    ")}
        __struct__: Atom = Atom(b"#{name}")
    """
  end

  defp prefix_elixir_to_module(mod) do
    if String.starts_with?(mod, "Elixir.") do
      mod
    else
      "Elixir." <> mod
    end
  end

  defp mod_into_struct_keys(module) do
    module = Module.concat([module])
    Code.ensure_loaded(module)

    if module |> Kernel.function_exported?(:__struct__, 0) do
      struct(module) |> Map.pop(:__struct__) |> elem(1) |> Map.keys()
    else
      Mix.raise("#{module} isn't a struct")
    end
  end

  defp create_encoding(class) do
    """
        if isinstance(value, #{class}):
            return #{class}Struct.from_dict(value.__dict__).into_erl()
    """
  end

  defp create_class_and_keypair([module, class], {classes, keypairs, encoder}) do
    {
      classes <> generate_inherited_class(module, class) <> "\n",
      keypairs <> "Atom(b\"#{module}\"): #{class}Struct,\n    ",
      encoder <> create_encoding(class)
    }
  end

  defp create_class_and_keypair([module], {classes, keypairs, encoder}) do
    {
      classes <> generate_python_class({to_string(module), mod_into_struct_keys(module)}) <> "\n",
      keypairs <> "Atom(b\"#{module}\"): #{sanitize_modulename(module)},\n    ",
      encoder <>
        create_encoding(sanitize_modulename(module) |> String.replace_suffix("Struct", ""))
    }
  end

  defp both_way_modules(modules) do
    encoder = """
        if isinstance(value, dict):
            return {encoder(key): encoder(value) for key, value in value.items()}
        if isinstance(value, (list, tuple, set)):
            return type(value)(encoder(item) for item in value)

    """

    {classes, keypairs, encoders} =
      modules
      |> Enum.reduce({"", "", encoder}, fn module, classes_and_keypair_and_encoder ->
        module = prefix_elixir_to_module(module)

        String.split(module, ":") |> create_class_and_keypair(classes_and_keypair_and_encoder)
      end)

    """
    #{@default_imports}
    #{classes}venomous_structs = {
        #{keypairs}}

    def encoder(value: Any) -> Any:
    #{encoders}
        return value

    def decoder(value: Any) -> Any:
        if isinstance(value, (Map, dict)):
            if struct := value.get(Atom(b"__struct__")):
                return venomous_structs[struct].from_dict(value, venomous_structs)
            return {decoder(key): decoder(val) for key, val in value.items()}
        elif isinstance(value, (set, list, tuple)):
            return type(value)(decoder(_val) for _val in value)

        return value
    """
  end

  defp all_from_module(module) do
    modules =
      case String.to_existing_atom(module) |> :application.get_all_key() do
        {:ok, mod} ->
          Keyword.get(mod, :modules)

        :undefined ->
          Mix.raise("Couldn't find the module. Make sure you aren't providing a sub-module.")
      end

    Enum.each(modules, &Code.ensure_loaded(&1))
    structs = Enum.filter(modules, &Kernel.function_exported?(&1, :__struct__, 0))

    struct_dic =
      Enum.reduce(structs, "\nvenomous_structs = {\n    ", fn struct_mod, acc ->
        name = to_string(struct_mod)
        acc <> "Atom(b\"#{name}\"): #{sanitize_modulename(name)},\n    "
      end)
      |> Kernel.<>("}")

    struct_classes =
      Enum.map(structs, fn struct_mod ->
        {to_string(struct_mod),
         struct_mod |> struct() |> Map.pop(:__struct__) |> elem(1) |> Map.keys()}
      end)
      |> Enum.map(&generate_python_class/1)

    """
    #{@default_imports}
    #{Enum.join(struct_classes, "\n")}#{struct_dic}

    def encoder(value: Any) -> Any:
        if isinstance(value, VenomousTrait):
           return value.into_erl()
        return value

    def decoder(value: Any) -> Any:
        if isinstance(value, Map):
            if struct := value.get(Atom(b"__struct__")):
                return venomous_structs[struct].from_dict(value)
        return value

    """
    |> IO.puts()
  end
end
