import {
  DeesElement,
  customElement,
  property,
  state,
  html,
  type TemplateResult,
} from '@design.estate/dees-element';

import '../dees-icon/dees-icon.js';
import '../dees-contextmenu/dees-contextmenu.js';
import { demoFunc } from './dees-dashboardgrid.demo.js';
import { dashboardGridStyles } from './styles.js';
import {
  resolveMargins,
  calculateCellMetrics,
  calculateGridHeight,
  findAvailablePosition,
  compactLayout,
  applyLayout,
  resolveWidgetPlacement,
  type PlacementResult,
} from './layout.js';
import {
  computeGridCoordinates,
  computeResizeDimensions,
  type PointerPosition,
} from './interaction.js';
import { openWidgetContextMenu } from './contextmenu.js';
import type {
  DashboardWidget,
  DashboardMargin,
  DashboardResolvedMargins,
  GridCellMetrics,
  DashboardLayoutItem,
  LayoutDirection,
  CellHeightUnit,
} from './types.js';

declare global {
  interface HTMLElementTagNameMap {
    'dees-dashboardgrid': DeesDashboardgrid;
  }
}

type DragState = {
  widgetId: string;
  pointerId: number;
  offsetX: number;
  offsetY: number;
  start: DashboardLayoutItem;
  previousPosition: DashboardLayoutItem;
  currentPointer: PointerPosition;
  lastPlacement: PlacementResult | null;
};

type ResizeState = {
  widgetId: string;
  pointerId: number;
  handler: 'e' | 's' | 'se';
  startPointer: PointerPosition;
  start: DashboardLayoutItem;
  startWidth: number;
  startHeight: number;
  lastPlacement: PlacementResult | null;
};

@customElement('dees-dashboardgrid')
export class DeesDashboardgrid extends DeesElement {
  public static demo = demoFunc;
  public static styles = dashboardGridStyles;

  @property({ type: Array })
  accessor widgets: DashboardWidget[] = [];

  @property({ type: Number })
  accessor cellHeight: number = 80;

  @property({ type: Object })
  accessor margin: DashboardMargin = 10;

  @property({ type: Number })
  accessor columns: number = 12;

  @property({ type: Boolean })
  accessor editable: boolean = true;

  @property({ type: Boolean, reflect: true })
  accessor enableAnimation: boolean = true;

  @property({ type: String })
  accessor cellHeightUnit: CellHeightUnit = 'px';

  @property({ type: Boolean })
  accessor rtl: boolean = false;

  @property({ type: Boolean })
  accessor showGridLines: boolean = false;

  @property({ attribute: false })
  accessor layouts: Record<string, DashboardLayoutItem[]> | undefined = undefined;

  @property({ type: String })
  accessor activeBreakpoint: string = 'base';

  @state()
  accessor placeholderPosition: DashboardLayoutItem | null = null;

  @state()
  accessor metrics: GridCellMetrics | null = null;

  @state()
  accessor resolvedMargins: DashboardResolvedMargins | null = null;

  @state()
  accessor previewWidgets: DashboardWidget[] | null = null;

  private containerBounds: DOMRect | null = null;
  private dragState: DragState | null = null;
  private resizeState: ResizeState | null = null;
  private resizeObserver?: ResizeObserver;
  private interactionActive = false;

  public override async connectedCallback(): Promise<void> {
    await super.connectedCallback();
    this.computeMetrics();
    this.observeResize();
  }

  public override async disconnectedCallback(): Promise<void> {
    await super.disconnectedCallback();
    this.disconnectResizeObserver();
    this.releasePointerEvents();
  }

  protected updated(changed: Map<string, unknown>): void {
    if (
      changed.has('margin') ||
      changed.has('columns') ||
      changed.has('cellHeight') ||
      changed.has('cellHeightUnit')
    ) {
      this.computeMetrics();
    }

    if (changed.has('widgets') && !this.interactionActive) {
      this.notifyLayoutChange();
    }
  }

