import { config } from '@grafana/runtime';
import {
  type AdHocFilterWithLabels as SceneAdHocFilterWithLabels,
  type MultiValueVariable,
  type SceneVariables,
  sceneUtils,
} from '@grafana/scenes';
import {
  type VariableModel,
  VariableRefresh as OldVariableRefresh,
  VariableHide as OldVariableHide,
  VariableSort as OldVariableSort,
} from '@grafana/schema';
import {
  type AdhocVariableKind,
  type ConstantVariableKind,
  type CustomVariableKind,
  type DataQueryKind,
  type DatasourceVariableKind,
  type IntervalVariableKind,
  type QueryVariableKind,
  type TextVariableKind,
  type GroupByVariableKind,
  defaultVariableHide,
  type VariableOption,
  defaultDataQueryKind,
  type AdHocFilterWithLabels,
  type SwitchVariableKind,
  defaultIntervalVariableSpec,
} from '@grafana/schema/apis/dashboard.grafana.app/v2';
import { getDefaultDatasource } from 'app/features/dashboard/api/ResponseTransformers';

import { getIntervalsQueryFromNewIntervalModel } from '../utils/utils';

import { type DSReferencesMapping } from './DashboardSceneSerializer';
import { getDataSourceForQuery } from './layoutSerializers/utils';
import { getDataQueryKind, getElementDatasource } from './transformSceneToSaveModelSchemaV2';
import {
  transformVariableRefreshToEnum,
  transformVariableHideToEnum,
  transformSortVariableToEnum,
  LEGACY_STRING_VALUE_KEY,
} from './transformToV2TypesUtils';
/**
 * Converts a SceneVariables object into an array of VariableModel objects.
 * @param set - The SceneVariables object containing the variables to convert.
 * @param keepQueryOptions - (Optional) A boolean flag indicating whether to keep the options for query variables.
 *                           This should be set to `false` when variables are saved in the dashboard model,
 *                           but should be set to `true` when variables are used in the templateSrv to keep them in sync.
 *                           If `true`, the options for query variables are kept.
 *  */

