/**
 * Dashboard layout shared by CauseWise.io, the WordPress admin, and Gutenberg embeds.
 * This module does not import React and does not open browser storage.
 * A host can reorder, resize, and hide panels, then save the small preference
 * object from layoutPreference(). Positions use a 12-column grid.
 */

export const DASHBOARD_LAYOUT_VERSION = 1;

export const DASHBOARD_COLUMNS = 12;

export const CHART_TYPES = [
  {
    id: "bar",
    label: "Bar",
    implemented: true,
    series: "counts",
  },
  {
    id: "line",
    label: "Line",
    implemented: false,
    series: "counts",
  },
  {
    id: "area",
    label: "Area",
    implemented: false,
    series: "counts",
  },
  {
    id: "donut",
    label: "Donut",
    implemented: false,
    series: "shares",
    aliases: ["pie"],
  },
] as const;

export type ChartType = (typeof CHART_TYPES)[number]["id"];

export const DASHBOARD_PANEL_TYPES = [
  "stats",
  "traffic",
  "top-pages",
  "referrers",
  "goals",
  "funnels",
  "events",
] as const;

export type DashboardPanelType = (typeof DASHBOARD_PANEL_TYPES)[number];

export const DASHBOARD_SECTION_KEYS = [
  "stats",
  "traffic",
  "pages",
  "referrers",
  "goals",
  "funnels",
  "events",
] as const;

export type DashboardSectionKey = (typeof DASHBOARD_SECTION_KEYS)[number];

export type DashboardRange = "today" | "7d" | "30d";

export type DashboardPanel = {
  id: string;
  type: DashboardPanelType;
  visible: boolean;
  /** Column start, zero-based, inside DASHBOARD_COLUMNS. */
  x: number;
  /** Row start, zero-based. */
  y: number;
  /** Column span. */
  w: number;
  /** Row span. Content sets the visual height until a resize editor stores a new span. */
  h: number;
  chartType: ChartType;
};

export type DashboardLayout = {
  version: typeof DASHBOARD_LAYOUT_VERSION;
  range: DashboardRange;
  columns: typeof DASHBOARD_COLUMNS;
  panels: DashboardPanel[];
};

const PANEL_NAV: Record<DashboardPanelType, { id: string; label: string }> = {
  stats: { id: "causewise-overview", label: "Overview" },
  traffic: { id: "causewise-traffic", label: "Traffic" },
  "top-pages": { id: "causewise-pages", label: "Pages" },
  referrers: { id: "causewise-referrers", label: "Referrers" },
  goals: { id: "causewise-goals", label: "Goals" },
  funnels: { id: "causewise-funnels", label: "Funnels" },
  events: { id: "causewise-recent-events", label: "Recent Events" },
};

const SECTION_PANEL: Record<string, DashboardPanelType> = {
  stats: "stats",
  traffic: "traffic",
  pages: "top-pages",
  "top-pages": "top-pages",
  referrers: "referrers",
  goals: "goals",
  funnels: "funnels",
  events: "events",
  "recent-events": "events",
};

function panel(partial: DashboardPanel): DashboardPanel {
  return partial;
}

export const DEFAULT_DASHBOARD_LAYOUT: DashboardLayout = {
  version: DASHBOARD_LAYOUT_VERSION,
  range: "7d",
  columns: DASHBOARD_COLUMNS,
  panels: [
    panel({ id: "overview", type: "stats", visible: true, x: 0, y: 0, w: 12, h: 2, chartType: "bar" }),
    panel({ id: "traffic", type: "traffic", visible: true, x: 0, y: 2, w: 12, h: 4, chartType: "bar" }),
    panel({ id: "top-pages", type: "top-pages", visible: true, x: 0, y: 6, w: 6, h: 4, chartType: "bar" }),
    panel({ id: "referrers", type: "referrers", visible: true, x: 6, y: 6, w: 6, h: 4, chartType: "bar" }),
    panel({ id: "goals", type: "goals", visible: true, x: 0, y: 10, w: 12, h: 4, chartType: "bar" }),
    panel({ id: "funnels", type: "funnels", visible: true, x: 0, y: 14, w: 12, h: 4, chartType: "bar" }),
    panel({ id: "events", type: "events", visible: true, x: 0, y: 18, w: 12, h: 4, chartType: "bar" }),
  ],
};