  public render(): TemplateResult {
    const baseWidgets = this.widgets;
    if (baseWidgets.length === 0) {
      return html`
        <div class="empty-state">
          <dees-icon .icon=${'lucide:layoutGrid'}></dees-icon>
          <div>No widgets configured</div>
          <div style="font-size: 14px; margin-top: 8px;">Add widgets to populate the dashboard</div>
        </div>
      `;
    }

    const metrics = this.ensureMetrics();
    const margins = this.resolvedMargins ?? resolveMargins(this.margin);
    const cellHeight = metrics.cellHeightPx;
    const layoutForHeight = this.previewWidgets ?? this.widgets;
    const gridHeight = calculateGridHeight(layoutForHeight, margins, cellHeight);
    const previewMap = this.previewWidgets ? new Map(this.previewWidgets.map(widget => [widget.id, widget])) : null;

    return html`
      <div class="grid-container" style="height: ${gridHeight}px;">
        ${this.showGridLines ? this.renderGridLines(metrics, gridHeight) : null}
        ${baseWidgets.map(widget => this.renderWidget(widget, metrics, margins, previewMap))}
        ${this.placeholderPosition ? this.renderPlaceholder(metrics, margins) : null}
      </div>
    `;
  }

  private renderGridLines(metrics: GridCellMetrics, gridHeight: number): TemplateResult {
    const vertical: TemplateResult[] = [];
    const horizontal: TemplateResult[] = [];
    const cellPlusMarginX = metrics.cellWidthPx + metrics.marginHorizontalPx;
    const cellPlusMarginY = metrics.cellHeightPx + metrics.marginVerticalPx;

    for (let i = 0; i <= this.columns; i++) {
      const leftPx = i * cellPlusMarginX + metrics.marginHorizontalPx;
      const leftPercent = this.pxToPercent(leftPx, metrics.containerWidth);
      vertical.push(html`<div class="grid-line-vertical" style="left: ${leftPercent}%;"></div>`);
    }

    const rows = Math.ceil(gridHeight / cellPlusMarginY);
    for (let row = 0; row <= rows; row++) {
      const top = row * cellPlusMarginY;
      horizontal.push(html`<div class="grid-line-horizontal" style="top: ${top}px;"></div>`);
    }

    return html`
      <div class="grid-lines">
        ${vertical}
        ${horizontal}
      </div>
    `;
  }

  private renderWidget(
    widget: DashboardWidget,
    metrics: GridCellMetrics,
    margins: DashboardResolvedMargins,
    previewMap: Map<string, DashboardWidget> | null,
  ): TemplateResult {
    const isDragging = this.dragState?.widgetId === widget.id;
    const isResizing = this.resizeState?.widgetId === widget.id;
    const isLocked = widget.locked || !this.editable;
    const previewWidget = previewMap?.get(widget.id) ?? null;
    const layoutForRender = isDragging ? widget : previewWidget ?? widget;
    const rect = this.computeWidgetRect(layoutForRender, metrics, margins);

    const sideProperty = this.rtl ? 'right' : 'left';
    const sideValue = this.pxToPercent(rect.left, metrics.containerWidth);
    const widthPercent = this.pxToPercent(rect.width, metrics.containerWidth);

    let transform = '';
    if (isDragging && this.dragState?.currentPointer) {
      const pointer = this.dragState.currentPointer;
      const bounds = this.containerBounds ?? this.getBoundingClientRect();
      const translateX = pointer.clientX - bounds.left - this.dragState.offsetX - rect.left;
      const translateY = pointer.clientY - bounds.top - this.dragState.offsetY - rect.top;
      transform = `transform: translate(${translateX}px, ${translateY}px);`;
    }

    return html`
      <div
        class="grid-widget ${isDragging ? 'dragging' : ''} ${isResizing ? 'resizing' : ''}"
        style="
          ${sideProperty}: ${sideValue}%;
          top: ${rect.top}px;
          width: ${widthPercent}%;
          height: ${rect.height}px;
          ${transform}
        "
        data-widget-id=${widget.id}
      >
        <div class="widget-content">
          ${widget.title
            ? html`
                <div
                  class="widget-header ${isLocked ? 'locked' : ''}"
                  @pointerdown=${!isLocked && !widget.noMove
                    ? (evt: PointerEvent) => this.startDrag(evt, widget)
                    : null}
                  @contextmenu=${(evt: MouseEvent) => this.handleWidgetContextMenu(evt, widget)}
                  tabindex=${!isLocked && !widget.noMove ? 0 : -1}
                  @keydown=${(evt: KeyboardEvent) => this.handleHeaderKeydown(evt, widget)}
                >
                  ${widget.icon ? html`<dees-icon .icon=${widget.icon}></dees-icon>` : null}
                  ${widget.title}
                </div>
              `
            : null}
          <div class="widget-body ${widget.title ? 'has-header' : ''}">
            ${widget.content}
          </div>
          ${!isLocked && !widget.noResize
            ? html`
                <div
                  class="resize-handle resize-handle-e"
                  @pointerdown=${(evt: PointerEvent) => this.startResize(evt, widget, 'e')}
                ></div>
                <div
                  class="resize-handle resize-handle-s"
                  @pointerdown=${(evt: PointerEvent) => this.startResize(evt, widget, 's')}
                ></div>
                <div
                  class="resize-handle resize-handle-se"
                  @pointerdown=${(evt: PointerEvent) => this.startResize(evt, widget, 'se')}
                ></div>
              `
            : null}
        </div>
      </div>
    `;
  }

