import { t } from '@grafana/i18n';
import {
  sceneGraph,
  type SceneGridItemLike,
  SceneGridLayout,
  SceneGridRow,
  type SceneObject,
  SceneObjectBase,
  type SceneObjectState,
  type VizPanel,
} from '@grafana/scenes';
import { type Spec as DashboardV2Spec } from '@grafana/schema/apis/dashboard.grafana.app/v2';

import { dashboardEditActions, ObjectsReorderedOnCanvasEvent } from '../../edit-pane/shared';
import { serializeRowsLayout } from '../../serialization/layoutSerializers/RowsLayoutSerializer';
import { dashboardSceneGraph, type PanelIdGenerator } from '../../utils/dashboardSceneGraph';
import { getDashboardSceneFor } from '../../utils/utils';
import { AutoGridItem } from '../layout-auto-grid/AutoGridItem';
import { AutoGridLayoutManager } from '../layout-auto-grid/AutoGridLayoutManager';
import { DashboardGridItem } from '../layout-default/DashboardGridItem';
import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager';
import { RowRepeaterBehavior } from '../layout-default/RowRepeaterBehavior';
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
import { getRowFromClipboard } from '../layouts-shared/paste';
import { showConvertMixedGridsModal, showUngroupConfirmation } from '../layouts-shared/ungroupConfirmation';
import { generateUniqueTitle, GridLayoutType, mapIdToGridLayoutType, ungroupLayout } from '../layouts-shared/utils';
import { type DashboardDropTarget } from '../types/DashboardDropTarget';
import { isDashboardLayoutGrid } from '../types/DashboardLayoutGrid';
import { type DashboardLayoutGroup, isDashboardLayoutGroup } from '../types/DashboardLayoutGroup';
import { type DashboardLayoutManager } from '../types/DashboardLayoutManager';
import { isLayoutParent } from '../types/LayoutParent';
import { type LayoutRegistryItem } from '../types/LayoutRegistryItem';

import { RowItem } from './RowItem';
import { RowLayoutManagerRenderer } from './RowsLayoutManagerRenderer';

interface RowsLayoutManagerState extends SceneObjectState {
  rows: RowItem[];
  isDropTarget?: boolean;
}