export function isChartType(value: string | undefined): value is ChartType {
  return CHART_TYPES.some((item) => item.id === value);
}

export function normalizeChartType(value: string | undefined): ChartType {
  if (value === "pie") return "donut";
  return isChartType(value) ? value : "bar";
}

/** The chart this release can draw. Line, area, and donut stay in the schema. */
export function renderableChartType(value: string | undefined): ChartType {
  const requested = normalizeChartType(value);
  const match = CHART_TYPES.find((item) => item.id === requested);
  return match?.implemented ? match.id : "bar";
}

export function chartTypeForPanel(layout: DashboardLayout, type: DashboardPanelType): ChartType {
  const found = layout.panels.find((item) => item.type === type);
  return renderableChartType(found?.chartType);
}

export function panelNavItem(type: DashboardPanelType) {
  return PANEL_NAV[type];
}

export function panelTypeForToken(token: string): DashboardPanelType | undefined {
  return SECTION_PANEL[token.trim().toLowerCase()];
}

function allVisible(visible: boolean): Record<DashboardPanelType, boolean> {
  return {
    stats: visible,
    traffic: visible,
    "top-pages": visible,
    referrers: visible,
    goals: visible,
    funnels: visible,
    events: visible,
  };
}

export function sectionVisibility(
  sections?: Partial<Record<DashboardSectionKey, boolean>> | string,
): Record<DashboardPanelType, boolean> {
  if (sections === undefined) return allVisible(true);
  if (typeof sections === "string") {
    const next = allVisible(false);
    for (const part of sections.split(",")) {
      const type = panelTypeForToken(part);
      if (type) next[type] = true;
    }
    return next;
  }
  const next = allVisible(true);
  for (const [key, value] of Object.entries(sections)) {
    const type = panelTypeForToken(key);
    if (type && typeof value === "boolean") next[type] = value;
  }
  return next;
}

export function clampPanel(panelItem: DashboardPanel): DashboardPanel {
  let w = Number.isFinite(panelItem.w) ? Math.floor(panelItem.w) : DASHBOARD_COLUMNS;
  if (w < 1) w = 1;
  if (w > DASHBOARD_COLUMNS) w = DASHBOARD_COLUMNS;
  let x = Number.isFinite(panelItem.x) ? Math.floor(panelItem.x) : 0;
  if (x < 0) x = 0;
  if (x > DASHBOARD_COLUMNS - w) x = DASHBOARD_COLUMNS - w;
  let y = Number.isFinite(panelItem.y) ? Math.floor(panelItem.y) : 0;
  if (y < 0) y = 0;
  let h = Number.isFinite(panelItem.h) ? Math.floor(panelItem.h) : 1;
  if (h < 1) h = 1;
  const id = (panelItem.id || panelItem.type).replace(/[^A-Za-z0-9_-]/g, "") || panelItem.type;
  return {
    id,
    type: panelItem.type,
    visible: panelItem.visible !== false,
    x,
    y,
    w,
    h,
    chartType: normalizeChartType(panelItem.chartType),
  };
}

export function dashboardLayout(options?: {
  range?: string;
  sections?: Partial<Record<DashboardSectionKey, boolean>> | string;
}): DashboardLayout {
  const range: DashboardRange =
    options?.range === "today" || options?.range === "30d" || options?.range === "7d"
      ? options.range
      : DEFAULT_DASHBOARD_LAYOUT.range;
  const visibility = sectionVisibility(options?.sections);
  return {
    version: DASHBOARD_LAYOUT_VERSION,
    range,
    columns: DASHBOARD_COLUMNS,
    panels: DEFAULT_DASHBOARD_LAYOUT.panels.map((item) =>
      clampPanel({
        ...item,
        visible: visibility[item.type],
      }),
    ),
  };
}

