import { type FormEvent, useId, useMemo, useRef, useState } from 'react';

import { VariableHide } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { locationService } from '@grafana/runtime';
import {
  LocalValueVariable,
  MultiValueVariable,
  type SceneObject,
  type SceneVariable,
  SceneVariableSet,
  useSceneObjectState,
} from '@grafana/scenes';
import { Input, TextArea, Button, Field, Box, Stack, Alert } from '@grafana/ui';
import { appEvents } from 'app/core/app_events';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
import { ShowConfirmModalEvent } from 'app/types/events';

import { dashboardEditActions } from '../../edit-pane/shared';
import { DashboardScene } from '../../scene/DashboardScene';
import { useEditPaneInputAutoFocus } from '../../scene/layouts-shared/utils';
import { type BulkActionElement } from '../../scene/types/BulkActionElement';
import {
  type EditableDashboardElement,
  type EditableDashboardElementInfo,
  isEditableDashboardElement,
} from '../../scene/types/EditableDashboardElement';
import { VariableDisplaySelect } from '../../settings/variables/components/VariableDisplaySelect';
import { getEditableVariableDefinition, validateVariableName } from '../../settings/variables/utils';
import { DashboardInteractions } from '../../utils/interactions';

import { openChangeVariableTypePane } from './VariableTypeSelectionPane';
import { useVariableSelectionOptionsCategory } from './useVariableSelectionOptionsCategory';

// TODO fix conditional hook usage here...
function useEditPaneOptions(this: VariableEditableElement, isNewElement: boolean): OptionsPaneCategoryDescriptor[] {
  const variable = this.variable;
  const variableOptionsCategoryId = useId();
  const variableNameId = useId();
  const labelId = useId();
  const descriptionId = useId();
  const variableDisplayId = useId();

  // Keep the variable activated while editing it in the side pane. The dashboard controls
  // component may unmount (e.g. when changing display to "hidden"), which would deactivate
  // the variable and destroy all state subscriptions used by the editors below.
  useSceneObjectState(variable, { shouldActivateOrKeepAlive: true });

  if (variable instanceof LocalValueVariable) {
    return useLocalVariableOptions(variable);
  }

  const basicOptions = useMemo(() => {
    return new OptionsPaneCategoryDescriptor({ title: '', id: variableOptionsCategoryId })
      .addItem(
        new OptionsPaneItemDescriptor({
          title: '',
          id: variableNameId,
          skipField: true,
          render: () => <VariableNameInput variable={variable} autoFocus={isNewElement} />,
        })
      )
      .addItem(
        new OptionsPaneItemDescriptor({
          title: t('dashboard.edit-pane.variable.label', 'Label'),
          id: labelId,
          description: t('dashboard.edit-pane.variable.label-description', 'Optional display name'),
          render: () => <VariableLabelInput variable={variable} />,
        })
      )
      .addItem(
        new OptionsPaneItemDescriptor({
          title: t('dashboard.edit-pane.variable.description', 'Description'),
          id: descriptionId,
          render: () => <VariableDescriptionTextArea variable={variable} />,
        })
      )
      .addItem(
        new OptionsPaneItemDescriptor({
          title: '',
          id: variableDisplayId,
          skipField: true,
          render: () => <VariableDisplayInput variable={variable} />,
        })
      );
  }, [variableOptionsCategoryId, variableNameId, labelId, descriptionId, variableDisplayId, variable, isNewElement]);

  const categories = [basicOptions];
  const typeCategory = useVariableTypeCategory(variable);
  categories.push(typeCategory);

  if (variable instanceof MultiValueVariable) {
    categories.push(useVariableSelectionOptionsCategory(variable));
  }

  return categories;
}

export class VariableEditableElement implements EditableDashboardElement, BulkActionElement {
  public readonly isEditableDashboardElement = true;
  public readonly typeName = 'Variable';

  public constructor(public variable: SceneVariable) {}

  public getEditableElementInfo(): EditableDashboardElementInfo {
    if (this.variable instanceof LocalValueVariable) {
      return {
        typeName: t('dashboard.edit-pane.elements.local-variable', 'Local variable'),
        icon: 'dollar-alt',
        instanceName: this.variable.state.name,
        isHidden: true,
      };
    }

    const variableEditorDef = getEditableVariableDefinition(this.variable.state.type);

    return {
      typeName: t('dashboard.edit-pane.elements.variable', '{{type}} variable', { type: variableEditorDef.name }),
      icon: 'dollar-alt',
      instanceName: this.variable.state.name,
      isHidden: this.variable.state.hide === VariableHide.hideVariable,
    };
  }

  public useEditPaneOptions = useEditPaneOptions.bind(this);