export class RowsLayoutManager
  extends SceneObjectBase<RowsLayoutManagerState>
  implements DashboardLayoutGroup, DashboardDropTarget
{
  public static Component = RowLayoutManagerRenderer;
  public readonly isDashboardLayoutManager = true;
  public readonly isDashboardDropTarget = true as const;

  public static readonly descriptor: LayoutRegistryItem = {
    get name() {
      return t('dashboard.rows-layout.name', 'Rows');
    },
    get description() {
      return t('dashboard.rows-layout.description', 'Collapsable panel groups with headings');
    },
    id: 'RowsLayout',
    createFromLayout: RowsLayoutManager.createFromLayout,
    isGridLayout: false,
    icon: 'list-ul',
  };

  public serialize(isSnapshot?: boolean): DashboardV2Spec['layout'] {
    return serializeRowsLayout(this, isSnapshot);
  }

  public readonly descriptor = RowsLayoutManager.descriptor;

  public addPanel(vizPanel: VizPanel) {
    this.state.rows[0]?.getLayout().addPanel(vizPanel);
  }

  public setIsDropTarget(isDropTarget: boolean): void {
    this.setState({ isDropTarget });
  }

  public getAllGridTypes(): string[] {
    return this.state.rows.flatMap((row) => row.getLayout().getAllGridTypes());
  }

  public draggedGridItemInside(gridItem: SceneGridItemLike): void {
    // Create a new row with a DefaultGridLayoutManager and add the grid item to it
    const newLayout = new DefaultGridLayoutManager({
      grid: new SceneGridLayout({ children: [], isDraggable: true, isResizable: true }),
    });
    const newRow = new RowItem({
      title: t('dashboard.rows-layout.new-row-title', 'New row'),
      layout: newLayout,
      collapse: false,
    });

    // Convert AutoGridItem to DashboardGridItem if needed
    if (gridItem instanceof AutoGridItem) {
      const vizPanel = gridItem.state.body;
      const newGridItem = new DashboardGridItem({
        body: vizPanel.clone(),
        width: 12,
        height: 8,
      });
      newLayout.addGridItem(newGridItem);
    } else if (gridItem instanceof DashboardGridItem) {
      newLayout.addGridItem(gridItem);
    }

    // Add the row to this layout
    this.setState({ rows: [...this.state.rows, newRow], isDropTarget: false });
  }

  public getVizPanels(): VizPanel[] {
    const panels: VizPanel[] = [];

    for (const row of this.state.rows) {
      const innerPanels = row.getLayout().getVizPanels();
      panels.push(...innerPanels);
    }

    return panels;
  }

  public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager {
    return this.clone({});
  }

  // a single panel ID generator is shared across all rows to ensure that each gets a unique range of panel IDs
  public duplicate(panelIdGenerator?: PanelIdGenerator): DashboardLayoutManager {
    const gen = panelIdGenerator ?? dashboardSceneGraph.getPanelIdGenerator(this);
    const newRows = this.state.rows.map((row) => row.duplicate(gen));
    return this.clone({ rows: newRows, key: undefined });
  }

  public duplicateRow(row: RowItem) {
    const newRow = row.duplicate();
    this.addNewRow(newRow);
  }

  public addNewRow(row?: RowItem): RowItem {
    const newRow = row ?? new RowItem({ layout: getDashboardSceneFor(this).getDefaultLayout() });
    const existingNames = new Set(this.state.rows.map((row) => row.state.title).filter((title) => title !== undefined));

    const newTitle = generateUniqueTitle(newRow.state.title, existingNames);
    if (newTitle !== newRow.state.title) {
      newRow.setState({ title: newTitle });
    }

    dashboardEditActions.addElement({
      addedObject: newRow,
      source: this,
      perform: () => {
        this.setState({ rows: [...this.state.rows, newRow] });
        const dashboard = getDashboardSceneFor(this);
        if (dashboard.state.isEditing) {
          newRow.getLayout().editModeChanged?.(true);
        }
      },
      undo: () => this.setState({ rows: this.state.rows.filter((r) => r !== newRow) }),
    });

    return newRow;
  }

  public editModeChanged(isEditing: boolean) {
    this.state.rows.forEach((row) => row.getLayout().editModeChanged?.(isEditing));
  }

  public pasteRow() {
    const scene = getDashboardSceneFor(this);
    const row = getRowFromClipboard(scene);
    this.addNewRow(row);
  }

  public getOutlineChildren() {
    const outlineChildren: SceneObject[] = [];

    for (const row of this.state.rows) {
      outlineChildren.push(row);

      if (row.state.repeatedRows) {
        for (const clone of row.state.repeatedRows!) {
          outlineChildren.push(clone);
        }
      }
    }

    return outlineChildren;
  }

  public convertAllGridLayouts(gridLayoutType: GridLayoutType) {
    for (const row of this.state.rows) {
      switch (gridLayoutType) {
        case GridLayoutType.AutoGridLayout:
          if (!(row.getLayout() instanceof AutoGridLayoutManager)) {
            row.switchLayout(AutoGridLayoutManager.createFromLayout(row.getLayout()));
          }
          break;
        case GridLayoutType.GridLayout:
          if (!(row.getLayout() instanceof DefaultGridLayoutManager)) {
            row.switchLayout(DefaultGridLayoutManager.createFromLayout(row.getLayout()));
          }
          break;
      }
    }
  }

  public ungroupRows() {
    const hasNonGridLayout = this.state.rows.some((row) => !row.getLayout().descriptor.isGridLayout);
    const gridTypes = new Set(this.getAllGridTypes());

    showUngroupConfirmation({
      hasNonGridLayout,
      gridTypes,
      onConfirm: (gridLayoutType) => {
        this.wrapUngroupRowsInEdit(gridLayoutType);
      },
      onConvertMixedGrids: (availableIds) => {
        this._confirmConvertMixedGrids(availableIds);
      },
    });
  }

  private _confirmConvertMixedGrids(availableIds: Set<string>) {
    showConvertMixedGridsModal(availableIds, (id: string) => {
      const selected = mapIdToGridLayoutType(id);
      if (selected) {
        this.wrapUngroupRowsInEdit(selected);
      }
    });
  }

  private wrapUngroupRowsInEdit(gridLayoutType: GridLayoutType) {
    const parent = this.parent;
    if (!parent || !isLayoutParent(parent)) {
      throw new Error('Ungroup rows failed: parent is not a layout container');
    }

    const previousLayout = this.clone({});
    const scene = getDashboardSceneFor(this);

    dashboardEditActions.edit({
      description: t('dashboard.rows-layout.edit.ungroup-rows', 'Ungroup rows'),
      source: scene,
      perform: () => {
        this.ungroup(gridLayoutType);
      },
      undo: () => {
        parent.switchLayout(previousLayout);
      },
    });
  }

  public ungroup(gridLayoutType: GridLayoutType) {
    const hasNonGridLayout = this.state.rows.some((row) => !row.getLayout().descriptor.isGridLayout);

    if (hasNonGridLayout) {
      for (const row of this.state.rows) {
        const layout = row.getLayout();
        if (!layout.descriptor.isGridLayout) {
          if (isDashboardLayoutGroup(layout)) {
            layout.ungroup(gridLayoutType);
          } else {
            throw new Error(`Ungrouping not supported for layout type: ${layout.descriptor.name}`);
          }
        }
      }
    }

    this.convertAllGridLayouts(gridLayoutType);

    const firstRow = this.state.rows[0];
    const firstRowLayout = firstRow.getLayout();
    const otherRows = this.state.rows.slice(1);

    for (const row of otherRows) {
      const layout = row.getLayout();
      if (isDashboardLayoutGrid(firstRowLayout) && isDashboardLayoutGrid(layout)) {
        firstRowLayout.mergeGrid(layout);
      } else {
        throw new Error(`Layout type ${firstRowLayout.descriptor.name} does not support merging`);
      }
    }

    this.setState({ rows: [firstRow] });
    this.removeRow(firstRow, true);
    ungroupLayout(this, firstRow.state.layout, true);
  }

  public removeRow(row: RowItem, skipUndo?: boolean) {
    const rowsBeforeRemoval = [...this.state.rows];
    const rowsAfterRemoval = rowsBeforeRemoval.filter((r) => r !== row);
    const parent = this.parent!;
    if (!isLayoutParent(parent)) {
      throw new Error('Parent object is not a LayoutParent');
    }

    const perform = () => {
      if (!rowsAfterRemoval.length) {
        parent.switchLayout(AutoGridLayoutManager.createEmpty());
      } else {
        this.setState({ rows: rowsAfterRemoval });
      }
    };

    const thisLayout = this;
    const undo = () => {
      if (!rowsAfterRemoval.length) {
        parent.switchLayout(thisLayout);
      } else {
        this.setState({ rows: rowsBeforeRemoval });
      }
    };

    if (skipUndo) {
      perform();
    } else {
      dashboardEditActions.removeElement({
        removedObject: row,
        source: this,
        perform,
        undo,
      });
    }
  }

  public moveRow(_rowKey: string, fromIndex: number, toIndex: number) {
    const rows = [...this.state.rows];
    const [removed] = rows.splice(fromIndex, 1);
    rows.splice(toIndex, 0, removed);
    this.setState({ rows });
    this.publishEvent(new ObjectsReorderedOnCanvasEvent(this), true);
  }

  public forceSelectRow(rowKey: string) {
    const rowIndex = this.state.rows.findIndex((row) => row.state.key === rowKey);
    const row = this.state.rows[rowIndex];

    if (!row) {
      return;
    }

    const editPane = getDashboardSceneFor(this).state.editPane;
    editPane.selectObject(row!, { force: true, multi: false });
  }

  public static createEmpty(): RowsLayoutManager {
    return new RowsLayoutManager({ rows: [new RowItem()] });
  }

  public static createFromLayout(layout: DashboardLayoutManager): RowsLayoutManager {
    let rows: RowItem[] = [];

    if (layout instanceof TabsLayoutManager) {
      for (const tab of layout.state.tabs) {
        if (tab.state.repeatSourceKey) {
          continue;
        }

        const conditionalRendering = tab.state.conditionalRendering;
        conditionalRendering?.clearParent();
        // We need to clear the target since we don't want to point the original tab anymore (if it was set)
        conditionalRendering?.setTarget(undefined);

        rows.push(
          new RowItem({
            layout: tab.state.layout.clone(),
            title: tab.state.title,
            conditionalRendering,
            repeatByVariable: tab.state.repeatByVariable,
            $variables: tab.state.$variables,
          })
        );
      }
    } else if (layout instanceof DefaultGridLayoutManager) {
      const config: Array<{
        title?: string;
        isCollapsed?: boolean;
        isDraggable?: boolean;
        isResizable?: boolean;
        children: SceneGridItemLike[];
        repeat?: string;
      }> = [];
      let children: SceneGridItemLike[] | undefined;

      layout.state.grid.forEachChild((child) => {
        if (!(child instanceof DashboardGridItem) && !(child instanceof SceneGridRow)) {
          throw new Error('Child is not a DashboardGridItem or SceneGridRow, invalid scene');
        }

        if (child instanceof SceneGridRow) {
          // Skip repeated row clones
          if ('repeatSourceKey' in child.state && child.state.repeatSourceKey) {
            return;
          }

          const behaviour = child.state.$behaviors?.find((b) => b instanceof RowRepeaterBehavior);

          config.push({
            title: child.state.title,
            isCollapsed: !!child.state.isCollapsed,
            isDraggable: child.state.isDraggable,
            isResizable: child.state.isResizable,
            children: child.state.children,
            repeat: behaviour?.state.variableName,
          });

          // Since we encountered a row item, any subsequent panels should be added to a new row
          children = undefined;
        } else {
          if (!children) {
            children = [];
            config.push({ children });
          }

          children.push(child);
        }
      });

      rows = config.map(
        (rowConfig) =>
          new RowItem({
            title: rowConfig.title,
            collapse: !!rowConfig.isCollapsed,
            repeatByVariable: rowConfig.repeat,
            layout: DefaultGridLayoutManager.fromGridItems(
              rowConfig.children,
              rowConfig.isDraggable ?? layout.state.grid.state.isDraggable,
              rowConfig.isResizable ?? layout.state.grid.state.isResizable
            ),
          })
      );
    } else {
      rows = [new RowItem({ layout: layout.clone() })];
    }

    // Ensure we always get at least one row
    if (rows.length === 0) {
      rows = [new RowItem()];
    }

    return new RowsLayoutManager({ rows });
  }

  public duplicateTitles(): Set<string | undefined> {
    const titleCounts = new Map<string | undefined, number>();
    const duplicateTitles = new Set<string | undefined>();

    this.state.rows.forEach((row) => {
      const title = sceneGraph.interpolate(row, row.state.title);
      const count = (titleCounts.get(title) ?? 0) + 1;
      titleCounts.set(title, count);
      if (count > 1 && title) {
        duplicateTitles.add(title);
      }
    });

    return duplicateTitles;
  }

  public collapseAllRows() {
    this.state.rows.forEach((row) => {
      if (!row.getCollapsedState()) {
        row.setCollapsedState(true);
      }
      row.state.repeatedRows?.forEach((repeatedRow) => {
        if (!repeatedRow.getCollapsedState()) {
          repeatedRow.setCollapsedState(true);
        }
      });
    });
  }

  public expandAllRows() {
    this.state.rows.forEach((row) => {
      if (row.getCollapsedState()) {
        row.setCollapsedState(false);
      }
      row.state.repeatedRows?.forEach((repeatedRow) => {
        if (repeatedRow.getCollapsedState()) {
          repeatedRow.setCollapsedState(false);
        }
      });
    });
  }
}
