import {
  Action,
  ActionModel,
  ActionType,
  ActionVariableInput,
  AppEvents,
  DataContextScopedVar,
  DataFrame,
  DataLink,
  Field,
  FieldType,
  getFieldDataContextClone,
  InterpolateFunction,
  InfinityOptions,
  ScopedVars,
  textUtil,
  ValueLinkConfig,
} from '@grafana/data';
import { BackendSrvRequest, config as grafanaConfig, getBackendSrv } from '@grafana/runtime';
import { appEvents } from 'app/core/app_events';

import { HttpRequestMethod } from '../../plugins/panel/canvas/panelcfg.gen';
import { createAbsoluteUrl, RelativeUrl } from '../alerting/unified/utils/url';
import { getTimeSrv } from '../dashboard/services/TimeSrv';
import { getNextRequestId } from '../query/state/PanelQueryRunner';

import { reportActionTrigger } from './analytics';

/** @internal */
export const isInfinityActionWithAuth = (action: Action): boolean => {
  return (grafanaConfig.featureToggles.vizActionsAuth ?? false) && action.type === ActionType.Infinity;
};

/** @internal */
export const genReplaceActionVars = (
  boundReplaceVariables: InterpolateFunction,
  action: Action,
  actionVars?: ActionVariableInput
): InterpolateFunction => {
  return (value, scopedVars, format) => {
    if (action.variables && actionVars) {
      value = value.replace(/\$\w+/g, (matched) => {
        const name = matched.slice(1);

        if (action.variables!.some((action) => action.key === name) && actionVars[name] != null) {
          return actionVars[name];
        }

        return matched;
      });
    }

    return boundReplaceVariables(value, scopedVars, format);
  };
};

/** @internal */
export const getActions = (
  frame: DataFrame,
  field: Field,
  fieldScopedVars: ScopedVars,
  replaceVariables: InterpolateFunction,
  actions: Action[],
  config: ValueLinkConfig,
  visualizationType?: string
): Array<ActionModel<Field>> => {
  if (!actions || actions.length === 0) {
    return [];
  }

  const actionModels = actions
    .filter((action) => {
      return action.type === ActionType.Fetch || isInfinityActionWithAuth(action);
    })
    .map((action: Action) => {
      const dataContext: DataContextScopedVar = getFieldDataContextClone(frame, field, fieldScopedVars);
      const actionScopedVars = {
        ...fieldScopedVars,
        __dataContext: dataContext,
      };

      const boundReplaceVariables: InterpolateFunction = (value, scopedVars, format) => {
        return replaceVariables(value, { ...actionScopedVars, ...scopedVars }, format);
      };

      // We are not displaying reduction result
      if (config.valueRowIndex !== undefined && !isNaN(config.valueRowIndex)) {
        dataContext.value.rowIndex = config.valueRowIndex;
      } else {
        dataContext.value.calculatedValue = config.calculatedValue;
      }

      const actionModel: ActionModel<Field> = {
        title: replaceVariables(action.title, actionScopedVars),
        type: action.type,
        confirmation: (actionVars?: ActionVariableInput) =>
          genReplaceActionVars(
            boundReplaceVariables,
            action,
            actionVars
          )(action.confirmation || `Are you sure you want to ${action.title}?`),
        onClick: (evt: MouseEvent, origin: Field, actionVars?: ActionVariableInput) => {
          if (visualizationType) {
            reportActionTrigger(action.type, action.oneClick ?? false, visualizationType);
          }

          // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
          let request = {} as BackendSrvRequest;
          if (isInfinityActionWithAuth(action)) {
            request = buildActionProxyRequest(action, genReplaceActionVars(boundReplaceVariables, action, actionVars));
          } else if (action.type === ActionType.Fetch) {
            request = buildActionRequest(action, genReplaceActionVars(boundReplaceVariables, action, actionVars));
          }

          try {
            getBackendSrv()
              .fetch(request)
              .subscribe({
                error: (error) => {
                  appEvents.emit(AppEvents.alertError, [
                    'An error has occurred. Check console output for more details.',
                  ]);
                  console.error(error);
                },
                complete: () => {
                  appEvents.emit(AppEvents.alertSuccess, ['API call was successful']);
                },
              });
          } catch (error) {
            appEvents.emit(AppEvents.alertError, ['An error has occurred. Check console output for more details.']);
            console.error(error);
            return;
          }
        },
        oneClick: action.oneClick ?? false,
        style: {
          backgroundColor: action.style?.backgroundColor ?? grafanaConfig.theme2.colors.secondary.main,
        },
        variables: action.variables,
      };

      return actionModel;
    });

  return actionModels.filter((action): action is ActionModel => !!action);
};

