export type DataGridSizingColumn = {
  id: string;
  width?: number | string;
  minWidth?: number;
  maxWidth?: number;
  resizable?: boolean;
  /** @deprecated Retained for compatibility with older column definitions. */
  fill?: boolean;
  sortable?: boolean;
  filterable?: boolean;
  sticky?: "start" | "end";
};

export type DataGridResizeTarget = {
  columnId: string;
  startWidth: number;
  minWidth: number;
  maxWidth: number;
  weight?: number;
};

export type DataGridFitMode = "free" | "cover" | "constrained";

export type DataGridColumnLayout = {
  widths: Record<string, number>;
  underflowWidth: number;
  overflowWidth: number;
};

export type DataGridColumnResize = {
  widths: Record<string, number>;
  appliedDelta: number;
};

const MIN_HEADER_LABEL_WIDTH = 72;
const SORT_CONTROL_RESERVE = 24;
const FILTER_CONTROL_RESERVE = 32;
const RESIZE_CONTROL_RESERVE = 20;
export const DATA_GRID_MAX_TRACK_WIDTH = 1_000_000;

export function dataGridLayoutSignature(
  columns: DataGridSizingColumn[],
  initialFit: string,
  resizeBehavior: string
): string {
  const columnSignature = columns.map((column) => [
    column.id,
    column.width ?? "",
    column.minWidth ?? "",
    column.maxWidth ?? "",
    column.resizable ? "r" : "f",
    column.fill ? "fill" : "",
    column.sticky ?? ""
  ].join(":")).join("|");
  return `${columnSignature}::${initialFit}::${resizeBehavior}`;
}

export function dataGridWidthsForLayout(
  persistedSignature: string | undefined,
  currentSignature: string,
  persistedWidths: Record<string, number> | undefined
): Record<string, number> | undefined {
  return persistedSignature === currentSignature ? persistedWidths : undefined;
}

export function effectiveDataGridColumnMinWidth(column: DataGridSizingColumn): number {
  const affordanceWidth = MIN_HEADER_LABEL_WIDTH
    + (column.sortable ? SORT_CONTROL_RESERVE : 0)
    + (column.filterable ? FILTER_CONTROL_RESERVE : 0)
    + (column.resizable ? RESIZE_CONTROL_RESERVE : 0);
  const declaredTrackMinimum = minmaxParts(column.width)?.minimum;
  const trackMinimum = declaredTrackMinimum ? parsePixelTrack(declaredTrackMinimum) ?? 0 : 0;
  return Math.max(column.minWidth ?? 0, trackMinimum, affordanceWidth);
}

export function effectiveDataGridColumnMaxWidth(
  column: DataGridSizingColumn,
  minimum = effectiveDataGridColumnMinWidth(column)
): number {
  const declaredMaximum = column.maxWidth ?? parsePixelTrack(minmaxParts(column.width)?.preferred);
  if (declaredMaximum !== null && declaredMaximum !== undefined) {
    return Math.max(minimum, declaredMaximum);
  }
  return Math.max(minimum, isFlexibleDataGridWidth(column.width, column.fill) ? DATA_GRID_MAX_TRACK_WIDTH : 2000);
}

export function isFlexibleDataGridWidth(width?: string | number, fill = false): boolean {
  if (fill || width === undefined) return true;
  if (typeof width === "number") return false;
  const normalized = width.trim().toLowerCase();
  return normalized === "auto"
    || parseFractionTrack(normalized) !== null
    || parsePercentageTrack(normalized) !== null
    || minmaxParts(normalized) !== null;
}

export function dataGridColumnResizeWeight(column: DataGridSizingColumn): number {
  if (column.fill) return 1;
  const width = column.width;
  if (typeof width !== "string") return 1;
  const preferred = minmaxParts(width)?.preferred ?? width;
  return parseFractionTrack(preferred) ?? parsePercentageTrack(preferred) ?? 1;
}

