defmodule MvWeb.StatisticsLive do
  @moduledoc """
  LiveView for the statistics page at /statistics.

  Displays aggregated member and membership fee cycle statistics.
  """
  use MvWeb, :live_view

  require Logger

  import MvWeb.LiveHelpers, only: [current_actor: 1]
  alias Mv.Statistics
  alias Mv.MembershipFees.MembershipFeeType
  alias MvWeb.Helpers.MembershipFeeHelpers

  @impl true
  def mount(_params, _session, socket) do
    # Only static assigns and fee types here; load_statistics runs once in handle_params
    socket =
      socket
      |> assign(:page_title, gettext("Statistics"))
      |> assign(:selected_fee_type_id, nil)
      |> load_fee_types()

    {:ok, socket}
  end

  @impl true
  def handle_params(params, uri, socket) do
    # Query params: after push_patch, params may not include query string in some cases;
    # always derive from URI as well so fee_type_id is reliable.
    uri_query = if uri, do: URI.decode_query(URI.parse(uri).query || ""), else: %{}
    fee_type_id = params["fee_type_id"] || uri_query["fee_type_id"]
    fee_type_id = normalize_fee_type_id(fee_type_id)

    socket =
      socket
      |> assign(:selected_fee_type_id, fee_type_id)
      |> load_statistics()

    {:noreply, socket}
  end

  defp normalize_fee_type_id(nil), do: nil
  defp normalize_fee_type_id(""), do: nil

  defp normalize_fee_type_id(id) when is_binary(id) do
    case String.trim(id) do
      "" -> nil
      trimmed -> trimmed
    end
  end

  defp normalize_fee_type_id(_), do: nil

  @impl true
  def render(assigns) do
    ~H"""
    <Layouts.app flash={@flash} current_user={@current_user}>
      <.header>
        {gettext("Statistics")}
        <:subtitle>{gettext("Overview from first membership to today")}</:subtitle>
      </.header>

      <section class="mb-8" aria-labelledby="members-heading">
        <h2 id="members-heading" class="text-xl font-semibold mb-4">{gettext("Members")}</h2>
        <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
          <div class="card bg-base-200 shadow-md border border-base-300">
            <div class="card-body p-5">
              <h3 class="card-title text-sm font-medium text-base-content/80">
                {gettext("Active members")}
              </h3>
              <p
                class="text-3xl font-bold tabular-nums"
                aria-label={gettext("Active members") <> ": " <> to_string(@active_count)}
              >
                {@active_count}
              </p>
            </div>
          </div>
          <div class="card bg-base-200 shadow-md border border-base-300">
            <div class="card-body p-5">
              <h3 class="card-title text-sm font-medium text-base-content/80">
                {gettext("Inactive members")}
              </h3>
              <p
                class="text-3xl font-bold tabular-nums"
                aria-label={gettext("Inactive members") <> ": " <> to_string(@inactive_count)}
              >
                {@inactive_count}
              </p>
            </div>
          </div>
        </div>
        <div class="card bg-base-200 shadow-md border border-base-300">
          <div class="card-body">
            <h3 class="card-title text-lg mb-4">{gettext("Member numbers by year")}</h3>
            <p class="text-sm text-base-content/70 mb-4">
              {gettext("From %{first} to %{last} (relevant years with membership data)",
                first: @years |> List.last() |> to_string(),
                last: @years |> List.first() |> to_string()
              )}
            </p>
            <.member_numbers_table joins_exits_by_year={@joins_exits_by_year} />
          </div>
        </div>
      </section>

      <section class="mb-8" aria-labelledby="contributions-heading">
        <h2 id="contributions-heading" class="text-xl font-semibold mb-4">
          {gettext("Contributions")}
        </h2>
        <div class="flex flex-wrap items-end gap-4 mb-6">
          <div class="flex items-center gap-2">
            <form id="fee-type-form" phx-change="change_fee_type" class="flex items-center gap-2">
              <label for="fee-type-filter" class="text-sm font-medium text-base-content/80">
                {gettext("Fee type")}:
              </label>
              <select
                id="fee-type-filter"
                name="fee_type_id"
                class="select select-bordered select-sm min-w-[10rem]"
              >
                <option value="" selected={@selected_fee_type_id in [nil, ""]}>
                  {gettext("All")}
                </option>
                <%= for ft <- @membership_fee_types do %>
                  <option
                    value={ft.id}
                    selected={to_string(@selected_fee_type_id) == to_string(ft.id)}
                  >
                    {ft.name}
                  </option>
                <% end %>
              </select>
            </form>
          </div>
        </div>
        <div class="card bg-base-200 shadow-md border border-base-300">
          <div class="card-body">
            <h3 class="card-title text-lg mb-4">{gettext("Contributions by year")}</h3>
            <div class="flex flex-col gap-8 items-start">
              <div class="w-full">
                <.contributions_bars_by_year
                  contributions_by_year={@contributions_by_year}
                  totals_over_all_years={@totals_over_all_years}
                />
              </div>
              <div class="w-full flex flex-col items-center pt-6 mt-2 border-t border-base-300">
                <h4 class="text-sm font-semibold mb-3">{gettext("All years combined (pie)")}</h4>
                <.contributions_pie cycle_totals={@totals_over_all_years} />
                <p class="text-xs text-base-content/70 mt-2">
                  <span class="inline-block w-2 h-2 rounded-full bg-success align-middle mr-1"></span>
                  {gettext("Paid")}
                  <span class="inline-block w-2 h-2 rounded-full bg-warning align-middle mx-2 mr-1">
                  </span>
                  {gettext("Unpaid")}
                  <span class="inline-block w-2 h-2 rounded-full bg-base-content/20 align-middle mx-2 mr-1">
                  </span>
                  {gettext("Suspended")}
                </p>
              </div>
            </div>
          </div>
        </div>
      </section>
    </Layouts.app>
    """
  end

  @impl true
  def handle_event("change_fee_type", %{"fee_type_id" => ""}, socket) do
    {:noreply, push_patch(socket, to: ~p"/statistics")}
  end

  def handle_event("change_fee_type", %{"fee_type_id" => id}, socket) when is_binary(id) do
    trimmed = String.trim(id)

    to =
      if trimmed == "",
        do: ~p"/statistics",
        else: ~p"/statistics" <> "?" <> URI.encode_query(%{"fee_type_id" => trimmed})

    {:noreply, push_patch(socket, to: to)}
  end

  attr :joins_exits_by_year, :list, required: true

  defp member_numbers_table(assigns) do
    rows = assigns.joins_exits_by_year
    total_activity = Enum.map(rows, fn r -> r.joins + r.exits end)
    max_total = (total_activity != [] && Enum.max(total_activity)) || 1

    rows_with_pct =
      Enum.map(rows, fn row ->
        sum = row.joins + row.exits

        bar_pct =
          if max_total > 0 and sum > 0 do
            min(100.0, Float.round(sum / max_total * 100, 1))
          else
            0
          end

        seg_scale = max(sum, 1)
        joins_pct = min(100.0, row.joins / seg_scale * 100)
        exits_pct = min(100.0, row.exits / seg_scale * 100)

        %{
          year: row.year,
          joins: row.joins,
          exits: row.exits,
          bar_pct: bar_pct,
          joins_pct: Float.round(joins_pct, 1),
          exits_pct: Float.round(exits_pct, 1)
        }
      end)

    assigns = assign(assigns, :rows, rows_with_pct)

    ~H"""
    <div
      class="overflow-x-auto"
      role="img"
      aria-label={gettext("Member numbers by year as table with bars")}
    >
      <table class="table table-sm w-full">
        <thead class="bg-base-300">
          <tr>
            <th scope="col" class="text-base-content font-semibold w-20">{gettext("Year")}</th>
            <th scope="col" class="text-base-content font-semibold">{gettext("Joins")}</th>
            <th scope="col" class="text-base-content font-semibold">{gettext("Exits")}</th>
          </tr>
        </thead>
        <tbody>
          <%= for row <- @rows do %>
            <tr>
              <td
                rowspan="2"
                class="font-mono align-middle border-b-0"
              >
                {row.year}
              </td>
              <td colspan="2" class="align-top p-1 pb-0 border-b-0">
                <div class="h-6 rounded overflow-hidden bg-base-300 relative min-w-[4rem]">
                  <div
                    class="flex h-full absolute left-0 top-0 bottom-0 min-w-0 rounded"
                    style={"width: #{max(0, row.bar_pct)}%"}
                  >
                    <div
                      class="h-full bg-success min-w-0 rounded-l"
                      style={"width: #{row.joins_pct}%"}
                      title={gettext("Joins")}
                    >
                    </div>
                    <div
                      class="h-full bg-error min-w-0 rounded-r"
                      style={"width: #{row.exits_pct}%"}
                      title={gettext("Exits")}
                    >
                    </div>
                  </div>
                </div>
              </td>
            </tr>
            <tr>
              <td class="align-middle text-xs font-mono text-base-content whitespace-nowrap pt-0">
                <span class="inline-flex items-center gap-1.5">
                  {row.joins}
                  <span
                    class="inline-block w-2 h-2 rounded-full bg-success shrink-0"
                    aria-hidden="true"
                    title={gettext("Joins")}
                  >
                  </span>
                </span>
              </td>
              <td class="align-middle text-xs font-mono text-base-content whitespace-nowrap pt-0">
                <span class="inline-flex items-center gap-1.5">
                  {row.exits}
                  <span
                    class="inline-block w-2 h-2 rounded-full bg-error shrink-0"
                    aria-hidden="true"
                    title={gettext("Exits")}
                  >
                  </span>
                </span>
              </td>
            </tr>
          <% end %>
        </tbody>
      </table>
    </div>
    """
  end

  attr :contributions_by_year, :list, required: true
  attr :totals_over_all_years, :map, required: true

  defp contributions_bars_by_year(assigns) do
    rows = assigns.contributions_by_year
    totals = assigns.totals_over_all_years

    all_rows_with_decimals =
      Enum.map(rows, fn row ->
        %{
          year: row.year,
          summary: false,
          total: row.total,
          paid: row.paid,
          unpaid: row.unpaid,
          suspended: row.suspended
        }
      end) ++
        [
          %{
            year: nil,
            summary: true,
            total: totals.total,
            paid: totals.paid,
            unpaid: totals.unpaid,
            suspended: totals.suspended
          }
        ]

    max_total = max_decimal(all_rows_with_decimals, :total)

    rows_with_pct =
      Enum.map(all_rows_with_decimals, fn row ->
        bar_pct = bar_pct(row.total, max_total)

        sum_positive =
          Decimal.add(Decimal.add(row.paid, row.unpaid), row.suspended)

        seg_scale =
          if Decimal.compare(sum_positive, 0) == :gt, do: sum_positive, else: Decimal.new(1)

        paid_pct =
          row.paid
          |> Decimal.div(seg_scale)
          |> Decimal.mult(100)
          |> Decimal.to_float()
          |> min(100.0)

        unpaid_pct =
          row.unpaid
          |> Decimal.div(seg_scale)
          |> Decimal.mult(100)
          |> Decimal.to_float()
          |> min(100.0)

        suspended_pct =
          row.suspended
          |> Decimal.div(seg_scale)
          |> Decimal.mult(100)
          |> Decimal.to_float()
          |> min(100.0)

        %{
          year: row.year,
          summary: row.summary,
          total_formatted: MembershipFeeHelpers.format_currency(row.total),
          paid_formatted: MembershipFeeHelpers.format_currency(row.paid),
          unpaid_formatted: MembershipFeeHelpers.format_currency(row.unpaid),
          suspended_formatted: MembershipFeeHelpers.format_currency(row.suspended),
          bar_pct: bar_pct,
          paid_pct: paid_pct,
          unpaid_pct: unpaid_pct,
          suspended_pct: suspended_pct
        }
      end)

    assigns = assign(assigns, :rows, rows_with_pct)

    ~H"""
    <div
      class="overflow-x-auto"
      role="img"
      aria-label={gettext("Contributions by year as table with stacked bars")}
    >
      <table class="table table-sm w-full">
        <thead class="bg-base-300">
          <tr>
            <th scope="col" class="text-base-content font-semibold w-20">{gettext("Year")}</th>
            <th scope="col" class="text-base-content font-semibold">{gettext("Paid")}</th>
            <th scope="col" class="text-base-content font-semibold">{gettext("Unpaid")}</th>
            <th scope="col" class="text-base-content font-semibold">{gettext("Suspended")}</th>
            <th scope="col" class="text-base-content font-semibold">{gettext("Total")}</th>
          </tr>
        </thead>
        <tbody>
          <%= for row <- @rows do %>
            <tr class={row.summary && "border-t-2 border-base-300 bg-base-300/30"}>
              <td
                rowspan="2"
                class={"font-mono align-middle border-b-0 #{if row.summary, do: "font-semibold", else: ""}"}
              >
                <%= if row.summary do %>
                  {gettext("Total")}
                <% else %>
                  {row.year}
                <% end %>
              </td>
              <td colspan="4" class="align-top p-1 pb-0 border-b-0">
                <div class="h-6 rounded overflow-hidden bg-base-300 relative min-w-[4rem]">
                  <div
                    class="flex h-full absolute left-0 top-0 bottom-0 min-w-0 rounded"
                    style={"width: #{max(0, Float.round(row.bar_pct, 1))}%"}
                  >
                    <div
                      class="h-full bg-success min-w-0 rounded-l"
                      style={"width: #{Float.round(row.paid_pct, 1)}%"}
                      title={gettext("Paid")}
                    >
                    </div>
                    <div
                      class="h-full bg-warning min-w-0"
                      style={"width: #{Float.round(row.unpaid_pct, 1)}%"}
                      title={gettext("Unpaid")}
                    >
                    </div>
                    <div
                      class="h-full bg-base-content/20 min-w-0 rounded-r"
                      style={"width: #{Float.round(row.suspended_pct, 1)}%"}
                      title={gettext("Suspended")}
                    >
                    </div>
                  </div>
                </div>
              </td>
            </tr>
            <tr class={row.summary && "bg-base-300/30"}>
              <td class="align-middle text-xs font-mono text-base-content whitespace-nowrap pt-0">
                <span class="inline-flex items-center gap-1.5">
                  {row.paid_formatted}
                  <span
                    class="inline-block w-2 h-2 rounded-full bg-success shrink-0"
                    aria-hidden="true"
                    title={gettext("Paid")}
                  >
                  </span>
                </span>
              </td>
              <td class="align-middle text-xs font-mono text-base-content whitespace-nowrap pt-0">
                <span class="inline-flex items-center gap-1.5">
                  {row.unpaid_formatted}
                  <span
                    class="inline-block w-2 h-2 rounded-full bg-warning shrink-0"
                    aria-hidden="true"
                    title={gettext("Unpaid")}
                  >
                  </span>
                </span>
              </td>
              <td class="align-middle text-xs font-mono text-base-content whitespace-nowrap pt-0">
                <span class="inline-flex items-center gap-1.5">
                  {row.suspended_formatted}
                  <span
                    class="inline-block w-2 h-2 rounded-full bg-base-content/20 shrink-0"
                    aria-hidden="true"
                    title={gettext("Suspended")}
                  >
                  </span>
                </span>
              </td>
              <td class="align-middle text-xs font-mono text-base-content whitespace-nowrap pt-0">
                {row.total_formatted}
              </td>
            </tr>
          <% end %>
        </tbody>
      </table>
    </div>
    """
  end

  defp max_decimal(rows, key) do
    Enum.reduce(rows, Decimal.new(0), fn row, acc ->
      val = Map.get(row, key)
      if Decimal.compare(val, acc) == :gt, do: val, else: acc
    end)
  end

  defp bar_pct(value, max) do
    scale = if Decimal.compare(max, 0) == :gt, do: max, else: Decimal.new(1)
    pct = value |> Decimal.div(scale) |> Decimal.mult(100) |> Decimal.to_float()
    min(100.0, pct)
  end

  attr :cycle_totals, :map, required: true

  defp contributions_pie(assigns) do
    paid = assigns.cycle_totals.paid
    unpaid = assigns.cycle_totals.unpaid
    suspended = assigns.cycle_totals.suspended

    sum_positive = Decimal.add(Decimal.add(paid, unpaid), suspended)
    scale = if Decimal.compare(sum_positive, 0) == :gt, do: sum_positive, else: Decimal.new(1)

    paid_pct = Decimal.div(paid, scale) |> Decimal.mult(100) |> Decimal.to_float()
    unpaid_pct = Decimal.div(unpaid, scale) |> Decimal.mult(100) |> Decimal.to_float()
    suspended_pct = Decimal.div(suspended, scale) |> Decimal.mult(100) |> Decimal.to_float()

    # Conic gradient: 0deg = top, clockwise. Success (paid), warning (unpaid), base-300 (suspended)
    # Use theme CSS variables (--color-*) so the pie renders in all themes
    paid_deg = paid_pct * 3.6
    unpaid_deg = unpaid_pct * 3.6

    gradient_stops =
      "var(--color-success) 0deg, var(--color-success) #{paid_deg}deg, var(--color-warning) #{paid_deg}deg, var(--color-warning) #{paid_deg + unpaid_deg}deg, var(--color-base-300) #{paid_deg + unpaid_deg}deg, var(--color-base-300) 360deg"

    assigns =
      assigns
      |> assign(:paid_pct, paid_pct)
      |> assign(:unpaid_pct, unpaid_pct)
      |> assign(:suspended_pct, suspended_pct)
      |> assign(:gradient_stops, gradient_stops)

    ~H"""
    <div
      class="w-40 h-40 min-h-[10rem] rounded-full shrink-0 border-2 border-base-300 bg-base-300"
      style={"background: conic-gradient(#{@gradient_stops});"}
      role="img"
      aria-label={
        gettext(
          "Contributions pie: paid %{paid}%%, unpaid %{unpaid}%%, suspended %{suspended}%%",
          paid: Float.round(@paid_pct, 1),
          unpaid: Float.round(@unpaid_pct, 1),
          suspended: Float.round(@suspended_pct, 1)
        )
      }
      title={"#{gettext("Paid")}: #{Float.round(@paid_pct, 1)}%, #{gettext("Unpaid")}: #{Float.round(@unpaid_pct, 1)}%, #{gettext("Suspended")}: #{Float.round(@suspended_pct, 1)}%"}
    >
    </div>
    """
  end

  defp load_fee_types(socket) do
    actor = current_actor(socket)

    case MembershipFeeType
         |> Ash.Query.sort(name: :asc)
         |> Ash.read(domain: Mv.MembershipFees, actor: actor) do
      {:ok, fee_types} ->
        assign(socket, :membership_fee_types, fee_types)

      {:error, reason} ->
        Logger.warning("StatisticsLive: failed to load fee types: #{inspect(reason)}")

        socket
        |> put_flash(:error, gettext("Fee types could not be loaded."))
        |> assign(:membership_fee_types, [])
    end
  end

  defp load_statistics(socket) do
    actor = current_actor(socket)
    fee_type_id = socket.assigns[:selected_fee_type_id]
    # Member stats must never depend on fee type (only contributions do)
    opts_member = [actor: actor]

    opts_contributions =
      [actor: actor] ++ if fee_type_id, do: [fee_type_id: fee_type_id], else: []

    current_year = Date.utc_today().year
    first_year = Statistics.first_join_year(opts_member) || current_year
    years = first_year..current_year |> Enum.to_list() |> Enum.reverse()

    active_count = Statistics.active_member_count(opts_member)
    inactive_count = Statistics.inactive_member_count(opts_member)
    joins_exits_by_year = build_joins_exits_by_year(years, opts_member)
    contributions_by_year = build_contributions_by_year(years, opts_contributions)
    totals_over_all_years = sum_cycle_totals(contributions_by_year)

    assign(socket,
      years: years,
      active_count: active_count,
      inactive_count: inactive_count,
      joins_exits_by_year: joins_exits_by_year,
      contributions_by_year: contributions_by_year,
      totals_over_all_years: totals_over_all_years
    )
  end

  defp build_joins_exits_by_year(years, opts) do
    Enum.map(years, fn y ->
      %{
        year: y,
        joins: Statistics.joins_by_year(y, opts),
        exits: Statistics.exits_by_year(y, opts)
      }
    end)
  end

  defp build_contributions_by_year(years, opts) do
    Enum.map(years, fn y ->
      totals = Statistics.cycle_totals_by_year(y, opts)

      %{
        year: y,
        total: totals.total,
        paid: totals.paid,
        unpaid: totals.unpaid,
        suspended: totals.suspended
      }
    end)
  end

  defp sum_cycle_totals(contributions_by_year) do
    Enum.reduce(
      contributions_by_year,
      %{
        total: Decimal.new(0),
        paid: Decimal.new(0),
        unpaid: Decimal.new(0),
        suspended: Decimal.new(0)
      },
      fn row, acc ->
        %{
          total: Decimal.add(acc.total, row.total),
          paid: Decimal.add(acc.paid, row.paid),
          unpaid: Decimal.add(acc.unpaid, row.unpaid),
          suspended: Decimal.add(acc.suspended, row.suspended)
        }
      end
    )
  end
end