export function visiblePanels(layout: DashboardLayout): DashboardPanel[] {
  return layout.panels
    .filter((item) => item.visible)
    .slice()
    .sort((a, b) => a.y - b.y || a.x - b.x);
}

export function panelsOverlap(layout: DashboardLayout): boolean {
  const panels = visiblePanels(layout);
  for (let i = 0; i < panels.length; i += 1) {
    for (let j = i + 1; j < panels.length; j += 1) {
      const a = panels[i];
      const b = panels[j];
      const columns = a.x < b.x + b.w && b.x < a.x + a.w;
      const rows = a.y < b.y + b.h && b.y < a.y + a.h;
      if (columns && rows) return true;
    }
  }
  return false;
}

export const PANEL_WIDTH_OPTIONS = [
  { id: "third", label: "One third", w: 4 },
  { id: "half", label: "Half", w: 6 },
  { id: "two-thirds", label: "Two thirds", w: 8 },
  { id: "full", label: "Full width", w: 12 },
] as const;

export type PanelWidth = (typeof PANEL_WIDTH_OPTIONS)[number]["w"];

/** Preference saved for one viewer. It has no analytics values. */
export type StoredDashboardPanel = {
  id: string;
  order: number;
  visible: boolean;
  w: PanelWidth;
};

export type StoredDashboardLayout = {
  v: 1;
  panels: StoredDashboardPanel[];
};

export function normalizePanelWidth(value: number): PanelWidth {
  let best: PanelWidth = 12;
  let distance = Number.POSITIVE_INFINITY;
  for (const option of PANEL_WIDTH_OPTIONS) {
    const next = Math.abs(option.w - value);
    if (next < distance) {
      distance = next;
      best = option.w;
    }
  }
  return best;
}

/** Place visible panels from left to right, wrapping onto the next row. */
export function packLayout(layout: DashboardLayout): DashboardLayout {
  const hidden = layout.panels.filter((item) => !item.visible);
  const visible = layout.panels.filter((item) => item.visible);
  let x = 0;
  let y = 0;
  let rowHeight = 1;
  const placed = visible.map((item) => {
    const w = normalizePanelWidth(item.w);
    if (x > 0 && x + w > DASHBOARD_COLUMNS) {
      y += rowHeight;
      x = 0;
      rowHeight = 1;
    }
    const next = clampPanel({ ...item, x, y, w, visible: true });
    x += next.w;
    rowHeight = Math.max(rowHeight, next.h);
    if (x >= DASHBOARD_COLUMNS) {
      y += rowHeight;
      x = 0;
      rowHeight = 1;
    }
    return next;
  });
  const parked = hidden.map((item, index) =>
    clampPanel({
      ...item,
      x: 0,
      y: y + index + 1,
      w: normalizePanelWidth(item.w),
      visible: false,
    }),
  );
  return { ...layout, panels: [...placed, ...parked] };
}

export function movePanel(layout: DashboardLayout, id: string, direction: "earlier" | "later"): DashboardLayout {
  const hidden = layout.panels.filter((item) => !item.visible);
  const visible = layout.panels.filter((item) => item.visible);
  const index = visible.findIndex((item) => item.id === id);
  const target = direction === "earlier" ? index - 1 : index + 1;
  if (index < 0 || target < 0 || target >= visible.length) return layout;
  const next = visible.slice();
  const [item] = next.splice(index, 1);
  next.splice(target, 0, item);
  return packLayout({ ...layout, panels: [...next, ...hidden] });
}

/** Drop `sourceId` in front of `targetId`. This is the drag result. */
export function movePanelBefore(layout: DashboardLayout, sourceId: string, targetId: string): DashboardLayout {
  if (!sourceId || sourceId === targetId) return layout;
  const hidden = layout.panels.filter((item) => !item.visible);
  const visible = layout.panels.filter((item) => item.visible);
  const sourceIndex = visible.findIndex((item) => item.id === sourceId);
  if (sourceIndex < 0 || !visible.some((item) => item.id === targetId)) return layout;
  const next = visible.slice();
  const [source] = next.splice(sourceIndex, 1);
  const insertAt = next.findIndex((item) => item.id === targetId);
  if (insertAt < 0) return layout;
  next.splice(insertAt, 0, source);
  return packLayout({ ...layout, panels: [...next, ...hidden] });
}