export function preferredDataGridColumnWidth(
  column: DataGridSizingColumn,
  containerWidth: number,
  measuredWidth?: number
): number {
  const minimum = effectiveDataGridColumnMinWidth(column);
  const maximum = effectiveDataGridColumnMaxWidth(column, minimum);
  const width = column.width;
  let preferred: number | null = null;

  if (typeof width === "number") {
    preferred = width;
  } else if (typeof width === "string") {
    const track = minmaxParts(width)?.preferred ?? width;
    preferred = parsePixelTrack(track);
    const percentage = parsePercentageTrack(track);
    if (preferred === null && percentage !== null) preferred = containerWidth * percentage / 100;
    if (preferred === null && track.trim().toLowerCase() === "auto") preferred = measuredWidth ?? minimum;
  }

  if (preferred === null) {
    preferred = isFlexibleDataGridWidth(width, column.fill) ? minimum : measuredWidth ?? minimum;
  }
  return clampWidth(preferred, minimum, maximum);
}

export function fitDataGridColumns(
  columns: DataGridSizingColumn[],
  containerWidth: number,
  measuredWidths: Record<string, number> = {},
  userWidths: Record<string, number> = {},
  fitMode: DataGridFitMode = "cover"
): DataGridColumnLayout {
  const safeContainerWidth = Math.max(0, containerWidth);
  const widths: Record<string, number> = {};

  for (const column of columns) {
    const minimum = effectiveDataGridColumnMinWidth(column);
    const maximum = effectiveDataGridColumnMaxWidth(column, minimum);
    const override = userWidths[column.id];
    widths[column.id] = override !== undefined && Number.isFinite(override)
      ? clampWidth(
        override,
        minimum,
        fitMode === "cover" ? DATA_GRID_MAX_TRACK_WIDTH : maximum
      )
      : preferredDataGridColumnWidth(column, safeContainerWidth, measuredWidths[column.id]);
  }

  const preferredTotal = totalDataGridColumnWidth(columns, widths);
  let remaining = safeContainerWidth - preferredTotal;
  const ordinaryColumns = columns.filter((column) => !column.sticky);
  const coverageColumns = ordinaryColumns.length > 0 ? ordinaryColumns : columns;
  const automaticColumns = coverageColumns.filter((column) => userWidths[column.id] === undefined);
  const responsiveColumns = automaticColumns.filter((column) =>
    isFlexibleDataGridWidth(column.width, column.fill)
  );

  if (fitMode === "free") {
    const targets = automaticColumns
      .filter((column) => remaining >= 0
        ? dataGridColumnGrowthWeight(column) > 0
        : isFlexibleDataGridWidth(column.width, column.fill) || Boolean(column.resizable))
      .map((column) => resizeTargetForLayout(
        column,
        widths,
        remaining >= 0 ? dataGridColumnGrowthWeight(column) : dataGridColumnResizeWeight(column)
      ));
    remaining = applyDataGridDistribution(widths, remaining, targets);
  } else if (remaining > 0.01) {
    remaining = applyDataGridDistribution(
      widths,
      remaining,
      responsiveColumns.map((column) =>
        resizeTargetForLayout(column, widths, dataGridColumnGrowthWeight(column))
      )
    );
    remaining = applyDataGridDistribution(
      widths,
      remaining,
      automaticColumns.map((column) =>
        resizeTargetForLayout(column, widths, widths[column.id])
      )
    );
    // Cover is a stronger invariant than maxWidth. Once preferred maxima have
    // been exhausted, ordinary data columns absorb the final residual space.
    remaining = applyDataGridDistribution(
      widths,
      remaining,
      coverageColumns.map((column) =>
        resizeTargetForLayout(column, widths, widths[column.id], DATA_GRID_MAX_TRACK_WIDTH)
      )
    );
  } else if (remaining < -0.01) {
    remaining = applyDataGridDistribution(
      widths,
      remaining,
      responsiveColumns
        .filter((column) => column.resizable || isFlexibleDataGridWidth(column.width, column.fill))
        .map((column) => resizeTargetForLayout(column, widths, widths[column.id]))
    );
    remaining = applyDataGridDistribution(
      widths,
      remaining,
      automaticColumns.map((column) =>
        resizeTargetForLayout(column, widths, widths[column.id])
      )
    );
    // Explicit user widths are hard during reconciliation. Cover prevents
    // underflow, but intentional user growth may remain horizontally scrollable.
  }

  const roundedWidths = roundDataGridWidths(widths);
  closeCoveredRoundingGap(columns, roundedWidths, safeContainerWidth, fitMode, remaining);
  const renderedTotal = totalDataGridColumnWidth(columns, roundedWidths);
  const renderedDifference = safeContainerWidth - renderedTotal;
  return {
    widths: roundedWidths,
    underflowWidth: roundWidth(Math.max(0, renderedDifference)),
    overflowWidth: roundWidth(Math.max(0, -renderedDifference))
  };
}