  private renderPlaceholder(
    metrics: GridCellMetrics,
    margins: DashboardResolvedMargins,
  ): TemplateResult {
    if (!this.placeholderPosition) {
      return html``;
    }

    const rect = this.computeWidgetRect(this.placeholderPosition, metrics, margins);
    const sideProperty = this.rtl ? 'right' : 'left';
    const sideValue = this.pxToPercent(rect.left, metrics.containerWidth);
    const widthPercent = this.pxToPercent(rect.width, metrics.containerWidth);

    return html`
      <div
        class="grid-widget placeholder"
        style="
          ${sideProperty}: ${sideValue}%;
          top: ${rect.top}px;
          width: ${widthPercent}%;
          height: ${rect.height}px;
        "
      >
        <div class="widget-content"></div>
      </div>
    `;
  }

  private startDrag(event: PointerEvent, widget: DashboardWidget): void {
    if (!this.editable || widget.noMove || widget.locked) {
      return;
    }

    event.preventDefault();
    event.stopPropagation();

    const widgetElement = (event.currentTarget as HTMLElement).closest('.grid-widget') as HTMLElement | null;
    if (!widgetElement) {
      return;
    }

    const widgetRect = widgetElement.getBoundingClientRect();
    this.containerBounds = this.getBoundingClientRect();
    this.ensureMetrics();

    this.dragState = {
      widgetId: widget.id,
      pointerId: event.pointerId,
      offsetX: event.clientX - widgetRect.left,
      offsetY: event.clientY - widgetRect.top,
      start: { id: widget.id, x: widget.x, y: widget.y, w: widget.w, h: widget.h },
      previousPosition: { id: widget.id, x: widget.x, y: widget.y, w: widget.w, h: widget.h },
      currentPointer: { clientX: event.clientX, clientY: event.clientY },
      lastPlacement: null,
    };

    this.interactionActive = true;
    (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
    document.addEventListener('pointermove', this.handleDragMove);
    document.addEventListener('pointerup', this.handleDragEnd);

    this.placeholderPosition = { id: widget.id, x: widget.x, y: widget.y, w: widget.w, h: widget.h };
  }

  private handleDragMove = (event: PointerEvent): void => {
    if (!this.dragState) return;
    const metrics = this.ensureMetrics();
    const activeWidgets = this.widgets;
    const widget = activeWidgets.find(item => item.id === this.dragState!.widgetId);
    if (!widget) return;

    event.preventDefault();

    const previousPosition = this.dragState.previousPosition;

    const coords = computeGridCoordinates({
      pointer: { clientX: event.clientX, clientY: event.clientY },
      containerRect: this.containerBounds ?? this.getBoundingClientRect(),
      metrics,
      columns: this.columns,
      widget,
      rtl: this.rtl,
      dragOffsetX: this.dragState.offsetX,
      dragOffsetY: this.dragState.offsetY,
    });

    const placement = resolveWidgetPlacement(
      activeWidgets,
      widget.id,
      { x: coords.x, y: coords.y },
      this.columns,
      previousPosition,
    );
    if (placement) {
      const updatedWidget = placement.widgets.find(item => item.id === widget.id);
      this.dragState = {
        ...this.dragState,
        currentPointer: { clientX: event.clientX, clientY: event.clientY },
        lastPlacement: placement,
        previousPosition: updatedWidget
          ? { id: updatedWidget.id, x: updatedWidget.x, y: updatedWidget.y, w: updatedWidget.w, h: updatedWidget.h }
          : { id: widget.id, x: coords.x, y: coords.y, w: widget.w, h: widget.h },
      };
      this.previewWidgets = placement.widgets;
      const previewWidget = placement.widgets.find(item => item.id === widget.id);
      if (previewWidget) {
        this.placeholderPosition = {
          id: previewWidget.id,
          x: previewWidget.x,
          y: previewWidget.y,
          w: previewWidget.w,
          h: previewWidget.h,
        };
      } else {
        this.placeholderPosition = { id: widget.id, x: coords.x, y: coords.y, w: widget.w, h: widget.h };
      }
    } else {
      this.previewWidgets = null;
      this.placeholderPosition = null;
    }

    this.requestUpdate();
  };

  private handleDragEnd = (event: PointerEvent): void => {
    const dragState = this.dragState;
    if (!dragState || event.pointerId !== dragState.pointerId) {
      return;
    }

    const layoutSource = this.widgets;
    this.previewWidgets = null;

    // Always validate the final position, don't rely on lastPlacement from drag
    const target = this.placeholderPosition ?? dragState.start;
    const placement = resolveWidgetPlacement(
      layoutSource,
      dragState.widgetId,
      { x: target.x, y: target.y },
      this.columns,
      dragState.previousPosition,
    );

    if (placement) {
      // Verify that the placement doesn't result in overlapping widgets
      const finalWidget = placement.widgets.find(w => w.id === dragState.widgetId);
      if (finalWidget) {
        const hasOverlap = placement.widgets.some(w => {
          if (w.id === dragState.widgetId) return false;
          return (
            finalWidget.x < w.x + w.w &&
            finalWidget.x + finalWidget.w > w.x &&
            finalWidget.y < w.y + w.h &&
            finalWidget.y + finalWidget.h > w.y
          );
        });

        if (!hasOverlap) {
          this.commitPlacement(placement, dragState.widgetId, 'widget-move');
        } else {
          // Return to start position if overlap detected
          this.widgets = this.widgets.map(widget =>
            widget.id === dragState.widgetId ? { ...widget, x: dragState.start.x, y: dragState.start.y } : widget,
          );
        }
      }
    } else {
      // Return to start position if no valid placement
      this.widgets = this.widgets.map(widget =>
        widget.id === dragState.widgetId ? { ...widget, x: dragState.start.x, y: dragState.start.y } : widget,
      );
    }

    this.placeholderPosition = null;
    this.dragState = null;
    this.interactionActive = false;
    this.releasePointerEvents();
  };

  private startResize(event: PointerEvent, widget: DashboardWidget, handler: 'e' | 's' | 'se'): void {
    if (!this.editable || widget.noResize || widget.locked) {
      return;
    }

    event.preventDefault();
    event.stopPropagation();

    this.ensureMetrics();

    this.resizeState = {
      widgetId: widget.id,
      pointerId: event.pointerId,
      handler,
      startPointer: { clientX: event.clientX, clientY: event.clientY },
      start: { id: widget.id, x: widget.x, y: widget.y, w: widget.w, h: widget.h },
      startWidth: widget.w,
      startHeight: widget.h,
      lastPlacement: null,
    };

    this.interactionActive = true;
    (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
    document.addEventListener('pointermove', this.handleResizeMove);
    document.addEventListener('pointerup', this.handleResizeEnd);

    this.placeholderPosition = { id: widget.id, x: widget.x, y: widget.y, w: widget.w, h: widget.h };
  }

  private handleResizeMove = (event: PointerEvent): void => {
    if (!this.resizeState) return;
    const metrics = this.ensureMetrics();
    const activeWidgets = this.widgets;
    const widget = activeWidgets.find(item => item.id === this.resizeState!.widgetId);
    if (!widget) return;

    event.preventDefault();

    const nextSize = computeResizeDimensions({
      pointer: { clientX: event.clientX, clientY: event.clientY },
      containerRect: this.containerBounds ?? this.getBoundingClientRect(),
      metrics,
      startWidth: this.resizeState.startWidth,
      startHeight: this.resizeState.startHeight,
      startPointer: this.resizeState.startPointer,
      handler: this.resizeState.handler,
      widget,
      columns: this.columns,
    });

    const placement = resolveWidgetPlacement(
      activeWidgets,
      widget.id,
      { x: widget.x, y: widget.y, w: nextSize.width, h: nextSize.height },
      this.columns,
      this.resizeState.start,
    );

    if (placement) {
      this.resizeState = { ...this.resizeState, lastPlacement: placement };
      this.previewWidgets = placement.widgets;
      const previewWidget = placement.widgets.find(item => item.id === widget.id);
      if (previewWidget) {
        this.placeholderPosition = {
          id: previewWidget.id,
          x: previewWidget.x,
          y: previewWidget.y,
          w: previewWidget.w,
          h: previewWidget.h,
        };
      } else {
        this.placeholderPosition = {
          id: widget.id,
          x: widget.x,
          y: widget.y,
          w: nextSize.width,
          h: nextSize.height,
        };
      }
    } else {
      this.previewWidgets = null;
      this.placeholderPosition = null;
    }

    this.requestUpdate();
  };

  private handleResizeEnd = (event: PointerEvent): void => {
    const resizeState = this.resizeState;
    if (!resizeState || event.pointerId !== resizeState.pointerId) {
      return;
    }

    const layoutSource = this.widgets;
    this.previewWidgets = null;
    const placement =
      resizeState.lastPlacement ??
      resolveWidgetPlacement(
        layoutSource,
        resizeState.widgetId,
        {
          x: this.placeholderPosition?.x ?? resizeState.start.x,
          y: this.placeholderPosition?.y ?? resizeState.start.y,
          w: this.placeholderPosition?.w ?? resizeState.start.w,
          h: this.placeholderPosition?.h ?? resizeState.start.h,
        },
        this.columns,
        resizeState.start,
      );

    if (placement) {
      this.commitPlacement(placement, resizeState.widgetId, 'widget-resize');
    } else {
      this.widgets = this.widgets.map(widget =>
        widget.id === resizeState.widgetId ? { ...widget, w: resizeState.start.w, h: resizeState.start.h } : widget,
      );
    }

    this.placeholderPosition = null;
    this.resizeState = null;
    this.interactionActive = false;
    this.releasePointerEvents();
  };

  private handleHeaderKeydown(event: KeyboardEvent, widget: DashboardWidget): void {
    if (!this.editable || widget.noMove || widget.locked) {
      return;
    }

    const key = event.key;
    const isResize = event.shiftKey;
    let placement: PlacementResult | null = null;

    if (isResize && ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(key)) {
      event.preventDefault();
      const delta = key === 'ArrowRight' || key === 'ArrowDown' ? 1 : -1;

      if (key === 'ArrowLeft' || key === 'ArrowRight') {
        const maxWidth = widget.maxW ?? this.columns - widget.x;
        const nextWidth = Math.max(widget.minW ?? 1, Math.min(maxWidth, widget.w + delta));
        placement = resolveWidgetPlacement(
          this.widgets,
          widget.id,
          { x: widget.x, y: widget.y, w: nextWidth, h: widget.h },
          this.columns,
        );
      } else {
        const maxHeight = widget.maxH ?? Number.POSITIVE_INFINITY;
        const nextHeight = Math.max(widget.minH ?? 1, Math.min(maxHeight, widget.h + delta));
        placement = resolveWidgetPlacement(
          this.widgets,
          widget.id,
          { x: widget.x, y: widget.y, w: widget.w, h: nextHeight },
          this.columns,
        );
      }

      if (placement) {
        this.commitPlacement(placement, widget.id, 'widget-resize');
      }
      return;
    }

    const moveMap: Record<string, { dx: number; dy: number }> = {
      ArrowLeft: { dx: -1, dy: 0 },
      ArrowRight: { dx: 1, dy: 0 },
      ArrowUp: { dx: 0, dy: -1 },
      ArrowDown: { dx: 0, dy: 1 },
    };

    const delta = moveMap[key];
    if (!delta) {
      return;
    }

    event.preventDefault();
    const targetX = Math.max(0, Math.min(this.columns - widget.w, widget.x + delta.dx));
    const targetY = Math.max(0, widget.y + delta.dy);

    placement = resolveWidgetPlacement(this.widgets, widget.id, { x: targetX, y: targetY }, this.columns);
    if (placement) {
      this.commitPlacement(placement, widget.id, 'widget-move');
    }
  }

  private handleWidgetContextMenu(event: MouseEvent, widget: DashboardWidget): void {
    event.preventDefault();
    event.stopPropagation();
    openWidgetContextMenu({ widget, host: this, event });
  }

  private commitPlacement(result: PlacementResult, widgetId: string, type: 'widget-move' | 'widget-resize'): void {
    this.previewWidgets = null;
    this.widgets = result.widgets;
    const subject = this.widgets.find(item => item.id === widgetId);
    if (subject) {
      this.dispatchEvent(
        new CustomEvent(type, {
          detail: {
            widget: subject,
            displaced: result.movedWidgets.filter(id => id !== widgetId),
            swappedWith: result.swappedWith,
          },
          bubbles: true,
          composed: true,
        }),
      );
    }
  }

  public removeWidget(widgetId: string): void {
    const target = this.widgets.find(widget => widget.id === widgetId);
    if (!target) return;
    this.widgets = this.widgets.filter(widget => widget.id !== widgetId);
    this.dispatchEvent(
      new CustomEvent('widget-remove', {
        detail: { widget: target },
        bubbles: true,
        composed: true,
      }),
    );
  }

  public updateWidget(widgetId: string, updates: Partial<DashboardWidget>): void {
    this.widgets = this.widgets.map(widget => (widget.id === widgetId ? { ...widget, ...updates } : widget));
  }

  public getLayout(): DashboardLayoutItem[] {
    return this.widgets.map(({ id, x, y, w, h }) => ({ id, x, y, w, h }));
  }

  public setLayout(layout: DashboardLayoutItem[]): void {
    this.widgets = applyLayout(this.widgets, layout);
  }

  public lockGrid(): void {
    this.editable = false;
  }

  public unlockGrid(): void {
    this.editable = true;
  }

  public addWidget(widget: DashboardWidget, autoPosition = false): void {
    const nextWidget = { ...widget };
    if (autoPosition || nextWidget.autoPosition) {
      const position = findAvailablePosition(this.widgets, nextWidget.w, nextWidget.h, this.columns);
      nextWidget.x = position.x;
      nextWidget.y = position.y;
    }

    this.widgets = [...this.widgets, nextWidget];
  }

  public compact(direction: LayoutDirection = 'vertical'): void {
    const nextWidgets = this.widgets.map(widget => ({ ...widget }));
    compactLayout(nextWidgets, direction);
    this.widgets = nextWidgets;
  }

  public applyBreakpointLayout(breakpoint: string): void {
    this.activeBreakpoint = breakpoint;
    const layout = this.layouts?.[breakpoint];
    if (layout) {
      this.setLayout(layout);
    }
  }

  public notifyLayoutChange(): void {
    this.dispatchEvent(
      new CustomEvent('layout-change', {
        detail: { layout: this.getLayout() },
        bubbles: true,
        composed: true,
      }),
    );
  }

  private ensureMetrics(): GridCellMetrics {
    if (!this.metrics) {
      this.computeMetrics();
    }
    return this.metrics!;
  }

  private computeMetrics(): void {
    if (!this.isConnected) return;
    const bounds = this.getBoundingClientRect();
    this.containerBounds = bounds;
    const margins = resolveMargins(this.margin);
    this.resolvedMargins = margins;
    this.metrics = calculateCellMetrics(bounds.width, this.columns, margins, this.cellHeight, this.cellHeightUnit);
  }

  private observeResize(): void {
    if (this.resizeObserver) return;
    this.resizeObserver = new ResizeObserver(() => {
      this.computeMetrics();
    });
    this.resizeObserver.observe(this);
  }

  private disconnectResizeObserver(): void {
    this.resizeObserver?.disconnect();
    this.resizeObserver = undefined;
  }

  private releasePointerEvents(): void {
    document.removeEventListener('pointermove', this.handleDragMove);
    document.removeEventListener('pointerup', this.handleDragEnd);
    document.removeEventListener('pointermove', this.handleResizeMove);
    document.removeEventListener('pointerup', this.handleResizeEnd);
  }

  private pxToPercent(value: number, container: number): number {
    if (!container) return 0;
    return Number(((value / container) * 100).toFixed(4));
  }

  private computeWidgetRect(
    widget: Pick<DashboardWidget, 'x' | 'y' | 'w' | 'h'>,
    metrics: GridCellMetrics,
    margins: DashboardResolvedMargins,
  ) {
    const cellWidth = metrics.cellWidthPx;
    const cellHeight = metrics.cellHeightPx;
    const left = widget.x * (cellWidth + margins.horizontal) + margins.horizontal;
    const top = widget.y * (cellHeight + margins.vertical) + margins.vertical;
    const width = widget.w * cellWidth + Math.max(0, widget.w - 1) * margins.horizontal;
    const height = widget.h * cellHeight + Math.max(0, widget.h - 1) * margins.vertical;

    return { left, top, width, height };
  }
}