  public renderActions() {
    return (
      <Stack grow={1}>
        <ChangeVariableTypeButton variable={this.variable} />
      </Stack>
    );
  }

  public onDuplicate() {
    const set = this.variable.parent!;
    if (!(set instanceof SceneVariableSet)) {
      return;
    }

    dashboardEditActions.addVariable({
      source: set,
      addedObject: this.variable.clone({
        key: undefined,
        name: `${this.variable.state.name}_copy${set.state.variables.length}`,
      }),
    });
    DashboardInteractions.variableActionButtonClicked('duplicate', { type: this.variable.state.type });
  }

  public onConfirmDelete() {
    const name = this.variable.state.name;
    appEvents.publish(
      new ShowConfirmModalEvent({
        title: t('dashboard-scene.variable-editable-element.delete-title', 'Delete variable'),
        text: t('dashboard-scene.variable-editable-element.delete-text', 'Are you sure you want to delete: {{name}}?', {
          name,
        }),
        yesText: t('dashboard-scene.variable-editable-element.delete-confirm', 'Delete variable'),
        onConfirm: () => {
          this.onDelete();
        },
      })
    );
  }

  public onDelete() {
    const set = this.variable.parent!;
    if (!(set instanceof SceneVariableSet)) {
      return;
    }

    DashboardInteractions.variableActionButtonClicked('delete', { type: this.variable.state.type });

    dashboardEditActions.removeVariable({
      source: set,
      removedObject: this.variable,
    });
  }

  public onChangeName(name: string) {
    this.variable.setState({ name });

    const result = validateVariableName(this.variable, name);
    if (result.errorMessage) {
      return result;
    }

    return;
  }

  public scrollIntoView() {
    let current: SceneObject | undefined = this.variable.parent;
    while (current) {
      if (isEditableDashboardElement(current) && current.scrollIntoView) {
        current.scrollIntoView();
        return;
      }
      current = current.parent;
    }
  }
}

interface VariableInputProps {
  variable: SceneVariable;
  id?: string;
}

function ChangeVariableTypeButton({ variable }: { variable: SceneVariable }) {
  if (!(variable.parent instanceof SceneVariableSet)) {
    return null;
  }

  return (
    <Button
      size="sm"
      fill="text"
      onClick={() => openChangeVariableTypePane(variable)}
      data-testid={selectors.components.PanelEditor.ElementEditPane.changeVariableType}
      aria-label={t('dashboard.edit-pane.variable.change-type-aria-label', 'Change variable type')}
    >
      <Trans i18nKey="dashboard.edit-pane.variable.change-type">Change</Trans>
    </Button>
  );
}

function VariableNameInput({ variable, autoFocus }: { variable: SceneVariable; autoFocus: boolean }) {
  const { name } = variable.useState();
  const ref = useEditPaneInputAutoFocus({ autoFocus });
  const [nameError, setNameError] = useState<string>();
  const [nameWarning, setNameWarning] = useState<string>();
  const id = useId();

  const onChange = (e: FormEvent<HTMLInputElement>) => {
    const result = validateVariableName(variable, e.currentTarget.value);
    if (result.errorMessage !== nameError) {
      setNameError(result.errorMessage);
    }
    if (result.warningMessage !== nameWarning) {
      setNameWarning(result.warningMessage);
    }

    variable.setState({ name: e.currentTarget.value });
  };

  const oldName = useRef(name);

  return (
    <>
      <Field
        label={t('dashboard.edit-pane.variable.name', 'Name')}
        invalid={!!nameError}
        error={nameError}
        noMargin={false}
      >
        <Input
          id={id}
          ref={ref}
          value={name}
          onFocus={() => {
            oldName.current = name;
          }}
          onChange={onChange}
          onBlur={(e) => {
            const labelUnchanged = oldName.current === name;
            const shouldSkip = labelUnchanged;

            if (nameError) {
              setNameError(undefined);
              variable.setState({ name: oldName.current });
              return;
            }

            if (shouldSkip) {
              return;
            }

            dashboardEditActions.changeVariableName({
              source: variable,
              oldValue: oldName.current,
              newValue: name,
            });
          }}
          data-testid={selectors.components.PanelEditor.ElementEditPane.variableNameInput}
          required
        />
      </Field>
      {/* Show warning message if the variable name is already used in the dashboard
        Unfortunately <Field> component only supports error messages, not warning messages.
      */}
      {nameWarning && <Alert title={nameWarning} severity="warning" topSpacing={0} bottomSpacing={1} />}
    </>
  );
}