/** @internal */
const processActionConfig = (action: Action, replaceVariables: InterpolateFunction) => {
  const config = action[action.type];
  if (!config) {
    throw new Error('Action does not have the correct configuration');
  }

  const url = new URL(getUrl(replaceVariables(config.url)));
  const data = config.method === HttpRequestMethod.GET ? undefined : config.body ? replaceVariables(config.body) : '{}';

  const processedHeaders: Array<[string, string]> = [];
  const processedQueryParams: Array<[string, string]> = [];
  let contentType = 'application/json';

  if (config.headers) {
    config.headers.forEach(([name, value]) => {
      const processedName = replaceVariables(name);
      const processedValue = replaceVariables(value);
      processedHeaders.push([processedName, processedValue]);

      if (processedName.toLowerCase() === 'content-type') {
        contentType = processedValue;
      }
    });
  }

  if (config.queryParams) {
    config.queryParams.forEach(([name, value]) => {
      processedQueryParams.push([replaceVariables(name), replaceVariables(value)]);
    });
  }

  return {
    config,
    url,
    data,
    processedHeaders,
    processedQueryParams,
    contentType,
  };
};

/** @internal */
export const buildActionRequest = (action: Action, replaceVariables: InterpolateFunction) => {
  const { config, url, data, processedHeaders, processedQueryParams } = processActionConfig(action, replaceVariables);

  const requestHeaders: Record<string, string> = {};

  processedHeaders.forEach(([name, value]) => {
    requestHeaders[name] = value;
  });

  processedQueryParams.forEach(([name, value]) => {
    url.searchParams.append(name, value);
  });

  requestHeaders['X-Grafana-Action'] = '1';

  const request: BackendSrvRequest = {
    url: url.toString(),
    method: config.method,
    data,
    headers: requestHeaders,
  };

  return request;
};

/** @internal */
export const getActionsDefaultField = (dataLinks: DataLink[] = [], actions: Action[] = []): Field => {
  return {
    name: 'Default field',
    type: FieldType.string,
    config: { links: dataLinks, actions: actions },
    values: [],
  };
};

/** @internal */
const getUrl = (endpoint: string) => {
  const isRelativeUrl = endpoint.startsWith('/');
  if (isRelativeUrl) {
    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
    const sanitizedRelativeURL = textUtil.sanitizeUrl(endpoint) as RelativeUrl;
    endpoint = createAbsoluteUrl(sanitizedRelativeURL, []);
  }

  return endpoint;
};

/** @internal */
interface KeyValuePair {
  key: string;
  value: string;
}

export const INFINITY_DATASOURCE_TYPE = 'yesoreyeram-infinity-datasource';

/** @internal */
class InfinityRequestBuilder {
  buildRequest(
    proxyConfig: InfinityOptions,
    url: URL,
    data: string | undefined,
    headers: Array<[string, string]>,
    queryParams: Array<[string, string]>,
    contentType: string
  ): BackendSrvRequest {
    const requestId = getNextRequestId();
    const infinityUrl = `api/ds/query?ds_type=${INFINITY_DATASOURCE_TYPE}&requestId=${requestId}`;
    const timeRange = getTimeSrv().timeRange();

    const requestHeaders: KeyValuePair[] = [];
    headers.forEach(([name, value]) => {
      requestHeaders.push({ key: name, value: value });
    });

    // Infinity needs [string, string] to {key: string, value: string}
    const requestQueryParams: KeyValuePair[] = [];
    queryParams.forEach(([name, value]) => {
      requestQueryParams.push({ key: name, value: value });
    });

    const infinityUrlOptions = {
      method: proxyConfig.method,
      data,
      headers: requestHeaders,
      params: requestQueryParams,
      body_type: 'raw',
      body_content_type: contentType,
    };

    return {
      url: infinityUrl,
      method: HttpRequestMethod.POST,
      data: {
        queries: [
          {
            refId: 'A',
            datasource: {
              type: INFINITY_DATASOURCE_TYPE,
              uid: proxyConfig.datasourceUid,
            },
            type: 'json',
            source: 'url',
            format: 'as-is',
            url,
            url_options: infinityUrlOptions,
          },
        ],
        from: timeRange.from.valueOf().toString(),
        to: timeRange.to.valueOf().toString(),
      },
    };
  }
}

/** @internal */
export const buildActionProxyRequest = (action: Action, replaceVariables: InterpolateFunction) => {
  const { config, url, data, processedHeaders, processedQueryParams, contentType } = processActionConfig(
    action,
    replaceVariables
  );

  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
  const infinityConfig = config as InfinityOptions;
  if (!infinityConfig.datasourceUid) {
    throw new Error('Datasource not configured for Infinity action');
  }

  const requestBuilder = new InfinityRequestBuilder();
  return requestBuilder.buildRequest(infinityConfig, url, data, processedHeaders, processedQueryParams, contentType);
};