export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptions?: boolean) {
  const variables: VariableModel[] = [];

  for (const variable of set.state.variables) {
    // Skipping default variables
    // (Default variables don't get persisted to the JSON schema.)
    if (variable.state.origin !== undefined) {
      continue;
    }

    const commonProperties = {
      name: variable.state.name,
      label: variable.state.label,
      description: variable.state.description ?? undefined,
      skipUrlSync: Boolean(variable.state.skipUrlSync),
      hide: variable.state.hide || OldVariableHide.dontHide,
      type: variable.state.type,
    };

    if (sceneUtils.isQueryVariable(variable)) {
      let options: VariableOption[] = [];
      if (keepQueryOptions) {
        options = variableValueOptionsToVariableOptions(variable.state);
      }
      const datasource = getElementDatasource(set, variable, 'variable');
      const variableObj: VariableModel = {
        ...commonProperties,
        current: {
          // @ts-expect-error
          value: variable.state.value,
          // @ts-expect-error
          text: variable.state.text,
        },
        options,
        query: variable.state.query,
        definition: variable.state.definition,
        sort: variable.state.sort,
        refresh: variable.state.refresh,
        regex: variable.state.regex,
        regexApplyTo: variable.state.regexApplyTo,
        allValue: variable.state.allValue,
        includeAll: variable.state.includeAll,
        multi: variable.state.isMulti,
        ...(variable.state.allowCustomValue !== undefined && { allowCustomValue: variable.state.allowCustomValue }),
        skipUrlSync: variable.state.skipUrlSync,
        staticOptions: variable.state.staticOptions?.map((option) => ({
          text: option.label,
          value: String(option.value),
          ...(option.properties && { properties: option.properties }),
        })),
        staticOptionsOrder: variable.state.staticOptionsOrder,
      };
      // Only add datasource if it exists and is not empty
      if (datasource && Object.keys(datasource).length > 0 && (datasource.uid || datasource.type)) {
        variableObj.datasource = datasource;
      }
      variables.push(variableObj);
    } else if (sceneUtils.isCustomVariable(variable)) {
      let options: VariableOption[] = [];
      if (keepQueryOptions) {
        options = variableValueOptionsToVariableOptions(variable.state);
      }
      const customVariable: VariableModel = {
        ...commonProperties,
        current: {
          // @ts-expect-error
          text: variable.state.value,
          // @ts-expect-error
          value: variable.state.value,
        },
        options,
        query: variable.state.query,
        multi: variable.state.isMulti,
        allValue: variable.state.allValue,
        includeAll: variable.state.includeAll,
        ...(variable.state.allowCustomValue !== undefined && { allowCustomValue: variable.state.allowCustomValue }),
        // Ensure we persist the backend default when not specified to stay aligned with
        // transformSaveModelSchemaV2ToScene which injects 'csv' on load.
        valuesFormat: variable.state.valuesFormat ?? 'csv',
      };
      variables.push(customVariable);
    } else if (sceneUtils.isDataSourceVariable(variable)) {
      variables.push({
        ...commonProperties,
        current: {
          // @ts-expect-error
          value: variable.state.value,
          // @ts-expect-error
          text: variable.state.text,
        },
        options: [],
        regex: variable.state.regex,
        refresh: OldVariableRefresh.onDashboardLoad,
        query: variable.state.pluginId,
        multi: variable.state.isMulti,
        allValue: variable.state.allValue,
        includeAll: variable.state.includeAll,
        ...(variable.state.allowCustomValue !== undefined && { allowCustomValue: variable.state.allowCustomValue }),
      });
    } else if (sceneUtils.isConstantVariable(variable)) {
      variables.push({
        ...commonProperties,
        type: 'constant', // Explicitly set type to constant
        current: {
          // @ts-expect-error
          value: variable.state.value,
          // @ts-expect-error
          text: variable.state.value,
        },
        // @ts-expect-error
        query: variable.state.value,
        hide: OldVariableHide.hideVariable,
      });
    } else if (sceneUtils.isIntervalVariable(variable)) {
      const intervals = getIntervalsQueryFromNewIntervalModel(variable.state.intervals);
      variables.push({
        ...commonProperties,
        current: {
          text: variable.state.value,
          value: variable.state.value,
        },
        query: intervals,
        // V2 schema mandates refresh: "onTimeRangeChanged" for interval variables,
        // which maps to OldVariableRefresh.onTimeRangeChanged (2) in V1
        refresh: variable.state.refresh ?? OldVariableRefresh.onTimeRangeChanged,
        options: variable.state.intervals.map((interval) => ({
          value: interval,
          text: interval,
          selected: interval === variable.state.value,
        })),
        // @ts-expect-error ?? how to fix this without adding the ts-expect-error
        auto: variable.state.autoEnabled,
        auto_min: variable.state.autoMinInterval,
        auto_count: variable.state.autoStepCount,
      });
    } else if (sceneUtils.isTextBoxVariable(variable)) {
      const current = {
        text: variable.state.value,
        value: variable.state.value,
      };

      variables.push({
        ...commonProperties,
        current,
        options: [{ ...current, selected: true }],
        query: variable.state.value,
      });
    } else if (sceneUtils.isGroupByVariable(variable) && config.featureToggles.groupByVariable) {
      // @ts-expect-error
      const defaultVariableOption: VariableOption | undefined = variable.state.defaultValue
        ? {
            value: variable.state.defaultValue.value,
            text: variable.state.defaultValue.text,
          }
        : undefined;

      variables.push({
        ...commonProperties,
        datasource: variable.state.datasource,
        // Only persist the statically defined options
        options: variable.state.defaultOptions?.map((option) => ({
          text: option.text,
          value: String(option.value),
        })),
        current: {
          // @ts-expect-error
          text: variable.state.text,
          // @ts-expect-error
          value: variable.state.value,
        },
        defaultValue: defaultVariableOption,
        ...(variable.state.allowCustomValue !== undefined && { allowCustomValue: variable.state.allowCustomValue }),
      });
    } else if (sceneUtils.isAdHocVariable(variable)) {
      const adhocVariable: VariableModel = {
        ...commonProperties,
        datasource: variable.state.datasource,
        // @ts-expect-error
        baseFilters: variable.state.baseFilters || [],
        filters: [
          ...validateFiltersOrigin(variable.getOriginalFilters()).map(
            ({ key, operator, value, values, keyLabel, valueLabels, origin }) => {
              return { key, origin, value, values, valueLabels, keyLabel, operator };
            }
          ),
          ...validateFiltersOrigin(variable.state.filters),
        ],
        defaultKeys: variable.state.defaultKeys,
        ...(variable.state.allowCustomValue !== undefined && { allowCustomValue: variable.state.allowCustomValue }),
        enableGroupBy: config.featureToggles.dashboardUnifiedDrilldownControls
          ? (variable.state.enableGroupBy ?? false)
          : false,
      };
      variables.push(adhocVariable);
    } else if (sceneUtils.isSwitchVariable(variable)) {
      variables.push({
        ...commonProperties,
        current: {
          value: variable.state.value,
          text: variable.state.value,
        },
        options: [
          {
            value: variable.state.enabledValue,
            text: variable.state.enabledValue,
          },
          {
            value: variable.state.disabledValue,
            text: variable.state.disabledValue,
          },
        ],
      });
    } else if (variable.state.type === 'system') {
      // Not persisted
    } else {
      throw new Error('Unsupported variable type');
    }
  }

  // Remove some defaults
  for (const variable of variables) {
    if (variable.hide === OldVariableHide.dontHide) {
      delete variable.hide;
    }

    if (!variable.skipUrlSync) {
      delete variable.skipUrlSync;
    }

    if (variable.label === '') {
      delete variable.label;
    }

    if (!variable.multi) {
      delete variable.multi;
    }

    if (variable.sort === OldVariableSort.disabled) {
      delete variable.sort;
    }
  }

  return variables;
}