export function resizeDataGridColumn(
  columns: DataGridSizingColumn[],
  baseWidths: Record<string, number>,
  columnId: string,
  requestedDelta: number,
  fitMode: DataGridFitMode,
  uncompensatedShrinkRoom = 0
): DataGridColumnResize {
  const activeIndex = columns.findIndex((column) => column.id === columnId);
  const activeColumn = columns[activeIndex];
  if (!activeColumn) return { widths: roundDataGridWidths(baseWidths), appliedDelta: 0 };

  const startWidth = baseWidths[columnId]
    ?? dataGridColumnPixelWidth(activeColumn);
  const minimum = effectiveDataGridColumnMinWidth(activeColumn);
  // Cover fitting may have expanded this track beyond its preferred maximum.
  // A pointer move must never snap such a track backwards before applying the
  // user's requested direction.
  const maximum = Math.max(
    startWidth,
    effectiveDataGridColumnMaxWidth(activeColumn, minimum)
  );
  const boundedDelta = Math.min(
    maximum - startWidth,
    Math.max(minimum - startWidth, requestedDelta)
  );
  const widths = { ...baseWidths };

  if (fitMode === "free") {
    widths[columnId] = startWidth + boundedDelta;
    return {
      widths: roundDataGridWidths(widths),
      appliedDelta: roundWidth(boundedDelta)
    };
  }

  if (fitMode === "cover") {
    if (boundedDelta >= 0) {
      widths[columnId] = startWidth + boundedDelta;
      return {
        widths: roundDataGridWidths(widths),
        appliedDelta: roundWidth(boundedDelta)
      };
    }

    const requestedShrink = -boundedDelta;
    const freeShrink = Math.min(
      requestedShrink,
      Math.max(0, uncompensatedShrinkRoom)
    );
    const rightTargets = columns
      .slice(activeIndex + 1)
      .filter((column) => column.resizable && !column.sticky)
      .map((column) =>
        resizeTargetForLayout(
          column,
          widths,
          dataGridColumnResizeWeight(column)
        )
      );
    const compensation = distributeDataGridResizeAmount(
      requestedShrink - freeShrink,
      rightTargets
    );
    applyDataGridAmounts(widths, compensation.amounts);
    const appliedShrink = freeShrink + compensation.applied;
    widths[columnId] = startWidth - appliedShrink;
    return {
      widths: roundDataGridWidths(widths),
      appliedDelta: roundWidth(-appliedShrink)
    };
  }

  const freeShrink = boundedDelta < 0
    ? Math.min(-boundedDelta, Math.max(0, uncompensatedShrinkRoom))
    : 0;
  const uncompensatedDelta = -freeShrink;
  const compensationRequest = -(boundedDelta - uncompensatedDelta);
  let remainingCompensation = compensationRequest;
  let appliedCompensation = 0;

  for (const group of resizeCompensationGroups(columns, activeIndex)) {
    if (Math.abs(remainingCompensation) <= 0.01) break;
    const targets = group.map((column) =>
      resizeTargetForLayout(
        column,
        widths,
        dataGridColumnResizeWeight(column)
      )
    );
    const distribution = distributeDataGridResizeAmount(remainingCompensation, targets);
    applyDataGridAmounts(widths, distribution.amounts);
    appliedCompensation += distribution.applied;
    remainingCompensation -= distribution.applied;
  }

  const appliedDelta = uncompensatedDelta - appliedCompensation;
  widths[columnId] = startWidth + appliedDelta;
  return {
    widths: roundDataGridWidths(widths),
    appliedDelta: roundWidth(appliedDelta)
  };
}