function VariableLabelInput({ variable, id }: VariableInputProps) {
  const { label } = variable.useState();
  const oldLabel = useRef(label ?? '');

  return (
    <Input
      id={id}
      value={label}
      onFocus={() => {
        oldLabel.current = label ?? '';
      }}
      onChange={(e) => variable.setState({ label: e.currentTarget.value })}
      onBlur={(e) => {
        const labelUnchanged = oldLabel.current === e.currentTarget.value;
        const shouldSkip = labelUnchanged;

        if (shouldSkip) {
          return;
        }

        dashboardEditActions.changeVariableLabel({
          source: variable,
          oldValue: oldLabel.current,
          newValue: e.currentTarget.value,
        });
      }}
      data-testid={selectors.components.PanelEditor.ElementEditPane.variableLabelInput}
    />
  );
}

function VariableDescriptionTextArea({ variable, id }: VariableInputProps) {
  const { description } = variable.useState();
  const oldDescription = useRef(description ?? '');

  return (
    <TextArea
      id={id}
      value={description ?? ''}
      placeholder={t('dashboard.edit-pane.variable.description-placeholder', 'Descriptive text')}
      onFocus={() => {
        oldDescription.current = description ?? '';
      }}
      onChange={(e) => variable.setState({ description: e.currentTarget.value })}
      onBlur={(e) => {
        const labelUnchanged = oldDescription.current === e.currentTarget.value;
        const shouldSkip = labelUnchanged;

        if (shouldSkip) {
          return;
        }

        dashboardEditActions.changeVariableDescription({
          source: variable,
          oldValue: oldDescription.current,
          newValue: e.currentTarget.value,
        });
      }}
    />
  );
}

function VariableDisplayInput({ variable }: VariableInputProps) {
  const { hide: display = VariableHide.dontHide } = variable.useState();

  const onChange = (option: VariableHide) => {
    dashboardEditActions.changeVariableHideValue({
      source: variable,
      oldValue: display,
      newValue: option,
    });
  };

  return (
    <VariableDisplaySelect
      display={display}
      type={variable.state.type}
      hideControlsMenuOption={shouldHideControlsMenuOption(variable)}
      onChange={onChange}
    />
  );
}

export function shouldHideControlsMenuOption(variable: SceneVariable): boolean {
  const set = variable.parent;
  const dashboardVariable = set instanceof SceneVariableSet && set.parent instanceof DashboardScene;
  return !dashboardVariable;
}

function useVariableTypeCategory(variable: SceneVariable) {
  const oldVariableId = useId();
  return useMemo(() => {
    const variableEditorDef = getEditableVariableDefinition(variable.state.type);
    const categoryName = t('dashboard.edit-pane.variable.type-category', '{{type}} options', {
      type: variableEditorDef.name,
    });

    const category = new OptionsPaneCategoryDescriptor({
      title: categoryName,
      id: 'variable-type',
      isOpenDefault: true,
    });

    if (variableEditorDef.getOptions) {
      const options = variableEditorDef.getOptions(variable);
      options.forEach((option) => category.addItem(option));
    } else {
      category.addItem(
        new OptionsPaneItemDescriptor({
          title: '',
          id: oldVariableId,
          skipField: true,
          render: () => <OpenOldVariableEditButton variable={variable} />,
        })
      );
    }

    return category;
  }, [oldVariableId, variable]);
}

function OpenOldVariableEditButton({ variable }: VariableInputProps) {
  const onOpenVariableEdior = () => {
    const set = variable.parent!;
    if (!(set instanceof SceneVariableSet)) {
      return;
    }

    const variableIndex = set.state.variables.indexOf(variable);
    locationService.partial({ editview: 'variables', editIndex: variableIndex });
  };

  return (
    <Box display={'flex'} direction={'column'} paddingBottom={1}>
      <Button
        tooltip={t(
          'dashboard.edit-pane.variable.open-editor-tooltip',
          'For more variable options open variable editor'
        )}
        onClick={onOpenVariableEdior}
        size="sm"
        fullWidth
      >
        <Trans i18nKey="dashboard.edit-pane.variable.open-editor">Open variable editor</Trans>
      </Button>
    </Box>
  );
}

function useLocalVariableOptions(variable: LocalValueVariable): OptionsPaneCategoryDescriptor[] {
  const localVariableOptionsCategoryId = useId();
  const localVariableId = useId();
  return useMemo(() => {
    const category = new OptionsPaneCategoryDescriptor({
      title: '',
      id: localVariableOptionsCategoryId,
    });

    category.addItem(
      new OptionsPaneItemDescriptor({
        title: '',
        id: localVariableId,
        skipField: true,
        render: () => {
          return (
            <Box paddingBottom={1}>
              <Stack>
                <Stack>
                  <span>${variable.state.name}</span>
                  <span>=</span>
                  <span>{variable.getValueText()}</span>
                </Stack>
              </Stack>
            </Box>
          );
        },
      })
    );

    return [category];
  }, [localVariableId, localVariableOptionsCategoryId, variable]);
}
