defmodule PainWeb.BookLive do
  use Surface.LiveView
  import PainWeb.Gettext

  import Pain.Schedule, only: [
    employees: 0,
    employee_keys: 0,
    menu: 0,
    menu_keys: 1,
    key_in_menu: 1,
    grouped_menu: 0,
    check_blocks_on_calendars: 3,
  ]

  alias PainWeb.Components.Accion
  alias PainWeb.Components.BodyMap
  alias PainWeb.Components.Page
  alias PainWeb.Components.Choices
  alias PainWeb.Components.Class
  alias PainWeb.Components.Conditions
  alias PainWeb.Components.Employee
  alias PainWeb.Components.Schedule
  alias PainWeb.Components.ServiceMap
  alias Surface.Components.Form

  import PainWeb.CoreComponents, only: [modal: 1, show_modal: 1, hide_modal: 1]

  data number, :integer, default: 1
  data open_class, :string, default: ""
  data schedule, :string, default: nil
  data display_bios, :boolean, default: true

  data services, :map, default: %{}
  data employed, :map, default: %{}
  data calendars, :map, default: %{}
  data limbs, :map, default: %{}
  data addons, :map, default: %{}
  data all_addons, :list, default: []

  data customer, :form, default:  %{
    "name" => "",
    "phone" => "",
    "email" => "",
    "reference" => "",
    "conditions" => false,
  }
  data booked, :list, default: nil

  def mount _, _, socket do
    { :ok, socket |> assign(:all_addons, Pain.Schedule.addons) }
  end

  # def handle_event("bypass", _, socket) do
  #   {:noreply, socket |> assign(%{
  #     number: 4,
  #     services: %{
  #       1 => "90Min Reflexology with Chinese Medicine",
  #       2 => "90min Massage",
  #       3 => "Cupping",
  #       4 => "Wet Cupping"
  #     },
  #     schedule: "2023-09-30T19:00",
  #     employed: %{1 => "_fem", 2 => "_masc", 3 => "Andy Ji", 4 => "Bin Wang"}
  #   })}
  # end

  def handle_event("number", params, socket),
    do: {:noreply, assign(socket, :number, String.to_integer params["num"])}
  def handle_event("open_class", params, socket),
    do: {:noreply, assign(socket, :open_class, params["name"])}

  def handle_event("choose_service", params, socket) do
    num = String.to_integer(params["num"])
    service = menu() |> Enum.find(& &1["name"] == params["name"])

    {:noreply, socket
    |> update(:services, &(Map.put(&1, num, params["name"])))
    |> update(:limbs, fn limbs ->
      case service["class"] do
        "Body Massage" ->
          Map.update(limbs, num, ["_choose"], & &1 ++ ["_choose"] |> Enum.uniq)
        _ -> limbs
      end
    end)
    }
  end

  def handle_event("choose_limb", params, socket) do
    num = String.to_integer(params["num"])
    {:noreply, socket
    |> update(:limbs, fn limbs ->
      Map.update(limbs, num, [params["limb"]], &
        case Enum.member?(&1, params["limb"]) do
          false -> &1 ++ [params["limb"]] |> Enum.uniq
          true -> &1 -- [params["limb"]]
        end
      )
    end)
    }
  end

  def handle_event("begin_choosing_limbs", params, socket) do
    num = String.to_integer(params["num"])
    {:noreply, socket
    |> update(:limbs, fn limbs ->
      Map.update(limbs, num, ["_choose"], & &1 ++ ["_choose"] |> Enum.uniq)
    end)
    }
  end

  def handle_event("clear_limbs", params, socket) do
    num = String.to_integer(params["num"])
    {:noreply, socket
    |> update(:limbs, fn limbs -> Map.put(limbs, num, ["_choose"]) end)
    }
  end

  def handle_event("done_choosing_limbs", params, socket) do
    num = String.to_integer(params["num"])
    {:noreply, socket
    |> update(:limbs, fn limbs -> Map.update(limbs, num, [], & &1 -- ["_choose"]) end)
    }
  end

  def handle_event("clear_services", _, socket) do
    {:noreply, socket
    |> assign(:services, %{})
    |> assign(:limbs, %{})
    |> assign(:schedule, nil)
    |> assign(:employed, %{})
    }
  end

  def handle_event("employ", params, socket) do
    {:noreply, update(socket, :employed, fn employed ->
      Map.update(employed, String.to_integer(params["num"]), params["name"],
        &(if &1 == params["name"], do: nil, else: params["name"]))
    end)}
  end

  def handle_event("clear_employees", _, socket) do
    {:noreply, socket |> assign(:employed, %{}) }
  end

  def handle_event("clear_schedule", _, socket) do
    {:noreply, socket
    |> assign(:schedule, nil)
    |> assign(:employed, %{})
    }
  end

  def handle_event("schedule", params, socket) do
    {:noreply, socket
    |> assign(:schedule, params["shape"])
    |> assign(:calendars, params["shape"] |> schedule_calendars(socket.assigns))
    }
  end

  def handle_event("render_bios", params, socket), do:
    {:noreply, socket |> assign(:display_bios, params["shape"]) }

  def handle_event("customer", params, socket) do
    {:noreply, socket |> assign(:customer, params
    |> Map.take(~w[name phone email reference conditions])) }
  end

  def handle_event("addon", params, socket) do
    num = String.to_integer(params["num"])
    addon = String.to_integer(params["addon"])
    {:noreply, socket |> update(:addons, fn addons ->
      if Enum.member?(addons[num] || [], addon),
        do: Map.update(addons, num, [], & &1 -- [addon]),
        else: Map.update(addons, num, [addon], & &1 ++ [addon])
    end) }
  end

  def handle_event("book", _, socket) do
    links = (
      socket.assigns
      |> Map.take(~w[ employed schedule customer limbs ]a)
      |> Pain.Order.book(
        chosen_services(socket.assigns[:services]),
        socket.assigns[:addons])
    ) |> IO.inspect()
    {:noreply, socket |> assign(:booked, links) }
  end

  def handle_info {process, response}, socket do
    Process.demonitor(process, [:flush])
    send_update Schedule, id: "schedule", possible: response, process: nil
    {:noreply, socket}
  end

  def chosen_services(services) do
    all = grouped_menu()["classes"] |> Enum.map(fn class ->
      class["services"]
      |> Enum.map(&(if &1["hanyu"], do: &1, else: Map.put(&1, "hanyu", class["hanyu"])))
      |> Enum.map(&(Map.put(&1, "class", class["name"])))
    end) |> List.flatten

    services
    |> Enum.reduce(%{}, fn { n, name }, chosen ->
      Map.put(chosen, n, all |> Enum.find(&(&1["name"] == name)))
    end)
  end

  def scheduled_block(schedule) do
    (schedule <> ":00Z-04:00")
    |> String.slice(0, 20)
    |> NaiveDateTime.from_iso8601()
    |> elem(1)
  end

  def schedule_calendars(schedule, assigns) do
    (schedule <> Pain.Schedule.ending)
    |> check_blocks_on_calendars(
      assigns[:services] |> Map.values() |> Enum.map(&Pain.Schedule.key_in_menu/1),
      employee_keys()
    )
  end

  def employee_bookable?(calendars, employee, services, employed) do
    ss = menu()
    case (employed |> Enum.find(fn {_, name} -> name == employee[:name] end)) do
      # remember: you can only book an employee once per block.
      {n, _employee} ->
        Map.put %{ 1 => false, 2 => false, 3 => false, 4 => false }, n, true
      # usually, check each employee's schedule.
      _ ->
        services
        |> Enum.map(fn {n, s} -> { n,
          calendars |> employee_can_do?(employee, key_in_menu(s))
        } end)
        |> Map.new()
    end
  end

  @doc """
  PainWeb.BookLive.bookable_by_gender(cals, %{
  1 => "90Min Reflexology with Chinese Medicine",
  2 => "90min Massage",
  3 => "Cupping",
  4 => "Wet Cupping" },
  %{ 1 => "Bin Wang"})
  """
  def bookable_by_gender(calendars, services, employed) do
    calendars
    |> bookable_employees_by_service(services)
    |> Enum.reduce(%{}, fn {n, employees}, remaining ->
      Map.put(remaining, n, employees
      |> Enum.filter(&(!Enum.member?(Map.values(employed), &1["name"]))))
    end)
    |> Enum.reduce(%{}, fn {n, employees}, by_service ->
      Map.put(by_service, n, employees |> Enum.reduce(%{}, fn employee, by_gender ->
        Map.update(by_gender, employee[:gender], 1, &(&1+1))
      end))
    end)
  end

  def bookable_any(calendars, services, employed, decide) do
    calendars
    |> bookable_by_gender(services, employed)
    |> Enum.reduce(%{}, fn {n, g}, by_service ->
      Map.put(by_service, n, decide.(Map.values(g) |> Enum.reduce(0, &(&1 + &2)))) end)
  end

  def bookable_as_gender(calendars, services, employed, gender, decide) do
    calendars
    |> bookable_by_gender(services, employed)
    |> Enum.reduce(%{}, fn {n, g}, by_service ->
      Map.put(by_service, n, decide.(g[gender] || 0)) end)
  end

  @doc """
  PainWeb.BookLive.bookable_employees_by_service(cals, %{
  1 => "90Min Reflexology with Chinese Medicine",
  2 => "90min Massage",
  3 => "Cupping",
  4 => "Wet Cupping" })
  """
  def bookable_employees_by_service calendars, services do
    es = employees()
    services
    |> Enum.map(fn {n, s} -> {n, es
      |> Enum.filter(& calendars |> employee_can_do?(&1, key_in_menu(s)))
      |> Enum.map(&(Map.take(&1, ~w[name gender]a)))
    } end)
    |> Map.new
  end

  def employee_can_do?(calendars, employee, service_key) do
    (calendars
    |> Enum.filter(&(&1["calendarID"] == employee[:schedule_key]))
    |> Enum.filter(&(&1["appointmentTypeID"] == service_key))
    |> Enum.filter(&(&1["valid"]))
    |> length()
    ) > 0
  end

  def needing_choice(limbs) do
    limbs
    |> Enum.filter(fn {_n, limbs} -> Enum.member?(limbs, "_choose") end)
    |> Map.new
  end

  def body_areas(chosen) do
    (chosen || [])
    |> Enum.filter(& !Enum.member?([nil, "_choose"], &1))
  end

  def sum_services chosen_services do
    chosen_services
    |> Enum.reduce(0, fn {_, service}, sum ->
      sum + (service["duracion"]
      |> String.split("$")
      |> Enum.at(1)
      |> String.to_float)
    end)
  end

  def sum_addons addons, all_addons do
    addons
    |> Map.values()
    |> List.flatten()
    |> Enum.map(fn key -> all_addons |> Enum.find(& &1["id"] == key) end)
    |> Enum.map(& &1["price"] |> String.to_float)
    |> Enum.sum()
  end

  def explain_services assigns do
    ~F"""
    <style>
      ul { margin-bottom: 1rem; padding-left: 1rem; list-style: disc; }
      li.service { margin-bottom: 1rem; }
      a { text-decoration: underline; }
    </style>
    <ul class="services">
    {#for {n, service} <- chosen_services(@services)}
      <li class="service">
        {service["name"]}
        {#if service["hanyu"]} / {service["hanyu"]}{/if}
        <br/>{service["duracion"]}<br/>
        {#if (@booked || []) |> Enum.at(n-1) == "/error"}
          <h3>An error occurred booking this appointment.</h3>
        {#elseif (@booked || []) |> Enum.at(n-1)}
          <a target="_blank" href={@booked |> Enum.at(n-1)}>
            See or cancel this booking.
          </a><br/>
        {/if}

        {#if length(body_areas @limbs[n]) == 0}
          on no specific location on body.
          {#if !@booked}
            (<a href="#" :on-click="begin_choosing_limbs" phx-value-num={n}>
            change</a>){/if}
        {#else}
        on:
        {#if !@booked}
          (<a href="#" :on-click="begin_choosing_limbs" phx-value-num={n}>
          change</a>){/if}
        <ul>{#for area <- body_areas(@limbs[n])}
          <li>{area}</li>
          {/for}</ul>
        {/if}

        <br/>Add-ons:<br/>
        {#if !@booked}
          {#for addon <- @all_addons}
            <button :on-click="addon" phx-value-num={n} phx-value-addon={addon["id"]}
              class={"btn", "join-item", "btn-sm",
              "btn-active": Enum.member?(@addons[n] || [], addon["id"]) } >
              <span>${addon["price"]}</span> /
              <span>{addon["name"]}</span>
            </button>
          {/for}
        {#else}
          <ul>{#for addon <- ((@addons[n] || [])
            |> Enum.map(fn key -> @all_addons |> Enum.find(& &1["id"] == key) end)) }
            <li><span>${addon["price"]}</span> / <span>{addon["name"]}</span></li>
          {#else}(none){/for}
          </ul>
        {/if}
      </li>
    {/for}
    </ul>

    {#if (@services |> chosen_services |> map_size) > 0}
      Once your appointments have ended, you'll be charged a sum of: <br/>
      ${sum_services(chosen_services(@services)) + sum_addons(@addons, @all_addons)}
      (plus taxes)
    {/if}
    """
  end
end