export function distributeDataGridResizeAmount(
  requestedAmount: number,
  targets: DataGridResizeTarget[]
): { applied: number; amounts: Record<string, number> } {
  const amounts: Record<string, number> = Object.fromEntries(targets.map((target) => [target.columnId, 0]));
  const direction = Math.sign(requestedAmount);
  let remaining = Math.abs(requestedAmount);
  if (direction === 0 || remaining <= 0.01) return { applied: 0, amounts };
  let activeTargets = targets.filter((target) => resizeCapacity(target, direction) > 0.01);

  while (remaining > 0.01 && activeTargets.length > 0) {
    const totalWeight = activeTargets.reduce((total, target) => total + normalizedWeight(target.weight), 0);
    let appliedThisRound = 0;
    const nextTargets: DataGridResizeTarget[] = [];

    for (const target of activeTargets) {
      const used = Math.abs(amounts[target.columnId] ?? 0);
      const available = Math.max(0, resizeCapacity(target, direction) - used);
      const share = remaining * normalizedWeight(target.weight) / totalWeight;
      const applied = Math.min(share, available);
      amounts[target.columnId] = (amounts[target.columnId] ?? 0) + applied * direction;
      appliedThisRound += applied;
      if (available - applied > 0.01) nextTargets.push(target);
    }

    if (appliedThisRound <= 0.01) break;
    remaining -= appliedThisRound;
    activeTargets = nextTargets;
  }

  return {
    applied: (Math.abs(requestedAmount) - remaining) * direction,
    amounts
  };
}

export function dataGridColumnPixelWidth(
  column: DataGridSizingColumn,
  savedWidth?: number,
  measuredWidth?: number,
  containerWidth = 0
): number {
  const minimum = effectiveDataGridColumnMinWidth(column);
  const maximum = effectiveDataGridColumnMaxWidth(column, minimum);
  const preferred = savedWidth
    ?? measuredWidth
    ?? preferredDataGridColumnWidth(column, containerWidth, measuredWidth);
  return clampWidth(preferred, minimum, maximum);
}

export function dataGridColumnTrackWithMinimum(width: string, minimum: number): string {
  const normalized = width.trim();
  const parts = minmaxParts(normalized);
  if (parts) {
    const parsedMinimum = parsePixelTrack(parts.minimum);
    const minimumTrack = parsedMinimum === null ? `${minimum}px` : `${Math.max(minimum, parsedMinimum)}px`;
    return `minmax(${minimumTrack}, ${parts.preferred})`;
  }
  const parsed = parsePixelTrack(normalized);
  if (parsed !== null) return `${Math.max(minimum, parsed)}px`;
  if (normalized.toLowerCase() === "auto" || parseFractionTrack(normalized) !== null) {
    return `minmax(${minimum}px, ${normalized})`;
  }
  return normalized;
}

export function totalDataGridColumnWidth(
  columns: DataGridSizingColumn[],
  widths: Record<string, number>
): number {
  return columns.reduce(
    (total, column) => total + (widths[column.id] ?? dataGridColumnPixelWidth(column)),
    0
  );
}

export function roundDataGridWidths(widths: Record<string, number>): Record<string, number> {
  return Object.fromEntries(
    Object.entries(widths).map(([columnId, width]) => [columnId, Math.round(width * 100) / 100])
  );
}

function minmaxParts(width?: string | number): { minimum: string; preferred: string } | null {
  if (typeof width !== "string") return null;
  const match = width.trim().match(/^minmax\(\s*([^,]+),\s*(.+)\)$/i);
  return match ? { minimum: match[1].trim(), preferred: match[2].trim() } : null;
}

function dataGridColumnGrowthWeight(column: DataGridSizingColumn): number {
  if (column.fill || column.width === undefined) return 1;
  if (typeof column.width !== "string") return 0;
  const preferred = minmaxParts(column.width)?.preferred ?? column.width;
  return parseFractionTrack(preferred) ?? parsePercentageTrack(preferred) ?? 0;
}

