import {
  getTimeZone,
  InterpolateFunction,
  LinkModel,
  locationUtil,
  PanelMenuItem,
  PanelPlugin,
  PluginExtensionLink,
  PluginExtensionPanelContext,
  PluginExtensionPoints,
  PluginExtensionTypes,
  urlUtil,
} from '@grafana/data';
import { t } from '@grafana/i18n';
import { config, locationService } from '@grafana/runtime';
import { LocalValueVariable, sceneGraph, VizPanel, VizPanelMenu } from '@grafana/scenes';
import { DataQuery, OptionsWithLegend } from '@grafana/schema';
import { appEvents } from 'app/core/app_events';
import { createErrorNotification } from 'app/core/copy/appNotification';
import { notifyApp } from 'app/core/reducers/appNotification';
import { contextSrv } from 'app/core/services/context_srv';
import { getMessageFromError } from 'app/core/utils/errors';
import { getCreateAlertInMenuAvailability } from 'app/features/alerting/unified/utils/access-control';
import { scenesPanelToRuleFormValues } from 'app/features/alerting/unified/utils/rule-form';
import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils';
import { InspectTab } from 'app/features/inspector/types';
import { getScenePanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
import { createPluginExtensionsGetter } from 'app/features/plugins/extensions/getPluginExtensions';
import { getPluginExtensionRegistries } from 'app/features/plugins/extensions/registry/setup';
import { PluginExtensionRegistries } from 'app/features/plugins/extensions/registry/types';
import { GetPluginExtensions } from 'app/features/plugins/extensions/types';
import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils';
import { dispatch } from 'app/store/store';
import { AccessControlAction } from 'app/types/accessControl';
import { ShowConfirmModalEvent } from 'app/types/events';

import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer';
import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
import { isRepeatCloneOrChildOf } from '../utils/clone';
import { DashboardInteractions } from '../utils/interactions';
import { getEditPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders';
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils';

import { DashboardScene } from './DashboardScene';
import { VizPanelLinks, VizPanelLinksMenu } from './PanelLinks';
import { UnlinkLibraryPanelModal } from './UnlinkLibraryPanelModal';
import { PanelTimeRangeDrawer } from './panel-timerange/PanelTimeRangeDrawer';

let getPluginExtensions: GetPluginExtensions;

function setupGetPluginExtensions(registries: PluginExtensionRegistries) {
  if (getPluginExtensions) {
    return getPluginExtensions;
  }

  getPluginExtensions = createPluginExtensionsGetter(registries);

  return getPluginExtensions;
}

// Define the category for metrics drilldown links
const METRICS_DRILLDOWN_CATEGORY = 'metrics-drilldown';

/**
 * Behavior is called when VizPanelMenu is activated (ie when it's opened).
 */
export function panelMenuBehavior(menu: VizPanelMenu) {
  const asyncFunc = async () => {
    // hm.. add another generic param to SceneObject to specify parent type?
    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
    const panel = menu.parent as VizPanel;
    const plugin = panel.getPlugin();

    const items: PanelMenuItem[] = [];
    const moreSubMenu: PanelMenuItem[] = [];
    const dashboard = getDashboardSceneFor(panel);
    const { isEmbedded } = dashboard.state.meta;
    const exploreMenuItem = await getExploreMenuItem(panel);
    const isReadOnlyRepeat = isRepeatCloneOrChildOf(panel);

    // For embedded dashboards we only have explore action for now
    if (isEmbedded) {
      if (exploreMenuItem) {
        menu.setState({ items: [exploreMenuItem] });
      }
      return;
    }

    const isEditingPanel = Boolean(dashboard.state.editPanel);
    if (!isEditingPanel) {
      items.push({
        text: t('panel.header-menu.view', `View`),
        iconClassName: 'eye',
        shortcut: 'v',
        href: locationUtil.getUrlForPartial(locationService.getLocation(), {
          viewPanel: panel.getPathId(),
          editPanel: undefined,
        }),
        onClick: () => {
          DashboardInteractions.panelActionClicked('view', getPanelIdForVizPanel(panel), 'panel');
        },
      });
    }

    if (dashboard.canEditDashboard() && dashboard.state.editable && !isReadOnlyRepeat && !isEditingPanel) {
      // We could check isEditing here but I kind of think this should always be in the menu,
      // and going into panel edit should make the dashboard go into edit mode is it's not already
      items.push({
        text: t('panel.header-menu.edit', `Edit`),
        iconClassName: 'edit',
        shortcut: 'e',
        href: getEditPanelUrl(getPanelIdForVizPanel(panel)),
        onClick: () => {
          DashboardInteractions.panelActionClicked('edit', getPanelIdForVizPanel(panel), 'panel');
        },
      });
    }

    const subMenu: PanelMenuItem[] = [];
    subMenu.push({
      text: t('share-panel.menu.share-link-title', 'Share link'),
      iconClassName: 'link',
      shortcut: 'p u',
      onClick: () => {
        DashboardInteractions.sharingCategoryClicked({
          item: shareDashboardType.link,
          shareResource: getTrackingSource(panel?.getRef()),
        });

        const drawer = new ShareDrawer({
          shareView: shareDashboardType.link,
          panelRef: panel.getRef(),
        });

        dashboard.showModal(drawer);
      },
    });
    subMenu.push({
      text: t('share-panel.menu.share-embed-title', 'Share embed'),
      iconClassName: 'arrow',
      shortcut: 'p e',
      onClick: () => {
        DashboardInteractions.sharingCategoryClicked({
          item: shareDashboardType.embed,
          shareResource: getTrackingSource(panel.getRef()),
        });

        const drawer = new ShareDrawer({
          shareView: shareDashboardType.embed,
          panelRef: panel.getRef(),
        });

        dashboard.showModal(drawer);
      },
    });

    if (
      contextSrv.isSignedIn &&
      config.snapshotEnabled &&
      contextSrv.hasPermission(AccessControlAction.SnapshotsCreate)
    ) {
      subMenu.push({
        text: t('share-panel.menu.share-snapshot-title', 'Share snapshot'),
        iconClassName: 'camera',
        shortcut: 'p s',
        onClick: () => {
          DashboardInteractions.sharingCategoryClicked({
            item: shareDashboardType.snapshot,
            shareResource: getTrackingSource(panel.getRef()),
          });

          const drawer = new ShareDrawer({
            shareView: shareDashboardType.snapshot,
            panelRef: panel.getRef(),
          });

          dashboard.showModal(drawer);
        },
      });
    }

    items.push({
      type: 'submenu',
      text: t('panel.header-menu.share', 'Share'),
      iconClassName: 'share-alt',
      subMenu,
      onClick: (e) => {
        e.preventDefault();
      },
    });

    if (dashboard.state.isEditing && !isReadOnlyRepeat && !isEditingPanel) {
      moreSubMenu.push({
        text: t('panel.header-menu.duplicate', `Duplicate`),
        iconClassName: 'file-copy-alt',
        onClick: () => {
          DashboardInteractions.panelActionClicked('duplicate', getPanelIdForVizPanel(panel), 'panel');
          dashboard.duplicatePanel(panel);
        },
        shortcut: 'p d',
      });
    }

    if (!isEditingPanel) {
      moreSubMenu.push({
        text: t('panel.header-menu.copy', `Copy`),
        iconClassName: 'copy',
        onClick: () => {
          DashboardInteractions.panelActionClicked('copy', getPanelIdForVizPanel(panel), 'panel');
          dashboard.copyPanel(panel);
        },
      });
    }

    if (dashboard.state.isEditing && !isReadOnlyRepeat && !isEditingPanel) {
      if (isLibraryPanel(panel)) {
        moreSubMenu.push({
          text: t('panel.header-menu.unlink-library-panel', `Unlink library panel`),
          iconClassName: 'link-broken',
          onClick: () => {
            dashboard.showModal(
              new UnlinkLibraryPanelModal({
                panelRef: panel.getRef(),
              })
            );
          },
        });

        moreSubMenu.push({
          text: t('panel.header-menu.replace-library-panel', `Replace library panel`),
          iconClassName: 'library-panel',
          onClick: () => {
            dashboard.onShowAddLibraryPanelDrawer(panel.getRef());
          },
        });
      } else {
        moreSubMenu.push({
          text: t('share-panel.menu.new-library-panel-title', 'New library panel'),
          iconClassName: 'plus-square',
          onClick: () => {
            const drawer = new ShareDrawer({
              shareView: shareDashboardType.libraryPanel,
              panelRef: panel.getRef(),
            });

            dashboard.showModal(drawer);
          },
        });
      }
    }

    const isCreateAlertMenuOptionAvailable = getCreateAlertInMenuAvailability();

    if (isCreateAlertMenuOptionAvailable) {
      moreSubMenu.push({
        text: t('panel.header-menu.new-alert-rule', `New alert rule`),
        iconClassName: 'bell',
        onClick: (e) => onCreateAlert(panel),
      });
    }

    if (hasLegendOptions(panel.state.options) && !isEditingPanel) {
      moreSubMenu.push({
        text: panel.state.options.legend.showLegend
          ? t('panel.header-menu.hide-legend', 'Hide legend')
          : t('panel.header-menu.show-legend', 'Show legend'),
        iconClassName: panel.state.options.legend.showLegend ? 'legend-hide' : 'legend-show',
        onClick: (e) => {
          e.preventDefault();
          toggleVizPanelLegend(panel);
        },
        shortcut: 'p l',
      });
    }

    if (dashboard.canEditDashboard() && plugin && !plugin.meta.skipDataQuery && !isReadOnlyRepeat) {
      moreSubMenu.push({
        text: t('panel.header-menu.get-help', 'Get help'),
        iconClassName: 'question-circle',
        onClick: (e: React.MouseEvent) => {
          e.preventDefault();
          dashboard.showModal(new PanelInspectDrawer({ panelRef: panel.getRef(), currentTab: InspectTab.Help }));
        },
      });
    }

    if (exploreMenuItem) {
      items.push(exploreMenuItem);
    }

    items.push(getInspectMenuItem(plugin, panel, dashboard));

    if (config.featureToggles.panelTimeSettings) {
      items.push({
        text: t('panel.header-menu.time-settings', 'Time settings'),
        iconClassName: 'clock-nine',
        onClick: (e) => {
          e.preventDefault();
          dashboard.showModal(new PanelTimeRangeDrawer({ panelRef: panel.getRef() }));
        },
      });
    }

    const registries = await getPluginExtensionRegistries();
    setupGetPluginExtensions(registries);

    const { extensions } = getPluginExtensions({
      extensionPointId: PluginExtensionPoints.DashboardPanelMenu,
      context: createExtensionContext(panel, dashboard),
      limitPerPlugin: 3,
    });

    if (extensions.length > 0 && !dashboard.state.isEditing) {
      const linkExtensions = extensions.filter((extension) => extension.type === PluginExtensionTypes.link);

      // Separate metrics drilldown links from other links
      const [metricsDrilldownLinks, otherLinks] = linkExtensions.reduce<[PluginExtensionLink[], PluginExtensionLink[]]>(
        ([metricsDrilldownLinks, otherLinks], link) => {
          if (link.category === METRICS_DRILLDOWN_CATEGORY) {
            metricsDrilldownLinks.push(link);
          } else {
            otherLinks.push(link);
          }
          return [metricsDrilldownLinks, otherLinks];
        },
        [[], []]
      );

      // Add specific "Metrics drilldown" menu
      if (metricsDrilldownLinks.length > 0) {
        items.push({
          text: t('dashboard-scene.panel-menu-behavior.async-func.text.metrics-drilldown', 'Metrics drilldown'),
          iconClassName: 'code-branch',
          type: 'submenu',
          subMenu: createExtensionSubMenu(metricsDrilldownLinks),
        });
      }

      // Add generic "Extensions" menu for other links
      if (otherLinks.length > 0) {
        items.push({
          text: t('dashboard-scene.panel-menu-behavior.async-func.text.extensions', 'Extensions'),
          iconClassName: 'plug',
          type: 'submenu',
          subMenu: createExtensionSubMenu(otherLinks),
        });
      }
    }

    if (panel.state.pluginId === 'timeseries' && config.featureToggles.panelStyleActions && dashboard.state.isEditing) {
      const stylesSubMenu: PanelMenuItem[] = [];

      stylesSubMenu.push({
        text: t('panel.header-menu.copy-styles', `Copy styles`),
        iconClassName: 'copy',
        onClick: () => {
          DashboardInteractions.panelStylesMenuClicked(
            'copy',
            panel.state.pluginId,
            getPanelIdForVizPanel(panel) ?? -1
          );
          dashboard.copyPanelStyles(panel);
        },
      });

      if (DashboardScene.hasPanelStylesToPaste('timeseries')) {
        stylesSubMenu.push({
          text: t('panel.header-menu.paste-styles', `Paste styles`),
          iconClassName: 'clipboard-alt',
          onClick: () => {
            DashboardInteractions.panelStylesMenuClicked(
              'paste',
              panel.state.pluginId,
              getPanelIdForVizPanel(panel) ?? -1
            );
            dashboard.pastePanelStyles(panel);
          },
        });
      }

      items.push({
        type: 'submenu',
        text: t('panel.header-menu.styles', `Styles`),
        iconClassName: 'palette',
        subMenu: stylesSubMenu,
        onClick: (e) => {
          e.preventDefault();
        },
      });
    }

    if (moreSubMenu.length) {
      items.push({
        type: 'submenu',
        text: t('panel.header-menu.more', `More...`),
        iconClassName: 'cube',
        subMenu: moreSubMenu,
        onClick: (e) => {
          e.preventDefault();
        },
      });
    }

    if (dashboard.state.isEditing && !isReadOnlyRepeat && !isEditingPanel) {
      items.push({
        text: '',
        type: 'divider',
      });

      items.push({
        text: t('panel.header-menu.remove', `Remove`),
        iconClassName: 'trash-alt',
        onClick: () => {
          DashboardInteractions.panelActionClicked('delete', getPanelIdForVizPanel(panel), 'panel');
          onRemovePanel(dashboard, panel);
        },
        shortcut: 'p r',
      });
    }

    menu.setState({ items });
  };

  asyncFunc();
}

async function getExploreMenuItem(panel: VizPanel): Promise<PanelMenuItem | undefined> {
  const exploreUrl = await tryGetExploreUrlForPanel(panel);
  if (!exploreUrl) {
    return undefined;
  }

  return {
    text: t('panel.header-menu.explore', `Explore`),
    iconClassName: 'compass',
    shortcut: 'p x',
    href: exploreUrl,
  };
}

function getInspectMenuItem(
  plugin: PanelPlugin | undefined,
  panel: VizPanel,
  dashboard: DashboardScene
): PanelMenuItem {
  const inspectSubMenu: PanelMenuItem[] = [];

  if (plugin && !plugin.meta.skipDataQuery) {
    inspectSubMenu.push({
      text: t('panel.header-menu.inspect-data', `Data`),
      onClick: (e) => {
        e.preventDefault();
        dashboard.showModal(new PanelInspectDrawer({ panelRef: panel.getRef(), currentTab: InspectTab.Data }));
      },
    });

    if (dashboard instanceof DashboardScene && dashboard.state.meta.canEdit) {
      inspectSubMenu.push({
        text: t('panel.header-menu.query', `Query`),
        onClick: (e) => {
          e.preventDefault();
          dashboard.showModal(new PanelInspectDrawer({ panelRef: panel.getRef(), currentTab: InspectTab.Query }));
        },
      });
    }
  }

  inspectSubMenu.push({
    text: t('panel.header-menu.inspect-json', `Panel JSON`),
    onClick: (e) => {
      e.preventDefault();
      dashboard.showModal(new PanelInspectDrawer({ panelRef: panel.getRef(), currentTab: InspectTab.JSON }));
    },
  });

  return {
    text: t('panel.header-menu.inspect', `Inspect`),
    iconClassName: 'info-circle',
    shortcut: 'i',
    onClick: (e) => {
      if (!e.isDefaultPrevented()) {
        dashboard.showModal(new PanelInspectDrawer({ panelRef: panel.getRef(), currentTab: InspectTab.Data }));
      }
    },
    subMenu: inspectSubMenu.length > 0 ? inspectSubMenu : undefined,
  };
}

/**
 * Behavior is called when VizPanelLinksMenu is activated (when it's opened).
 */
export function panelLinksBehavior(panelLinksMenu: VizPanelLinksMenu) {
  if (!(panelLinksMenu.parent instanceof VizPanelLinks)) {
    throw new Error('parent of VizPanelLinksMenu must be VizPanelLinks');
  }
  const panel = panelLinksMenu.parent.parent;

  if (!(panel instanceof VizPanel)) {
    throw new Error('parent of VizPanelLinks must be VizPanel');
  }

  panelLinksMenu.setState({ links: getPanelLinks(panel) });
}

export function getPanelLinks(panel: VizPanel) {
  const interpolate: InterpolateFunction = (v, scopedVars) => {
    return sceneGraph.interpolate(panel, v, scopedVars);
  };

  const linkSupplier = getScenePanelLinksSupplier(panel, interpolate);

  if (!linkSupplier) {
    return [];
  }

  const panelLinks = linkSupplier.getLinks(interpolate);

  return panelLinks.map((panelLink) => {
    const updatedLink: LinkModel<VizPanel> = {
      ...panelLink,
      onClick: (e, origin) => {
        DashboardInteractions.panelLinkClicked({ has_multiple_links: panelLinks.length > 1 });
        panelLink.onClick?.(e, origin);
      },
    };
    return updatedLink;
  });
}

function createExtensionContext(panel: VizPanel, dashboard: DashboardScene): PluginExtensionPanelContext {
  const timeRange = sceneGraph.getTimeRange(panel);
  let queryRunner = getQueryRunnerFor(panel);
  const targets: DataQuery[] = queryRunner?.state.queries as DataQuery[];
  const id = getPanelIdForVizPanel(panel);

  let scopedVars = {};

  // Handle panel repeats scenario
  if (panel.state.$variables) {
    panel.state.$variables.state.variables.forEach((variable) => {
      if (variable instanceof LocalValueVariable) {
        scopedVars = {
          ...scopedVars,
          [variable.state.name]: { value: variable.getValue(), text: variable.getValueText() },
        };
      }
    });
  }

  return {
    id,
    pluginId: panel.state.pluginId,
    title: panel.state.title,
    timeRange: timeRange.state.value.raw,
    timeZone: getTimeZone({
      timeZone: timeRange.getTimeZone(),
    }),
    dashboard: {
      uid: dashboard.state.uid!,
      title: dashboard.state.title,
      tags: dashboard.state.tags || [],
    },
    targets,
    scopedVars,
    data: queryRunner?.state.data,
  };
}

export function onRemovePanel(dashboard: DashboardScene, panel: VizPanel) {
  appEvents.publish(
    new ShowConfirmModalEvent({
      title: t('dashboard-scene.on-remove-panel.title.remove-panel', 'Remove panel'),
      text: t('dashboard-scene.on-remove-panel.text.remove-panel', 'Are you sure you want to remove this panel?'),
      icon: 'trash-alt',
      yesText: 'Remove',
      onConfirm: () => dashboard.removePanel(panel),
    })
  );
}

const onCreateAlert = async (panel: VizPanel) => {
  try {
    const formValues = await scenesPanelToRuleFormValues(panel);
    const ruleFormUrl = urlUtil.renderUrl('/alerting/new', {
      defaults: JSON.stringify(formValues),
      returnTo: window.location.pathname + window.location.search,
    });
    locationService.push(ruleFormUrl);
  } catch (err) {
    const message = `Error getting rule values from the panel: ${getMessageFromError(err)}`;
    dispatch(notifyApp(createErrorNotification(message)));
    return;
  }
};

export function toggleVizPanelLegend(vizPanel: VizPanel): void {
  const options = vizPanel.state.options;
  if (hasLegendOptions(options) && typeof options.legend.showLegend === 'boolean') {
    vizPanel.onOptionsChange({
      legend: {
        showLegend: options.legend.showLegend ? false : true,
      },
    });
  }
}

function hasLegendOptions(optionsWithLegend: unknown): optionsWithLegend is OptionsWithLegend {
  return optionsWithLegend != null && typeof optionsWithLegend === 'object' && 'legend' in optionsWithLegend;
}