export function resizePanel(layout: DashboardLayout, id: string, width: number): DashboardLayout {
  return packLayout({
    ...layout,
    panels: layout.panels.map((item) => (item.id === id ? { ...item, w: normalizePanelWidth(width) } : item)),
  });
}

export function setPanelVisible(layout: DashboardLayout, id: string, visible: boolean): DashboardLayout {
  return packLayout({
    ...layout,
    panels: layout.panels.map((item) => (item.id === id ? { ...item, visible } : item)),
  });
}

export function layoutPreference(layout: DashboardLayout): StoredDashboardLayout {
  return {
    v: 1,
    panels: layout.panels.map((item, order) => ({
      id: item.id,
      order,
      visible: item.visible,
      w: normalizePanelWidth(item.w),
    })),
  };
}

export function parseStoredDashboardLayout(raw: string): StoredDashboardLayout | null {
  try {
    const value = JSON.parse(raw) as unknown;
    if (!value || typeof value !== "object" || Array.isArray(value)) return null;
    const record = value as Record<string, unknown>;
    if (Object.keys(record).some((key) => key !== "v" && key !== "panels")) return null;
    if (record.v !== 1 || !Array.isArray(record.panels)) return null;
    const panels: StoredDashboardPanel[] = [];
    for (const item of record.panels) {
      if (!item || typeof item !== "object" || Array.isArray(item)) return null;
      const panel = item as Record<string, unknown>;
      if (Object.keys(panel).some((key) => !["id", "order", "visible", "w"].includes(key))) return null;
      if (typeof panel.id !== "string" || !/^[A-Za-z0-9_-]{1,40}$/.test(panel.id)) return null;
      if (typeof panel.order !== "number" || !Number.isInteger(panel.order)) return null;
      if (typeof panel.visible !== "boolean") return null;
      if (panel.w !== 4 && panel.w !== 6 && panel.w !== 8 && panel.w !== 12) return null;
      panels.push({ id: panel.id, order: panel.order, visible: panel.visible, w: panel.w });
    }
    return { v: 1, panels };
  } catch {
    return null;
  }
}

/**
 * Apply a viewer preference onto the author layout.
 * Panels the author turned off stay unavailable, even if the preference shows them.
 */
export function applyLayoutPreference(base: DashboardLayout, stored: StoredDashboardLayout | null): DashboardLayout {
  const allowed = new Set(base.panels.filter((item) => item.visible).map((item) => item.id));
  if (!stored) return packLayout({ ...base, panels: base.panels.filter((item) => allowed.has(item.id)) });
  const byId = new Map(base.panels.filter((item) => allowed.has(item.id)).map((item) => [item.id, item]));
  const ordered: DashboardPanel[] = [];
  const seen = new Set<string>();
  for (const pref of [...stored.panels].sort((a, b) => a.order - b.order)) {
    const panel = byId.get(pref.id);
    if (!panel || seen.has(pref.id)) continue;
    seen.add(pref.id);
    ordered.push({ ...panel, visible: pref.visible, w: pref.w });
  }
  for (const panel of base.panels) {
    if (!allowed.has(panel.id) || seen.has(panel.id)) continue;
    ordered.push(panel);
  }
  return packLayout({ ...base, panels: ordered });
}

export function panelStyle(panelItem: DashboardPanel): Record<"--cw-col-start" | "--cw-col-span" | "--cw-row-start" | "--cw-row-span", string> {
  const placed = clampPanel(panelItem);
  return {
    "--cw-col-start": String(placed.x + 1),
    "--cw-col-span": String(placed.w),
    "--cw-row-start": String(placed.y + 1),
    "--cw-row-span": String(placed.h),
  };
}