function resizeTargetForLayout(
  column: DataGridSizingColumn,
  widths: Record<string, number>,
  weight: number,
  maximum?: number
): DataGridResizeTarget {
  const minimum = effectiveDataGridColumnMinWidth(column);
  return {
    columnId: column.id,
    startWidth: widths[column.id] ?? dataGridColumnPixelWidth(column),
    minWidth: minimum,
    maxWidth: maximum ?? effectiveDataGridColumnMaxWidth(column, minimum),
    weight
  };
}

function applyDataGridDistribution(
  widths: Record<string, number>,
  requestedAmount: number,
  targets: DataGridResizeTarget[]
): number {
  if (Math.abs(requestedAmount) <= 0.01 || targets.length === 0) return requestedAmount;
  const distribution = distributeDataGridResizeAmount(requestedAmount, targets);
  applyDataGridAmounts(widths, distribution.amounts);
  return requestedAmount - distribution.applied;
}

function applyDataGridAmounts(
  widths: Record<string, number>,
  amounts: Record<string, number>
): void {
  for (const [columnId, amount] of Object.entries(amounts)) {
    widths[columnId] = (widths[columnId] ?? 0) + amount;
  }
}

function resizeCompensationGroups(
  columns: DataGridSizingColumn[],
  activeIndex: number
): DataGridSizingColumn[][] {
  const right = columns.slice(activeIndex + 1).filter((column) => !column.sticky);
  const left = columns.slice(0, activeIndex).reverse().filter((column) => !column.sticky);
  return [
    right.filter((column) => column.resizable),
    right.filter((column) => !column.resizable),
    left.filter((column) => column.resizable),
    left.filter((column) => !column.resizable)
  ].filter((group) => group.length > 0);
}

function closeCoveredRoundingGap(
  columns: DataGridSizingColumn[],
  widths: Record<string, number>,
  containerWidth: number,
  fitMode: DataGridFitMode,
  unallocatedAmount: number
): void {
  if (fitMode !== "cover" || Math.abs(unallocatedAmount) > 0.01) return;
  const roundedTotal = totalDataGridColumnWidth(columns, widths);
  const correction = roundWidth(containerWidth - roundedTotal);
  if (Math.abs(correction) <= 0.001) return;
  const ordinaryColumns = columns.filter((column) => !column.sticky);
  const target = ordinaryColumns[ordinaryColumns.length - 1] ?? columns[columns.length - 1];
  if (!target) return;
  widths[target.id] = roundWidth((widths[target.id] ?? 0) + correction);
}

function parsePixelTrack(track?: string): number | null {
  if (!track) return null;
  const match = track.trim().match(/^(\d+(?:\.\d+)?)px$/i);
  if (!match) return null;
  const value = Number.parseFloat(match[1]);
  return Number.isFinite(value) ? value : null;
}

function parsePercentageTrack(track?: string): number | null {
  if (!track) return null;
  const match = track.trim().match(/^(\d+(?:\.\d+)?)%$/);
  if (!match) return null;
  const value = Number.parseFloat(match[1]);
  return Number.isFinite(value) && value >= 0 ? value : null;
}

function parseFractionTrack(track?: string): number | null {
  if (!track) return null;
  const match = track.trim().match(/^(\d+(?:\.\d+)?)?fr$/i);
  if (!match) return null;
  const value = match[1] ? Number.parseFloat(match[1]) : 1;
  return Number.isFinite(value) && value > 0 ? value : null;
}

function resizeCapacity(target: DataGridResizeTarget, direction: number): number {
  return direction > 0
    ? Math.max(0, target.maxWidth - target.startWidth)
    : Math.max(0, target.startWidth - target.minWidth);
}

function normalizedWeight(weight?: number): number {
  return Number.isFinite(weight) && (weight ?? 0) > 0 ? weight as number : 1;
}

function clampWidth(width: number, minimum: number, maximum: number): number {
  return Math.min(maximum, Math.max(minimum, width));
}

function roundWidth(width: number): number {
  return Math.round(width * 100) / 100;
}
