.radar.plotly <- function(
  agg,                                 # output of .radar_aggregate()
  x_name, by_name, facet_name,         # for hover labels
  main = NULL,
  fill, border, opacity,
  digits_d = 2,
  facet_opts = .plotly_facet_opts(),
  style_opts = .plotly_style()
) {


# ----- angular ticks from factor levels ----------------------------------
.plotly_theta_from_levels <- function(x_levels) {
  K <- length(x_levels)
  if (!is.finite(K) || K < 1L) stop("x_levels must have length >= 1")
  seq(0, 360 - 360 / K, length.out = K)
}


# ----- apply per-facet polar layout blocks -------------------------------
.plotly_apply_polar_layout <- function(p, domains, theta_deg, x_labels, rmax) {
  lab.cex <- getOption("lab_size",  1.0)
  ax.cex  <- getOption("axis_size", 0.9)
  ang.font <- list(size = round(14.5 * lab.cex))  # outer, category labels
  rad.font <- list(size = round(16.0 * ax.cex))  # inner, numeric labels

  for (i in seq_along(domains)) {
    nm <- if (i == 1L) "polar" else paste0("polar", i)
    blk <- list(
      domain     = list(x = domains[[i]]$x, y = domains[[i]]$y),
      radialaxis = list(
        visible = TRUE, showline = TRUE,
        range = c(0, rmax * 1.08),
        tickfont = rad.font
      ),
      angularaxis = list(
        type = "linear", direction = "clockwise",
        rotation = 10, tickmode = "array",
        tickvals = theta_deg, ticktext = as.list(x_labels),
        tickfont = ang.font
      )
    )
    # dynamic-named layout arg: polar, polar2, polar3, ...
    p <- do.call(plotly::layout, c(list(p), setNames(list(blk), nm)))
  }
  p
}


  # Pre-aggregated bundle from Chart() / .radar_aggregate()
  r.tab     <- agg$r.tab
  facet.fac <- agg$facet.fac
  by.fac    <- agg$by.fac
  x.fac.out <- agg$x.fac.out
  rmax      <- agg$rmax
  n.fac     <- agg$n.fac
  n.by      <- agg$n.by
  y.label   <- agg$y.label

  title.size <- round(16 * getOption("main_size", 1))

  # ---- 4) Facet domains + annotations ----
  # gaps are literal; facet_size scales each panel within its grid cell
  # (the shared rule in .plotly_make_domains_grid, matching .hier.plotly)
  if (n.fac == 1L) {
    # single panel: use most of the available space but leave breathing room
    domains <- list(list(x = c(0.08, 0.95), y = c(0.05, 0.88)))
    ann     <- list()
  } else {
    lay <- .plotly_facet_layout(
      n_fac        = n.fac,
      facet_levels = levels(facet.fac),
      facet_name   = facet_name,
      n_col_max    = facet_opts$n_col_max,
      gap_x        = facet_opts$gap_x, gap_y = facet_opts$gap_y,
      facet_size   = facet_opts$facet_size,
      # compress panels: shift down and shrink so main title has clear space
      domain_adjust = function(d) {
        span <- (d$y[2] - d$y[1]) * 0.72
        new.y1 <- min(0.78, d$y[2] - 0.10)
        new.y0 <- max(0, new.y1 - span)
        list(x = d$x, y = c(new.y0, new.y1), row = d$row, col = d$col)
      },
      y_base       = facet_opts$title_y_base,
      y_row_shift  = facet_opts$title_row_shift,
      yanchor      = "bottom",
      ann_adjust   = function(i, a, d) {
        a$y <- min(d$y[2] + 0.12, 0.97); a
      }
    )
    domains <- lay$domains
    ann     <- lay$annotations
  }

  # ---- 5) Angular setup ----
  theta.deg    <- .plotly_theta_from_levels(levels(x.fac.out))
  theta.closed <- c(theta.deg, theta.deg[1])
  text.labels  <- c(levels(x.fac.out), levels(x.fac.out)[1])

  # ---- 6) Colors + opacity ----
  # fill arrives pre-resolved from Chart() as a length-n.by vector
  fill.vec <- rep_len(fill, n.by)

  cols.hex   <- .to_hex(fill.vec)
  fills.rgba <- .maketrans_plotly(cols.hex, alpha = opacity)

  # ---- 7) Build traces ----
  p <- plotly::plot_ly()
  val.spec <- paste0(":.", if (is.null(digits_d)) 0L
                           else max(0L, as.integer(digits_d)), "f")

  for (i in seq_along(facet.fac)) {
    fac <- facet.fac[i]
    subplot.name <- if (i == 1) "polar" else paste0("polar", i)
    show.leg <- (i == 1 && n.by > 1)

    for (j in seq_along(by.fac)) {
      r.j <- as.numeric(r.tab[fac, by.fac[j], ])
      r.closed <- c(r.j, r.j[1])

      p <- plotly::add_trace(
        p,
        type  = "scatterpolar",
        theta = theta.closed,
        thetaunit = "degrees",
        r     = r.closed,
        mode  = "lines+markers",
        line  = list(width = 1.5, color = cols.hex[j]),
        marker = list(size = 4, color = cols.hex[j]),
        fill  = "toself",
        fillcolor = fills.rgba[j],
        name  = as.character(by.fac[j]),
        text  = text.labels,
        hovertemplate = paste0(
          x_name, ": %{text}<br>",
          if (n.by  > 1) paste0(by_name,    ": ", by.fac[j], "<br>") else "",
          if (n.fac > 1) paste0(facet_name, ": ", fac,       "<br>") else "",
          y.label, ": %{r", val.spec, "}<extra></extra>"
        ),
        subplot = subplot.name,
        showlegend = show.leg
      )
    }
  }

  # ---- 8) Polar layout ----
  p <- .plotly_apply_polar_layout(
    p,
    domains   = domains,
    theta_deg = theta.deg,
    x_labels  = levels(x.fac.out),
    rmax      = rmax
  )

  # `ann` was built above by .plotly_facet_layout() (empty list when n.fac==1)

  # ---- 10) Final layout ----
  # When a legend is shown (n.by > 1) place it OUTSIDE the paper (x > 1) so
  # Plotly never auto-shifts the polar domain to avoid legend overlap.
  has.legend <- n.by > 1
  r.margin   <- if (has.legend) 110L else 20L
  l.margin   <- if (n.fac == 1L) 20L else r.margin  # left never needs legend space

p <- plotly::layout(
  p,
  title = list(
    text = main,
    x = 0.5, xanchor = "center",
    y = 0.97, yanchor = "top",
    font = list(size = title.size)
  ),
  margin = list(t = round(title.size * 2.2),
               b = 10, l = l.margin, r = r.margin),
  showlegend = has.legend,
  annotations = ann,
  legend = list(
    x = 1.02,
    xanchor = "left",
    y = 1.0,
    yanchor = "top",
    bordercolor = .to_hex(style_opts$panel_border),
    borderwidth = 1,
    title = list(
      font = list(size = round(
        16 * getOption("lab_size", 1)))),
    font = list(size = round(
      18 * getOption("axis_size", 0.9)))
  )
)

  # Finalize to ensure htmlwidget form + sizingPolicy + unique id
  p <- .finalize_plotly_widget(
    p,
    kind         = "radar",
    x_name       = x_name,
    by_name      = by_name,
    add_title    = FALSE,
    nudge_viewer = (.allow.interactive() && !isTRUE(getOption("knitr.in.progress"))),
    unique_element = TRUE
  )

  return(invisible(p))
}
