import { readdirSync, statSync } from 'fs';
import path from 'path';

import {
  type Spec as DashboardV2Spec,
  type GridLayoutItemKind,
  type RowsLayoutRowKind,
} from '@grafana/schema/apis/dashboard.grafana.app/v2';

/**
 * Recursively gets all JSON files from a directory.
 * Returns an array of objects containing the full file path and relative path from the base directory.
 */
export function getFilesRecursively(
  dir: string,
  baseDir: string = dir
): Array<{ filePath: string; relativePath: string }> {
  const files: Array<{ filePath: string; relativePath: string }> = [];
  const entries = readdirSync(dir);

  for (const entry of entries) {
    const fullPath = path.join(dir, entry);
    const stat = statSync(fullPath);

    if (stat.isDirectory()) {
      files.push(...getFilesRecursively(fullPath, baseDir));
    } else if (entry.endsWith('.json')) {
      files.push({
        filePath: fullPath,
        relativePath: path.relative(baseDir, fullPath),
      });
    }
  }

  return files;
}

/**
 * Normalizes backend output to match frontend behavior.
 * The backend sets repeat properties on library panel grid items from the library panel definition,
 * but the frontend only sets repeat when explicitly set on the panel instance.
 * This function removes repeat properties from library panel items where they weren't set on the instance.
 *
 * The difference in behavior is due to how the frontend conversion is done.
 * It is not feasible to fetch all library panels async in all cases where the transformation is done.
 * Library panel repeats will be set by the library panel behavior in those cases.
 *
 * This is a temporary solution until public dashboards and scripted dashboards have proper backend conversions.
 */
export function normalizeBackendOutputForFrontendComparison(
  backendSpec: DashboardV2Spec,
  inputPanels: Array<{
    id?: number;
    libraryPanel?: { uid?: string };
    repeat?: string;
    gridPos?: { w?: number; h?: number; x?: number; y?: number };
  }>
): DashboardV2Spec {
  // Deep clone the spec to avoid mutating the original
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
  const normalized = JSON.parse(JSON.stringify(backendSpec)) as DashboardV2Spec;

  // Create a map of panel ID to whether it has explicit repeat, original width, and if it's a library panel
  const panelHasExplicitRepeat = new Map<number, boolean>();
  const panelOriginalWidth = new Map<number, number>();
  const panelIsLibraryPanel = new Map<number, boolean>();
  inputPanels.forEach((panel) => {
    if (panel.id !== undefined) {
      panelHasExplicitRepeat.set(panel.id, !!panel.repeat);
      panelIsLibraryPanel.set(panel.id, !!panel.libraryPanel);
      if (panel.gridPos?.w !== undefined) {
        panelOriginalWidth.set(panel.id, panel.gridPos.w);
      }
    }
  });

  // Helper to recursively process grid items
  function processGridItems(items: GridLayoutItemKind[]): void {
    if (!Array.isArray(items)) {
      return;
    }

    items.forEach((item) => {
      if (item.spec?.element?.name) {
        // Extract panel ID from element name (format: "panel-{id}")
        const match = item.spec.element.name.match(/^panel-(\d+)$/);
        if (match) {
          const panelId = parseInt(match[1], 10);
          const hasExplicitRepeat = panelHasExplicitRepeat.get(panelId);
          const isLibraryPanel = panelIsLibraryPanel.get(panelId);

          // Only normalize library panels - check both input panel and element kind
          const element = normalized.elements?.[item.spec.element.name];
          const isElementLibraryPanel = element?.kind === 'LibraryPanel';

          // If this is a library panel item and repeat wasn't explicitly set on the instance,
          // remove the repeat property (backend adds it from library panel definition)
          // Also restore the original width when removing repeat properties
          if ((isLibraryPanel || isElementLibraryPanel) && hasExplicitRepeat === false && item.spec.repeat) {
            delete item.spec.repeat;
            // Always restore the original width from the input panel
            const originalWidth = panelOriginalWidth.get(panelId);
            if (originalWidth !== undefined) {
              item.spec.width = originalWidth;
            }
          }
        }
      }
    });
  }

  // Process GridLayout items
  if (normalized.layout?.kind === 'GridLayout' && normalized.layout.spec?.items) {
    processGridItems(normalized.layout.spec.items);
  }

  // Process RowsLayout items
  if (normalized.layout?.kind === 'RowsLayout' && normalized.layout.spec?.rows) {
    normalized.layout.spec.rows.forEach((row: RowsLayoutRowKind) => {
      if (row.spec?.layout?.kind === 'GridLayout' && row.spec.layout.spec?.items) {
        processGridItems(row.spec.layout.spec.items);
      }
    });
  }

  return normalized;
}

/**
 * Recursively removes empty arrays from an object.
 * This normalizes the difference between frontend (which preserves empty arrays)
 * and Go backend (which omits empty arrays due to `omitempty`).
 */
export function removeEmptyArrays<T>(value: T): T {
  if (Array.isArray(value)) {
    // Recursively process array items, but don't remove the array itself here
    // (parent will handle removal if this array becomes empty after processing)
    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
    return value.map((item) => removeEmptyArrays(item)) as T;
  }

  if (value !== null && typeof value === 'object') {
    const result: Record<string, unknown> = {};
    for (const key of Object.keys(value)) {
      // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
      const v = (value as Record<string, unknown>)[key];
      if (Array.isArray(v)) {
        // Only include non-empty arrays
        if (v.length > 0) {
          result[key] = removeEmptyArrays(v);
        }
        // Skip empty arrays (don't add to result)
      } else if (v !== null && typeof v === 'object') {
        result[key] = removeEmptyArrays(v);
      } else {
        result[key] = v;
      }
    }
    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
    return result as T;
  }

  return value;
}
