defmodule BerrypodWeb.Admin.Analytics do
  use BerrypodWeb, :live_view

  alias Berrypod.Analytics
  alias Berrypod.Cart

  @periods %{
    "today" => 0,
    "7d" => 6,
    "30d" => 29,
    "12m" => 364
  }

  @filterable_dimensions ~w(pathname referrer_source country_code browser os screen_size)

  @impl true
  def mount(_params, _session, socket) do
    {:ok,
     socket
     |> assign(:page_title, "Analytics")
     |> assign(:period, "30d")
     |> assign(:tab, "pages")
     |> assign(:filters, %{})
     |> load_analytics("30d")}
  end

  @impl true
  def handle_event("change_period", %{"period" => period}, socket)
      when is_map_key(@periods, period) do
    {:noreply,
     socket
     |> assign(:period, period)
     |> load_analytics(period)}
  end

  def handle_event("change_tab", %{"tab" => tab}, socket)
      when tab in ["pages", "sources", "countries", "devices", "funnel"] do
    {:noreply, assign(socket, :tab, tab)}
  end

  def handle_event("add_filter", %{"dimension" => dim, "value" => val}, socket)
      when dim in @filterable_dimensions do
    filters = Map.put(socket.assigns.filters, String.to_existing_atom(dim), val)

    {:noreply,
     socket
     |> assign(:filters, filters)
     |> load_analytics(socket.assigns.period)}
  end

  def handle_event("remove_filter", %{"dimension" => dim}, socket)
      when dim in @filterable_dimensions do
    filters = Map.delete(socket.assigns.filters, String.to_existing_atom(dim))

    {:noreply,
     socket
     |> assign(:filters, filters)
     |> load_analytics(socket.assigns.period)}
  end

  def handle_event("clear_filters", _params, socket) do
    {:noreply,
     socket
     |> assign(:filters, %{})
     |> load_analytics(socket.assigns.period)}
  end

  # ── Data loading ──

  defp load_analytics(socket, period) do
    range = date_range(period)
    prev_range = previous_date_range(period)
    filters = socket.assigns.filters

    trend_data =
      if period == "today",
        do: Analytics.visitors_by_hour(range, filters),
        else: Analytics.visitors_by_date(range, filters)

    visitors = Analytics.count_visitors(range, filters)
    pageviews = Analytics.count_pageviews(range, filters)
    bounce_rate = Analytics.bounce_rate(range, filters)
    avg_duration = Analytics.avg_duration(range, filters)

    prev_visitors = Analytics.count_visitors(prev_range, filters)
    prev_pageviews = Analytics.count_pageviews(prev_range, filters)
    prev_bounce_rate = Analytics.bounce_rate(prev_range, filters)
    prev_avg_duration = Analytics.avg_duration(prev_range, filters)

    socket
    |> assign(:visitors, visitors)
    |> assign(:pageviews, pageviews)
    |> assign(:bounce_rate, bounce_rate)
    |> assign(:avg_duration, avg_duration)
    |> assign(:visitors_delta, compute_delta(visitors, prev_visitors))
    |> assign(:pageviews_delta, compute_delta(pageviews, prev_pageviews))
    |> assign(:bounce_rate_delta, compute_delta(bounce_rate, prev_bounce_rate))
    |> assign(:avg_duration_delta, compute_delta(avg_duration, prev_avg_duration))
    |> assign(:trend_data, trend_data)
    |> assign(:trend_mode, if(period == "today", do: :hourly, else: :daily))
    |> assign(:top_pages, Analytics.top_pages(range, filters: filters))
    |> assign(:top_sources, Analytics.top_sources(range, filters: filters))
    |> assign(:top_referrers, Analytics.top_referrers(range, filters: filters))
    |> assign(:top_countries, Analytics.top_countries(range, filters: filters))
    |> assign(:browsers, Analytics.device_breakdown(range, :browser, filters))
    |> assign(:oses, Analytics.device_breakdown(range, :os, filters))
    |> assign(:screen_sizes, Analytics.device_breakdown(range, :screen_size, filters))
    |> assign(:funnel, Analytics.funnel(range, filters))
    |> assign(:revenue, Analytics.total_revenue(range, filters))
    |> assign(:entry_pages, Analytics.entry_pages(range, filters: filters))
    |> assign(:exit_pages, Analytics.exit_pages(range, filters: filters))
  end

  defp date_range(period) do
    days = Map.fetch!(@periods, period)
    today = Date.utc_today()
    start_date = Date.add(today, -days)
    end_date = Date.add(today, 1)

    {DateTime.new!(start_date, ~T[00:00:00], "Etc/UTC"),
     DateTime.new!(end_date, ~T[00:00:00], "Etc/UTC")}
  end

  defp previous_date_range(period) do
    days = Map.fetch!(@periods, period)
    today = Date.utc_today()
    duration = days + 1
    current_start = Date.add(today, -days)
    prev_start = Date.add(current_start, -duration)

    {DateTime.new!(prev_start, ~T[00:00:00], "Etc/UTC"),
     DateTime.new!(current_start, ~T[00:00:00], "Etc/UTC")}
  end

  defp compute_delta(0, 0), do: nil
  defp compute_delta(_current, 0), do: :new
  defp compute_delta(current, previous), do: round((current - previous) / previous * 100)

  # ── Render ──

  @impl true
  def render(assigns) do
    ~H"""
    <.header>Analytics</.header>

    <%!-- Period selector --%>
    <div class="analytics-periods">
      <button
        :for={period <- ["today", "7d", "30d", "12m"]}
        phx-click={
          JS.push("change_period", value: %{period: period})
          |> JS.set_attribute({"data-period", period}, to: "#analytics-export-link")
        }
        class={["admin-btn admin-btn-sm", @period == period && "admin-btn-primary"]}
      >
        {period_label(period)}
      </button>
      <a
        id="analytics-export-link"
        data-period={@period}
        href={export_url(@period, @filters)}
        class="admin-btn admin-btn-sm analytics-export"
        phx-hook="AnalyticsExport"
      >
        Export CSV
      </a>
    </div>

    <%!-- Active filters --%>
    <div
      :if={@filters != %{}}
      id="analytics-filters"
      class="analytics-filters"
    >
      <.filter_chip
        :for={{dim, val} <- @filters}
        dimension={dim}
        value={val}
      />
      <button
        :if={map_size(@filters) > 1}
        phx-click="clear_filters"
        class="admin-btn admin-btn-sm admin-text-secondary"
      >
        Clear all
      </button>
    </div>

    <%!-- Stat cards --%>
    <div class="admin-stats-grid admin-card-spaced">
      <.stat_card
        label="Unique visitors"
        value={format_number(@visitors)}
        icon="hero-users"
        delta={@visitors_delta}
      />
      <.stat_card
        label="Total pageviews"
        value={format_number(@pageviews)}
        icon="hero-eye"
        delta={@pageviews_delta}
      />
      <.stat_card
        label="Bounce rate"
        value={"#{@bounce_rate}%"}
        icon="hero-arrow-uturn-left"
        delta={@bounce_rate_delta}
        invert
      />
      <.stat_card
        label="Visit duration"
        value={format_duration(@avg_duration)}
        icon="hero-clock"
        delta={@avg_duration_delta}
      />
    </div>

    <%!-- Visitor trend chart --%>
    <div class="admin-card admin-card-spaced analytics-chart-card">
      <h3 class="analytics-tab-heading">
        Visitors over time
      </h3>
      <.bar_chart data={@trend_data} mode={@trend_mode} />
    </div>

    <%!-- Detail tabs --%>
    <div class="analytics-tab-bar">
      <button
        :for={
          tab <- [
            {"pages", "Pages"},
            {"sources", "Sources"},
            {"countries", "Countries"},
            {"devices", "Devices"},
            {"funnel", "Funnel"}
          ]
        }
        phx-click="change_tab"
        phx-value-tab={elem(tab, 0)}
        class={["admin-btn admin-btn-sm", @tab == elem(tab, 0) && "admin-btn-primary"]}
      >
        {elem(tab, 1)}
      </button>
    </div>

    <%!-- Tab content --%>
    <div class="admin-card analytics-tab-panel">
      <.tab_content tab={@tab} {assigns} />
    </div>
    """
  end

  # ── Stat card component ──

  attr :label, :string, required: true
  attr :value, :any, required: true
  attr :icon, :string, required: true
  attr :delta, :any, default: nil
  attr :invert, :boolean, default: false

  defp stat_card(assigns) do
    ~H"""
    <div class="admin-card">
      <div class="admin-stat-card-body">
        <div class="admin-stat-icon">
          <.icon name={@icon} class="size-5" />
        </div>
        <div>
          <div class="admin-stat-value-row">
            <p class="admin-stat-value">{@value}</p>
            <.delta_badge :if={@delta != nil} delta={@delta} invert={@invert} />
          </div>
          <p class="admin-stat-label">{@label}</p>
        </div>
      </div>
    </div>
    """
  end

  attr :delta, :any, required: true
  attr :invert, :boolean, required: true

  defp delta_badge(%{delta: :new} = assigns) do
    ~H"""
    <span class="analytics-delta admin-text-secondary">new</span>
    """
  end

  defp delta_badge(assigns) do
    {color_class, arrow} =
      cond do
        assigns.delta > 0 && !assigns.invert -> {"admin-icon-positive", "↑"}
        assigns.delta > 0 && assigns.invert -> {"admin-text-error", "↑"}
        assigns.delta < 0 && !assigns.invert -> {"admin-text-error", "↓"}
        assigns.delta < 0 && assigns.invert -> {"admin-icon-positive", "↓"}
        true -> {"admin-text-secondary", "–"}
      end

    assigns = assign(assigns, color_class: color_class, arrow: arrow)

    ~H"""
    <span class={["analytics-delta", @color_class]}>
      {@arrow} {if abs(@delta) > 999, do: ">999%", else: "#{abs(@delta)}%"}
    </span>
    """
  end

  # ── Filter chip ──

  attr :dimension, :atom, required: true
  attr :value, :string, required: true

  defp filter_chip(assigns) do
    assigns = assign(assigns, label: filter_label(assigns.dimension, assigns.value))

    ~H"""
    <span
      data-filter-dimension={@dimension}
      data-filter-value={@value}
      class="analytics-filter-chip"
    >
      {@label}
      <button
        phx-click="remove_filter"
        phx-value-dimension={@dimension}
        class="analytics-filter-remove"
        aria-label={"Remove #{@label} filter"}
      >
        <.icon name="hero-x-mark" class="size-3" />
      </button>
    </span>
    """
  end

  defp filter_label(:pathname, val), do: "Page: #{val}"
  defp filter_label(:referrer_source, val), do: "Source: #{val}"
  defp filter_label(:country_code, val), do: "Country: #{country_name(val)}"
  defp filter_label(:browser, val), do: "Browser: #{val}"
  defp filter_label(:os, val), do: "OS: #{val}"
  defp filter_label(:screen_size, val), do: "Screen: #{val}"

  # ── Bar chart (HTML/CSS bars with readable labels) ──

  attr :data, :list, required: true
  attr :mode, :atom, required: true

  defp bar_chart(assigns) do
    data = assigns.data
    max_val = data |> Enum.map(& &1.visitors) |> Enum.max(fn -> 1 end)

    scale_max = nice_max(max_val)
    scale_mid = div(scale_max, 2)
    bar_count = max(length(data), 1)
    # No gap for dense charts (12m), small gap for sparse (today/7d)
    bar_gap = if bar_count > 60, do: 0, else: 1

    bars =
      data
      |> Enum.with_index()
      |> Enum.map(fn {entry, i} ->
        visitors = entry.visitors
        height_pct = if scale_max > 0, do: visitors / scale_max * 100, else: 0

        label =
          case assigns.mode do
            :hourly -> "#{entry.hour}:00"
            :daily -> format_date_label(entry.date)
          end

        %{height_pct: height_pct, label: label, visitors: visitors, index: i}
      end)

    # X-axis labels
    x_labels =
      cond do
        # 12m: show month names at month boundaries
        bar_count > 60 ->
          data
          |> Enum.with_index()
          |> Enum.chunk_by(fn {entry, _i} ->
            date = parse_date(entry.date)
            {date.year, date.month}
          end)
          |> Enum.map(fn [{entry, i} | _] ->
            date = parse_date(entry.date)
            %{label: Calendar.strftime(date, "%b"), index: i}
          end)
          # Skip first partial month if it starts mid-month
          |> then(fn labels ->
            case labels do
              [first | rest] ->
                date = parse_date(Enum.at(data, first.index).date)
                if date.day > 7, do: rest, else: labels

              _ ->
                labels
            end
          end)

        # Hourly: 6 evenly spaced labels
        assigns.mode == :hourly ->
          step = bar_count / 6
          Enum.filter(bars, fn bar -> rem(bar.index, max(round(step), 1)) == 0 end)

        # 30d: weekly intervals
        bar_count > 14 ->
          Enum.filter(bars, fn bar -> rem(bar.index, 7) == 0 end)

        # 7d: every bar gets a label
        true ->
          bars
      end

    assigns =
      assign(assigns,
        bars: bars,
        scale_max: scale_max,
        scale_mid: scale_mid,
        x_labels: x_labels,
        bar_gap: bar_gap
      )

    ~H"""
    <div :if={@data == []} class="analytics-empty">
      No data for this period
    </div>
    <div
      :if={@data != []}
      id="analytics-chart"
      phx-hook="ChartTooltip"
      class="analytics-chart-grid"
    >
      <%!-- Tooltip --%>
      <div data-tooltip class="analytics-tooltip"></div>
      <%!-- Row 1: Y-axis labels + chart --%>
      <div class="analytics-y-labels">
        <span>{format_number(@scale_max)}</span>
        <span>{format_number(@scale_mid)}</span>
        <span>0</span>
      </div>
      <div class="analytics-chart-area">
        <%!-- Gridlines --%>
        <div class="analytics-gridline-mid"></div>
        <div class="analytics-gridline-bottom"></div>
        <%!-- Bars container --%>
        <div data-bars class="analytics-bars" style={"gap: #{@bar_gap}px;"}>
          <div
            :for={bar <- @bars}
            data-label={bar.label}
            data-visitors={bar.visitors}
            class="analytics-bar"
            style={"height: #{max(bar.height_pct, 0.5)}%;"}
          >
          </div>
        </div>
      </div>
      <%!-- Row 2: empty cell + X-axis labels --%>
      <div></div>
      <div class="analytics-x-labels">
        <span :for={bar <- @x_labels}>{bar.label}</span>
      </div>
    </div>
    """
  end

  defp format_date_label(date) when is_binary(date) do
    case Date.from_iso8601(date) do
      {:ok, d} -> Calendar.strftime(d, "%b %d")
      _ -> date
    end
  end

  defp format_date_label(date), do: to_string(date)

  defp parse_date(date) when is_binary(date) do
    case Date.from_iso8601(date) do
      {:ok, d} -> d
      _ -> Date.utc_today()
    end
  end

  defp parse_date(%Date{} = date), do: date

  defp nice_max(0), do: 10

  defp nice_max(val) do
    magnitude = :math.pow(10, floor(:math.log10(val)))
    step = magnitude / 2
    ceil(val / step) * trunc(step)
  end

  # ── Tab content ──

  defp tab_content(%{tab: "pages"} = assigns) do
    ~H"""
    <h3 class="analytics-tab-heading">Top pages</h3>
    <.detail_table
      rows={@top_pages}
      empty_message="No page data yet"
      columns={[
        %{label: "Page", key: :pathname, filter: {:pathname, :pathname}},
        %{label: "Visitors", key: :visitors, align: :right},
        %{label: "Pageviews", key: :pageviews, align: :right}
      ]}
    />
    <h3 class="analytics-tab-heading-spaced">Entry pages</h3>
    <.detail_table
      rows={@entry_pages}
      empty_message="No entry page data yet"
      columns={[
        %{label: "Page", key: :pathname, filter: {:pathname, :pathname}},
        %{label: "Sessions", key: :sessions, align: :right}
      ]}
    />
    <h3 class="analytics-tab-heading-spaced">Exit pages</h3>
    <.detail_table
      rows={@exit_pages}
      empty_message="No exit page data yet"
      columns={[
        %{label: "Page", key: :pathname, filter: {:pathname, :pathname}},
        %{label: "Sessions", key: :sessions, align: :right}
      ]}
    />
    """
  end

  defp tab_content(%{tab: "sources"} = assigns) do
    ~H"""
    <h3 class="analytics-tab-heading">Top sources</h3>
    <.detail_table
      rows={@top_sources}
      empty_message="No referrer data yet"
      columns={[
        %{label: "Source", key: :source, filter: {:referrer_source, :source}},
        %{label: "Visitors", key: :visitors, align: :right}
      ]}
    />
    <h3 class="analytics-tab-heading-spaced">Top referrers</h3>
    <.detail_table
      rows={@top_referrers}
      empty_message="No referrer data yet"
      columns={[
        %{label: "Referrer", key: :referrer},
        %{label: "Visitors", key: :visitors, align: :right}
      ]}
    />
    """
  end

  defp tab_content(%{tab: "countries"} = assigns) do
    rows =
      Enum.map(assigns.top_countries, fn c ->
        Map.put(c, :display_name, country_name(c.country_code))
      end)

    assigns = assign(assigns, :country_rows, rows)

    ~H"""
    <h3 class="analytics-tab-heading">Countries</h3>
    <.detail_table
      rows={@country_rows}
      empty_message="No country data yet"
      columns={[
        %{label: "Country", key: :display_name, filter: {:country_code, :country_code}},
        %{label: "Visitors", key: :visitors, align: :right}
      ]}
    />
    """
  end

  defp tab_content(%{tab: "devices"} = assigns) do
    ~H"""
    <h3 class="analytics-tab-heading">Browsers</h3>
    <.detail_table
      rows={@browsers}
      empty_message="No browser data yet"
      columns={[
        %{label: "Browser", key: :name, filter: {:browser, :name}},
        %{label: "Visitors", key: :visitors, align: :right}
      ]}
    />
    <h3 class="analytics-tab-heading-spaced">Operating systems</h3>
    <.detail_table
      rows={@oses}
      empty_message="No OS data yet"
      columns={[
        %{label: "OS", key: :name, filter: {:os, :name}},
        %{label: "Visitors", key: :visitors, align: :right}
      ]}
    />
    <h3 class="analytics-tab-heading-spaced">Screen sizes</h3>
    <.detail_table
      rows={@screen_sizes}
      empty_message="No screen data yet"
      columns={[
        %{label: "Size", key: :name, filter: {:screen_size, :name}},
        %{label: "Visitors", key: :visitors, align: :right}
      ]}
    />
    """
  end

  defp tab_content(%{tab: "funnel"} = assigns) do
    ~H"""
    <h3 class="analytics-tab-heading">Conversion funnel</h3>
    <.funnel_chart funnel={@funnel} revenue={@revenue} />
    """
  end

  # ── Detail table ──

  attr :rows, :list, required: true
  attr :columns, :list, required: true
  attr :empty_message, :string, default: "No data"

  defp detail_table(assigns) do
    ~H"""
    <div :if={@rows == []} class="analytics-empty">
      {@empty_message}
    </div>
    <table :if={@rows != []} class="admin-table">
      <thead>
        <tr>
          <th
            :for={col <- @columns}
            class={col[:align] == :right && "admin-cell-end"}
          >
            {col.label}
          </th>
        </tr>
      </thead>
      <tbody>
        <tr :for={row <- @rows}>
          <td
            :for={col <- @columns}
            class={col[:align] == :right && "admin-cell-numeric"}
          >
            <%= if col[:filter] do %>
              <span
                class="admin-link"
                phx-click="add_filter"
                phx-value-dimension={elem(col.filter, 0)}
                phx-value-value={Map.get(row, elem(col.filter, 1))}
              >
                {Map.get(row, col.key)}
              </span>
            <% else %>
              {Map.get(row, col.key)}
            <% end %>
          </td>
        </tr>
      </tbody>
    </table>
    """
  end

  # ── Funnel chart ──

  attr :funnel, :map, required: true
  attr :revenue, :integer, required: true

  defp funnel_chart(assigns) do
    steps = [
      {"Product views", assigns.funnel.product_views},
      {"Add to cart", assigns.funnel.add_to_carts},
      {"Checkout", assigns.funnel.checkouts},
      {"Purchase", assigns.funnel.purchases}
    ]

    top_count = assigns.funnel.product_views

    conversion_rate =
      if top_count > 0,
        do: Float.round(assigns.funnel.purchases / top_count * 100, 1),
        else: 0.0

    steps_with_rates =
      steps
      |> Enum.with_index()
      |> Enum.map(fn {{label, count}, i} ->
        overall_rate = if top_count > 0, do: Float.round(count / top_count * 100, 1), else: 0.0
        width_pct = if top_count > 0, do: max(count / top_count * 100, 5), else: 5

        %{label: label, count: count, overall_rate: overall_rate, width_pct: width_pct, index: i}
      end)

    assigns = assign(assigns, steps: steps_with_rates, conversion_rate: conversion_rate)

    ~H"""
    <div :if={@funnel.product_views == 0} class="analytics-empty">
      No funnel data yet
    </div>
    <div :if={@funnel.product_views > 0} class="analytics-funnel">
      <div :for={step <- @steps} class="analytics-funnel-step">
        <div class="analytics-funnel-label">{step.label}</div>
        <div
          class="analytics-funnel-bar"
          style={"flex: 0 0 #{step.width_pct}%; opacity: #{1 - step.index * 0.15};"}
        >
          <span class="analytics-funnel-value">{format_number(step.count)}</span>
        </div>
        <span :if={step.index > 0} class="analytics-funnel-rate">
          {step.overall_rate}%
        </span>
      </div>
      <div class="analytics-funnel-summary">
        <span class="admin-text-bold">{@conversion_rate}% overall conversion</span>
        <span :if={@revenue > 0} class="admin-text-secondary">
          · Revenue: {Cart.format_price(@revenue)}
        </span>
      </div>
    </div>
    """
  end

  # ── Helpers ──

  defp period_label("today"), do: "Today"
  defp period_label("7d"), do: "7 days"
  defp period_label("30d"), do: "30 days"
  defp period_label("12m"), do: "12 months"

  defp export_url(period, filters) do
    params =
      Enum.reduce(filters, %{"period" => period}, fn {k, v}, acc ->
        Map.put(acc, "filter[#{k}]", v)
      end)

    "/admin/analytics/export?" <> URI.encode_query(params)
  end

  defp format_number(n) when n >= 1_000_000, do: "#{Float.round(n / 1_000_000, 1)}M"
  defp format_number(n) when n >= 1_000, do: "#{Float.round(n / 1_000, 1)}k"
  defp format_number(n), do: to_string(n)

  defp format_duration(seconds) when seconds < 60, do: "#{seconds}s"

  defp format_duration(seconds) do
    mins = div(seconds, 60)
    secs = rem(seconds, 60)
    "#{mins}m #{secs}s"
  end

  @country_names %{
    "GB" => "United Kingdom",
    "US" => "United States",
    "CA" => "Canada",
    "AU" => "Australia",
    "DE" => "Germany",
    "FR" => "France",
    "NL" => "Netherlands",
    "IE" => "Ireland",
    "AT" => "Austria",
    "BE" => "Belgium",
    "IT" => "Italy",
    "ES" => "Spain",
    "PT" => "Portugal",
    "SE" => "Sweden",
    "NO" => "Norway",
    "DK" => "Denmark",
    "FI" => "Finland",
    "PL" => "Poland",
    "CH" => "Switzerland",
    "NZ" => "New Zealand",
    "JP" => "Japan",
    "IN" => "India",
    "BR" => "Brazil",
    "MX" => "Mexico"
  }

  defp country_name(code) do
    Map.get(@country_names, code, code)
  end
end
