defmodule Tds.Error do
  @moduledoc """
  Defines the `Tds.Error` struct.

  The struct has two fields:

  * `:message`: expected to be a string
  * `:mssql`: expected to be a keyword list with the fields `line_number`,
              `number` and `msg_text`

  ## Usage

      iex> raise Tds.Error
      ** (Tds.Error) An error occurred.

      iex> raise Tds.Error, "some error"
      ** (Tds.Error) some error

      iex> raise Tds.Error, line_number: 10, number: 8, msg_text: "some error"
      ** (Tds.Error) Line 10 (8): some error

  """

  @type error_details :: %{line_number: integer(), number: integer(), msg_text: String.t()}
  @type t :: %__MODULE__{message: String.t(), mssql: error_details}

  defexception [:message, :mssql]

  def exception(message) when is_binary(message) or is_atom(message) do
    %__MODULE__{message: message}
  end

  def exception(line_number: line_number, number: number, msg_text: msg) do
    %__MODULE__{
      mssql: %{
        line_number: line_number,
        number: number,
        msg_text: msg
      }
    }
  end

  def exception(_) do
    %__MODULE__{message: "An error occured."}
  end

  @spec message(%__MODULE__{}) :: String.t()
  def message(%__MODULE__{mssql: mssql}) when is_map(mssql) do
    "Line #{mssql[:line_number]} (Error #{mssql[:number]}): #{mssql[:msg_text]}"
  end

  def message(%__MODULE__{message: message}) when is_binary(message) do
    message
  end

  @external_resource errcodes_path = Path.join(__DIR__, "errors.csv")

  errcodes =
    for line <- File.stream!(errcodes_path) do
      [type, code, regex] = String.split(line, ",", trim: true)
      type = String.to_atom(type)
      code = code |> String.trim()
      regex = String.replace_trailing(regex, "\n", "")

      if code == nil do
        raise CompileError, "Error code must be an integer value"
      end

      {code, {type, regex}}
    end

  Enum.group_by(errcodes, &elem(&1, 0), &elem(&1, 1))
  |> Enum.map(fn {code, type_regexes} ->
    {error_code, ""} = Integer.parse(code)

    def get_constraint_violations(unquote(error_code), message) do
      constraint_checks =
        Enum.map(unquote(type_regexes), fn {key, val} ->
          {key, Regex.compile!(val)}
        end)

      extract = fn {key, test}, acc ->
        concatenate_match = fn [match], acc -> [{key, match} | acc] end

        case Regex.scan(test, message, capture: :all_but_first) do
          [] -> acc
          matches -> Enum.reduce(matches, acc, concatenate_match)
        end
      end

      Enum.reduce(constraint_checks, [], extract)
    end
  end)

  def get_constraint_violations(_, _) do
    []
  end
end

defmodule Tds.ConfigError do
  defexception message: "Tds configuration error."

  def exception(message) when is_binary(message) or is_atom(message) do
    %__MODULE__{message: message}
  end
end
