Operand

op allah, geez.

gram: pain

> ./lib/pain/schedule.ex

Lenses
(coming soon!)


defmodule Pain.Schedule do
  @api "https://acuityscheduling.com/api/v1"
  import Acuity, only: [headers: 0]

  def ending, do: ":00-0400"
  def log(node), do: node |> IO.inspect

  def today, do: now() |> DateTime.to_date()
  def now do
    case DateTime.now("America/New_York") do
      {:ok, c } -> c
      {:error, _ } -> DateTime.utc_now()
    end
  end

  def day(phrase), do: Date.from_iso8601(phrase) |> elem(1)
  def range(day), do: Date.range(day, day)

  def this_month do
    Date.range(today(), today() |> Date.end_of_month)
  end

  def month(month) do
    [y,m] = month |> String.split("-") |> Enum.map(&String.to_integer/1)
    beginning = Date.new(y,m,1) |> elem(1) |> Date.beginning_of_month
    Date.range(beginning, beginning |> Date.end_of_month)
  end

  def service_demand(menu_keys) do
    menu_keys
    |> Enum.reduce(%{}, fn x, acc -> Map.update(acc, x, 1, &(&1 + 1)) end)
  end

  def calendars() do
    (@api <> "/calendars")
    |> Req.get!(headers: headers())
    |> Map.get(:body)
  end

  def employee_genders do
    :pain
    |> Application.app_dir("priv")
    |> Path.join("employees.yml")
    |> YamlElixir.read_from_file
    |> elem(1)
    |> Map.get("employees")
    |> Enum.map(fn %{ "name" => n, "gender" => g} -> { n, g} end)
    |> Map.new()
  end

  def employees do
    genders = employee_genders()
    Pain.Schedule.calendars() |> Enum.map(& %{
      name: &1["name"],
      biography: &1["description"] |> String.trim(),
      schedule_key: &1["id"],
      gender: genders[&1["name"]],
      image: "https:" <> &1["image"],
    })
  end

  def employee_keys, do: employees() |> Enum.map(&(&1[:schedule_key]))

  def menu do
    grouped_menu()["classes"] |> Enum.map(fn c ->
      c["services"] |> Enum.map(&(Map.put(&1, "class", c["name"])))
    end) |> List.flatten
  end

  def menu_keys(menu), do: menu |> Enum.map(& &1["schedule_key"])
  def menu_keys,       do: menu() |> menu_keys

  def key_in_menu(name), do: (menu() |> Enum.find(& &1["name"] == name))["schedule_key"]

  def grouped_menu do
    {:ok, s} = (
      :pain
      |> Application.app_dir("priv")
      |> Path.join("services.yml")
      |> YamlElixir.read_from_file
    ); s
  end

  @doc """
      import Pain.Schedule

      menu_keys() |> Enum.take(3)
      |> service_demand()
      |> check_blocks(employee_keys(), this_month())
  """
  def check_blocks demand, employee_keys, range do
    (range |> Enum.take(14) |> Parallel.map(fn day ->
      demand |> Parallel.map(fn { service, demand } ->
        check_calendar_day_service(service, employee_keys, day)
        |> reduce_calendars
        |> Enum.filter(fn { _, num } -> num >= demand end)
        |> Enum.map(fn { block, _ } -> block end)
      end)
      |> reduce_blocks
    end))
  end

  def check_calendar_day_service service, employee_keys, day do
    employee_keys
    |> Enum.map(fn employee ->
      search_hours = @api <> "/availability/times?date=#{day}&appointmentTypeID=#{service}&calendarID=#{employee}"
      case Req.get(search_hours, headers: headers()) do
        {:error, r} -> log r; []
        {:ok, r = %Req.Response{status: 400}} -> log r; []
        {:ok, r } -> r.body
      end
    end)
  end

  @doc """
      Pain.Schedule.check_blocks_on_calendars(
        "2023-08-28T14:00:00-0400",
        menu_keys() |> Enum.take(3),
        employee_keys()
      )
  """
  def check_blocks_on_calendars block, menu_keys, employee_keys do
    address = @api <> "/availability/check-times"
    menu = menu()

    menu_keys |> Enum.map(fn m ->
      body = (
        employee_keys
        |> Enum.map(fn employee -> %{
          datetime: block,
          appointmentTypeID: m,
          calendarID: employee
        } end)
      )

      case Req.post(address, json: body, headers: headers()) do
        {:error, r} -> log r; []
        {:ok, r = %Req.Response{status: 400}} -> log r; []
        {:ok, r } -> r.body
      end
    end) |> Squish.squish
  end

  def menu do
    Req.get!(@api <> "/appointment-types", headers: headers()).body
    |> Enum.map(& { &1["id"], &1["calendarIDs"] })
  end

  def addons, do: Req.get!(@api <> "/appointment-addons", headers: headers()).body

  def reduce_calendars cals do
    cals |> Enum.reduce(%{}, fn calendar, all ->
      calendar |> Enum.reduce(all, fn calBlock, cal ->
        Map.update(cal, calBlock["time"], 0, &(&1 + calBlock["slotsAvailable"]))
      end)
    end)
  end

  def reduce_blocks [original | remaining] do
    remaining |> Enum.reduce(MapSet.new(original), fn blocks, solid ->
      MapSet.intersection MapSet.new(blocks), solid
    end) |> MapSet.to_list
  end

  def blocks_by_day blocks do
    blocks
    |> Enum.filter(&(length(&1) > 0))
    |> Enum.reduce(%{}, fn day, all ->
      Map.put(all, (day |> hd |> String.split("T") |> hd), day)
    end)
  end

  @doc """
      import Pain.Schedule
      ( service_demand([ 39928578, 39928780, 39931669, ])
        |> check_blocks(employee_keys(), this_month())
        |> blocks_by_day()
      )["2023-09-30"] |> hour_map()
  """
  def hour_map(clocks) do
    clocks
    |> Enum.map(&( Regex.scan(~r/\d{2}:\d{2}/, &1) |> hd |> hd))
    |> Enum.sort()
    |> Enum.reduce(%{}, fn c, by_hour ->
      [h | [m | []]] = c |> String.split(":")
      Map.update(by_hour, h, [m], &(&1 ++ [m]))
    end)
  end
end