function variableValueOptionsToVariableOptions(varState: MultiValueVariable['state']): VariableOption[] {
  return varState.options.map((o) => ({
    value: String(o.value),
    text: o.label,
    selected: Array.isArray(varState.value) ? varState.value.includes(o.value) : varState.value === o.value,
    ...(o.properties && { properties: o.properties }),
  }));
}

export function sceneVariablesSetToSchemaV2Variables(
  set: SceneVariables,
  keepQueryOptions?: boolean,
  dsReferencesMapping?: DSReferencesMapping
): Array<
  | QueryVariableKind
  | TextVariableKind
  | IntervalVariableKind
  | DatasourceVariableKind
  | CustomVariableKind
  | ConstantVariableKind
  | GroupByVariableKind
  | AdhocVariableKind
  | SwitchVariableKind
> {
  let variables: Array<
    | QueryVariableKind
    | TextVariableKind
    | IntervalVariableKind
    | DatasourceVariableKind
    | CustomVariableKind
    | ConstantVariableKind
    | GroupByVariableKind
    | AdhocVariableKind
    | SwitchVariableKind
  > = [];

  for (const variable of set.state.variables) {
    // Skipping default variables
    // (Default variables don't get persisted to the JSON schema.)
    if (variable.state.origin !== undefined) {
      continue;
    }

    const commonProperties = {
      name: variable.state.name,
      label: variable.state.label,
      description: variable.state.description ?? undefined,
      skipUrlSync: Boolean(variable.state.skipUrlSync),
      hide: transformVariableHideToEnum(variable.state.hide) || defaultVariableHide(),
    };

    // current: VariableOption;
    const currentVariableOption: VariableOption = {
      // @ts-expect-error
      value: variable.state.value,
      // @ts-expect-error
      text: variable.state.text,
    };

    let options: VariableOption[] = [];

    // Query variable
    if (sceneUtils.isQueryVariable(variable)) {
      if (keepQueryOptions) {
        options = variableValueOptionsToVariableOptions(variable.state);
      }
      const query = variable.state.query;
      let dataQuery: DataQueryKind | string;
      const datasource = getElementDatasource(set, variable, 'variable', undefined, dsReferencesMapping);

      if (typeof query !== 'string') {
        dataQuery = {
          kind: 'DataQuery',
          version: defaultDataQueryKind().version,
          group: datasource?.type || getDataQueryKind(query),
          ...(datasource?.uid && {
            datasource: {
              name: datasource.uid,
            },
          }),
          spec: query,
        };
      } else {
        // Only include LEGACY_STRING_VALUE_KEY if query is a non-empty string
        const spec: Record<string, string> = {};
        if (query) {
          spec[LEGACY_STRING_VALUE_KEY] = query;
        }
        dataQuery = {
          kind: 'DataQuery',
          version: defaultDataQueryKind().version,
          group: datasource?.type || getDataQueryKind(query),
          ...(datasource?.uid && {
            datasource: {
              name: datasource.uid,
            },
          }),
          spec,
        };
      }
      const queryVariable: QueryVariableKind = {
        kind: 'QueryVariable',
        spec: {
          ...commonProperties,
          current: currentVariableOption,
          options,
          query: dataQuery,
          definition: variable.state.definition,
          sort: transformSortVariableToEnum(variable.state.sort),
          refresh: transformVariableRefreshToEnum(variable.state.refresh),
          regex: variable.state.regex ?? '',
          regexApplyTo: variable.state.regexApplyTo ?? 'value',
          allValue: variable.state.allValue,
          includeAll: variable.state.includeAll || false,
          multi: variable.state.isMulti || false,
          skipUrlSync: variable.state.skipUrlSync || false,
          allowCustomValue: variable.state.allowCustomValue ?? true,
          staticOptions: variable.state.staticOptions?.map((option) => ({
            text: option.label,
            value: String(option.value),
            ...(option.properties && { properties: option.properties }),
          })),
          staticOptionsOrder: variable.state.staticOptionsOrder,
        },
      };
      variables.push(queryVariable);

      // Custom variable
    } else if (sceneUtils.isCustomVariable(variable)) {
      const customVariable: CustomVariableKind = {
        kind: 'CustomVariable',
        spec: {
          ...commonProperties,
          current: currentVariableOption,
          options: [],
          query: variable.state.query,
          multi: variable.state.isMulti || false,
          allValue: variable.state.allValue,
          includeAll: variable.state.includeAll ?? false,
          allowCustomValue: variable.state.allowCustomValue ?? true,
          valuesFormat: variable.state.valuesFormat ?? 'csv',
        },
      };
      variables.push(customVariable);

      // Datasource variable
    } else if (sceneUtils.isDataSourceVariable(variable)) {
      const datasourceVariable: DatasourceVariableKind = {
        kind: 'DatasourceVariable',
        spec: {
          ...commonProperties,
          current: currentVariableOption,
          options: [],
          regex: variable.state.regex ?? '',
          refresh: 'onDashboardLoad',
          pluginId: variable.state.pluginId ?? getDefaultDatasource().type,
          multi: variable.state.isMulti || false,
          includeAll: variable.state.includeAll || false,
          allowCustomValue: variable.state.allowCustomValue ?? true,
        },
      };

      if (variable.state.allValue !== undefined) {
        datasourceVariable.spec.allValue = variable.state.allValue;
      }

      variables.push(datasourceVariable);

      // Constant variable
    } else if (sceneUtils.isConstantVariable(variable)) {
      const value = variable.state.value ? String(variable.state.value) : '';
      const constantVariable: ConstantVariableKind = {
        kind: 'ConstantVariable',
        spec: {
          ...commonProperties,
          current: {
            text: value,
            value: value,
          },
          query: value,
        },
      };
      variables.push(constantVariable);

      // Interval variable
    } else if (sceneUtils.isIntervalVariable(variable)) {
      const intervals = getIntervalsQueryFromNewIntervalModel(variable.state.intervals);
      const intervalVariable: IntervalVariableKind = {
        kind: 'IntervalVariable',
        spec: {
          ...commonProperties,
          current: {
            ...currentVariableOption,
            // Interval variable doesn't use text state
            text: variable.state.value,
          },
          query: intervals,
          refresh: defaultIntervalVariableSpec().refresh,
          options: variable.state.intervals.map((interval) => ({
            value: interval,
            text: interval,
            selected: interval === variable.state.value,
          })),
          auto: variable.state.autoEnabled ?? defaultIntervalVariableSpec().auto,
          auto_min: variable.state.autoMinInterval ?? defaultIntervalVariableSpec().auto_min,
          auto_count: variable.state.autoStepCount ?? defaultIntervalVariableSpec().auto_count,
        },
      };
      variables.push(intervalVariable);

      // Textbox variable
    } else if (sceneUtils.isTextBoxVariable(variable)) {
      const current = {
        text: variable.state.value ?? '',
        value: variable.state.value ?? '',
      };

      const textBoxVariable: TextVariableKind = {
        kind: 'TextVariable',
        spec: {
          ...commonProperties,
          current,
          query: variable.state.value ?? '',
        },
      };

      variables.push(textBoxVariable);

      // Groupby variable
    } else if (sceneUtils.isGroupByVariable(variable) && config.featureToggles.groupByVariable) {
      options = variableValueOptionsToVariableOptions(variable.state);

      // @ts-expect-error
      const defaultVariableOption: VariableOption | undefined = variable.state.defaultValue
        ? {
            value: variable.state.defaultValue.value,
            text: variable.state.defaultValue.text,
          }
        : undefined;

      const ds = getDataSourceForQuery(
        variable.state.datasource,
        variable.state.datasource?.type || getDefaultDatasource().type!
      );

      const groupVariable: GroupByVariableKind = {
        kind: 'GroupByVariable',
        group: ds.type!,
        datasource: {
          name: ds.uid,
        },
        spec: {
          ...commonProperties,
          // Only persist the statically defined options
          options:
            variable.state.defaultOptions?.map((option) => ({
              text: option.text,
              value: String(option.value),
            })) || [],
          current: currentVariableOption,
          defaultValue: defaultVariableOption,
          multi: variable.state.isMulti || false,
        },
      };
      variables.push(groupVariable);

      // Adhoc variable
    } else if (sceneUtils.isAdHocVariable(variable)) {
      const ds = getDataSourceForQuery(
        variable.state.datasource,
        variable.state.datasource?.type || getDefaultDatasource().type!
      );
      const adhocVariable: AdhocVariableKind = {
        kind: 'AdhocVariable',
        group: ds.type!,
        datasource: {
          name: ds.uid,
        },
        spec: {
          ...commonProperties,
          name: variable.state.name,

          baseFilters: validateFiltersOrigin(variable.state.baseFilters) || [],
          filters: [
            ...validateFiltersOrigin(variable.getOriginalFilters()).map(
              ({ key, operator, value, values, keyLabel, valueLabels, origin }) => {
                return { key, origin, value, values, valueLabels, keyLabel, operator };
              }
            ),
            ...validateFiltersOrigin(variable.state.filters),
          ],
          defaultKeys: variable.state.defaultKeys || [],
          allowCustomValue: variable.state.allowCustomValue ?? true,
          enableGroupBy: config.featureToggles.dashboardUnifiedDrilldownControls
            ? (variable.state.enableGroupBy ?? false)
            : false,
        },
      };
      variables.push(adhocVariable);

      // Switch variable
    } else if (sceneUtils.isSwitchVariable(variable)) {
      const switchVariable: SwitchVariableKind = {
        kind: 'SwitchVariable',
        spec: {
          ...commonProperties,
          current: variable.state.value,
          enabledValue: variable.state.enabledValue,
          disabledValue: variable.state.disabledValue,
        },
      };
      variables.push(switchVariable);
    } else if (variable.state.type === 'system') {
      // Do nothing
    } else {
      throw new Error('Unsupported variable type: ' + variable.state.type);
    }
  }

  return variables;
}

export function validateFiltersOrigin(filters?: SceneAdHocFilterWithLabels[]): AdHocFilterWithLabels[] {
  // Only keep dashboard originated filters in the schema
  return filters?.filter((f): f is AdHocFilterWithLabels => !f.origin || f.origin === 'dashboard') || [];
}
