hs.plotly <- function(
  x, by = NULL,
  x_name, by_name = NULL,
  breaks,
  freq,
  fill, border,
  x_lab, y_lab,
  ax, gridT1, gridT2,
  digits_d = 2,
  position = c("overlay", "stack"),
  main = NULL,
  style_opts = .plotly_style()
) {


  position <- match.arg(position)

  # --- bin geometry ---
  left   <- breaks[-length(breaks)]
  right  <- breaks[-1L]
  mids   <- (left + right) / 2
  widths <- right - left
  right.closed <- isTRUE(attr(breaks, "right") %||% TRUE)
  n.bins <- length(mids)

  # --- groups ---
  if (is.null(by)) {
    groups <- "Series 1"
    G <- 1L
  } else {
    if (is.factor(by)) groups <- levels(by) else groups <- unique(by[!is.na(by)])
    G <- length(groups)
  }

  # --- counts per bin ---
  bins.fac <- cut(x, breaks, right = right.closed, include.lowest = TRUE)

  if (G == 1L) {
    y.vec <- as.integer(table(bins.fac))
    y.mat <- cbind(y.vec)
  } else {
    tab <- table(bins.fac, factor(by, levels = groups))
    y.mat <- unclass(tab)
  }

  # --- density (per group) ---
  if (!isTRUE(freq)) {
    for (g in seq_len(G)) {
      n.g <- sum(y.mat[, g])
      if (n.g > 0) y.mat[, g] <- y.mat[, g] / (n.g * widths)
    }
  }

  rng.txt <- paste0(
    if (right.closed) "(" else "[",
    format(signif(left, 6)), ", ", format(signif(right, 6)),
    if (right.closed) "]" else ")"
  )

  .hover.group <- function(yv) {
    if (isTRUE(freq)) {
      total.y <- sum(yv)
      share   <- if (total.y > 0) yv / total.y else rep(NA_real_, n.bins)
      cum.y   <- cumsum(yv)
      cum.pct <- if (total.y > 0) cum.y / total.y else rep(NA_real_, n.bins)
      y.fmt      <- format(round(yv, digits_d), trim = TRUE)
      cum.valfmt <- format(cum.y, trim = TRUE)
      pct.fmt    <- paste0(formatC(100 * share, format = "f", digits = 1), "%")
      cum.pctfmt <- paste0(formatC(100 * cum.pct, format = "f", digits = 1), "%")
      paste0(
        "Bin: ", rng.txt,
        "<br>", y_lab, ": ", y.fmt,
        "<br>% of total: ", pct.fmt,
        "<br>Cumulative: ", cum.valfmt,
        "<br>Cumulative %: ", cum.pctfmt
      )
    } else {
      amt     <- yv * widths
      total.y <- sum(amt)
      share   <- if (total.y > 0) amt / total.y else rep(NA_real_, n.bins)
      cum.y   <- cumsum(amt)
      cum.pct <- if (total.y > 0) cum.y / total.y else rep(NA_real_, n.bins)
      y.fmt      <- format(round(yv, digits_d), trim = TRUE)
      cum.valfmt <- format(round(cum.y, digits_d), trim = TRUE)
      pct.fmt    <- paste0(formatC(100 * share, format = "f", digits = 1), "%")
      cum.pctfmt <- paste0(formatC(100 * cum.pct, format = "f", digits = 1), "%")
      paste0(
        "Bin: ", rng.txt,
        "<br>", y_lab, ": ", y.fmt,
        "<br>% of total: ", pct.fmt,
        "<br>Cumulative (area): ", cum.valfmt,
        "<br>Cumulative %: ", cum.pctfmt
      )
    }
  }

  # colors
  alpha.fill <- .auto_opacity(
    k = if (G > 1) G else 1L,
    mode = if (G > 1 && position == "stack") "stack" else "overlay"
  )
  fill.rgba.grp  <- .maketrans_plotly(rep_len(fill,   G),     alpha = alpha.fill)
  border.hex.grp <- .to_hex(         rep_len(border, G))
  fill.rgba.bin  <- .maketrans_plotly(rep_len(fill,   n.bins), alpha = alpha.fill)
  border.hex.bin <- .to_hex(         rep_len(border, n.bins))

  # --- base widget (NO elementId here) ---
  plt <- plotly::plot_ly(
    type       = "bar",
    showlegend = G > 1
  )

  align.id <- "lessR.hs.align"

  for (g in seq_len(G)) {
    yv    <- y.mat[, g]
    hover <- .hover.group(yv)
    nm    <- if (G > 1) groups[g] else NULL

    plt <- plotly::add_trace(
      plt,
      x = mids,
      y = yv,
      name   = nm,
      width  = widths * 0.98,
      offset = 0,
      alignmentgroup = align.id,
      marker = if (G == 1) {
        list(
          color = fill.rgba.bin,
          line  = list(color = border.hex.bin, width = 1)
        )
      } else {
        list(
          color = fill.rgba.grp[g],
          line  = list(color = border.hex.grp[g], width = 1)
        )
      },
      hoverinfo = "text",
      hovertext = if (!is.null(nm) && length(by_name))
        paste0(by_name, ": ", groups[g], "<br>", hover) else hover
    )
  }

  # axes, grid, layout
  border.shapes <- plot_border()
  x.shapes      <- x_grid(gridT1)

  ax.x <- axis_num(x_lab, ax$axT1, ax$axL1)
  ax.y <- axis_num(y_lab, ax$axT2, ax$axL2)

  ax.y$showgrid  <- TRUE
  ax.y$gridcolor <- .to_hex(style_opts$grid_col)
  ax.y$gridwidth <- 1
  ax.y$griddash  <- "dot"

  plt <- plotly::layout(
    plt,
    xaxis    = ax.x,
    yaxis    = ax.y,
    shapes   = c(x.shapes, border.shapes),
    template = NULL,
    barmode  = if (G > 1 && position == "stack") "stack" else "overlay",
    bargap   = 0
  )

  plt$x$layout$plot_bgcolor  <- .to_hex(style_opts$panel_fill)
  plt$x$layout$paper_bgcolor <- .to_hex(style_opts$window_fill)

  if (G > 1) {
    leg.border <- .to_hex(style_opts$legend_border)
    leg.bg     <- .to_hex(style_opts$window_fill)
    plt <- plotly::layout(
      plt,
      legend = list(
        title = list(
          text = if (length(by_name)) by_name else "",
          font = list(size = round(
            15 * getOption("lab_size", 1)))),
        font = list(size = round(
          16 * getOption("axis_size", 0.9))),
        bgcolor = leg.bg,
        bordercolor = leg.border,
        borderwidth = 1
      )
    )
  }

  plt$x$layout$title <- NULL

  plt <- .finalize_plotly_widget(
    plt,
    kind        = "histogram",
    x_name      = x_name,
    by_name     = if (G > 1) by_name else NULL,
    add_title   = FALSE,
    nudge_viewer = (.allow.interactive() && !isTRUE(getOption("knitr.in.progress")))
  )

  invisible(plt)
}
