/**
 * This hook will combine data from both the Alertmanager config
 * and (if available) it will also fetch the status from the Grafana Managed status endpoint
 */

import { merge, set } from 'lodash';
import { useMemo } from 'react';

import { receiversApi } from 'app/features/alerting/unified/api/receiversK8sApi';
import { useOnCallIntegration } from 'app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration';
import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver } from 'app/features/alerting/unified/openapi/receiversApi.gen';
import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks';
import { cloudNotifierTypes } from 'app/features/alerting/unified/utils/cloud-alertmanager-notifier-types';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { isK8sEntityProvisioned, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils';
import {
  GrafanaManagedContactPoint,
  GrafanaManagedReceiverConfig,
  Receiver,
} from 'app/plugins/datasource/alertmanager/types';

import { getAPINamespace } from '../../../../../api/utils';
import { alertmanagerApi } from '../../api/alertmanagerApi';
import { onCallApi } from '../../api/onCallApi';
import { useAsync } from '../../hooks/useAsync';
import { usePluginBridge } from '../../hooks/usePluginBridge';
import { useProduceNewAlertmanagerConfiguration } from '../../hooks/useProduceNewAlertmanagerConfig';
import { addReceiverAction, deleteReceiverAction, updateReceiverAction } from '../../reducers/alertmanager/receivers';
import { getIrmIfPresentOrOnCallPluginId } from '../../utils/config';

import { enhanceContactPointsWithMetadata } from './utils';

const RECEIVER_STATUS_POLLING_INTERVAL = 10 * 1000; // 10 seconds

/**
 * This hook will combine data from several endpoints;
 * 1. the alertmanager config endpoint where the definition of the receivers are
 * 2. (if available) the alertmanager receiver status endpoint, currently Grafana Managed only
 * 3. (if available) additional metadata about Grafana Managed contact points
 * 4. (if available) the OnCall plugin metadata
 */

const {
  useGetAlertmanagerConfigurationQuery,
  useGetContactPointsListQuery,
  useGetContactPointsStatusQuery,
  useGrafanaNotifiersQuery,
  useLazyGetAlertmanagerConfigurationQuery,
} = alertmanagerApi;
const { useGrafanaOnCallIntegrationsQuery } = onCallApi;
const {
  useListNamespacedReceiverQuery,
  useReadNamespacedReceiverQuery,
  useDeleteNamespacedReceiverMutation,
  useCreateNamespacedReceiverMutation,
  useReplaceNamespacedReceiverMutation,
} = receiversApi;

const defaultOptions = {
  refetchOnFocus: true,
  refetchOnReconnect: true,
};

/**
 * Check if OnCall is installed, and fetch the list of integrations if so.
 *
 * Otherwise, returns no data
 */
const useOnCallIntegrations = ({ skip }: Skippable = {}) => {
  const { installed, loading } = usePluginBridge(getIrmIfPresentOrOnCallPluginId());
  const oncallIntegrationsResponse = useGrafanaOnCallIntegrationsQuery(undefined, { skip: skip || !installed });

  return useMemo(() => {
    if (installed) {
      return oncallIntegrationsResponse;
    }
    return {
      isLoading: loading,
      data: undefined,
    };
  }, [installed, loading, oncallIntegrationsResponse]);
};

type K8sReceiver = ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver;

const parseK8sReceiver = (item: K8sReceiver): GrafanaManagedContactPoint => {
  return {
    id: item.metadata.name || item.metadata.uid || item.spec.title,
    name: item.spec.title,
    provisioned: isK8sEntityProvisioned(item),
    grafana_managed_receiver_configs: item.spec.integrations,
    metadata: item.metadata,
  };
};

const useK8sContactPoints = (...[hookParams, queryOptions]: Parameters<typeof useListNamespacedReceiverQuery>) => {
  return useListNamespacedReceiverQuery(hookParams, {
    ...queryOptions,
    selectFromResult: (result) => {
      const data = result.data?.items.map((item) => parseK8sReceiver(item));

      return {
        ...result,
        // K8S API will return 403 error for no-permissions case, so its cleaner to fallback to empty array
        data: result.error ? [] : data,
        currentData: data,
      };
    },
  });
};

/**
 * Fetch contact points for Grafana Alertmanager, either from the k8s API,
 * or the `/notifications/receivers` endpoint
 */
const useFetchGrafanaContactPoints = ({ skip }: Skippable = {}) => {
  const namespace = getAPINamespace();
  const useK8sApi = shouldUseK8sApi(GRAFANA_RULES_SOURCE_NAME);

  const grafanaResponse = useGetContactPointsListQuery(undefined, {
    skip: skip || useK8sApi,
    selectFromResult: (result) => {
      const data = result.data?.map((item) => ({
        ...item,
        provisioned: item.grafana_managed_receiver_configs?.some((item) => item.provenance),
      }));
      return {
        ...result,
        data,
        currentData: data,
      };
    },
  });
  const k8sResponse = useK8sContactPoints({ namespace }, { skip: skip || !useK8sApi });

  return useK8sApi ? k8sResponse : grafanaResponse;
};

type GrafanaFetchOptions = {
  /**
   * Should we fetch and include status information about each contact point?
   */
  fetchStatuses?: boolean;
  /**
   * Should we fetch and include the number of notification policies that reference each contact point?
   */
  fetchPolicies?: boolean;
};

/**
 * Fetch list of contact points from separate endpoint (i.e. not the Alertmanager config) and combine with
 * OnCall integrations and any additional metadata from list of notifiers
 * (e.g. hydrate with additional names/descriptions)
 */
export const useGrafanaContactPoints = ({
  fetchStatuses,
  fetchPolicies,
  skip,
}: GrafanaFetchOptions & Skippable = {}) => {
  const potentiallySkip = { skip };
  const onCallResponse = useOnCallIntegrations(potentiallySkip);
  const alertNotifiers = useGrafanaNotifiersQuery(undefined, potentiallySkip);
  const contactPointsListResponse = useFetchGrafanaContactPoints(potentiallySkip);

  const contactPointsStatusResponse = useGetContactPointsStatusQuery(undefined, {
    ...defaultOptions,
    pollingInterval: RECEIVER_STATUS_POLLING_INTERVAL,
    skip: skip || !fetchStatuses,
  });
  const alertmanagerConfigResponse = useGetAlertmanagerConfigurationQuery(GRAFANA_RULES_SOURCE_NAME, {
    skip: skip || !fetchPolicies,
  });

  return useMemo(() => {
    const isLoading = onCallResponse.isLoading || alertNotifiers.isLoading || contactPointsListResponse.isLoading;

    if (isLoading) {
      return {
        ...contactPointsListResponse,
        // If we're inside this block, it means that at least one of the endpoints we care about is still loading,
        // but the contactPointsListResponse may have in fact finished.
        // If we were to use _that_ loading state, it might be inaccurate elsewhere when consuming this hook,
        // so we explicitly say "yes, this is definitely still loading"
        isLoading: true,
        contactPoints: [],
      };
    }

    const enhanced = enhanceContactPointsWithMetadata({
      status: contactPointsStatusResponse.data,
      notifiers: alertNotifiers.data,
      onCallIntegrations: onCallResponse?.data,
      contactPoints: contactPointsListResponse.data || [],
      alertmanagerConfiguration: alertmanagerConfigResponse.data,
    });

    return {
      ...contactPointsListResponse,
      contactPoints: enhanced,
    };
  }, [
    alertNotifiers,
    alertmanagerConfigResponse,
    contactPointsListResponse,
    contactPointsStatusResponse,
    onCallResponse,
  ]);
};

/**
 * Fetch single contact point via the alertmanager config
 */
const useGetAlertmanagerContactPoint = (
  { alertmanager, name }: BaseAlertmanagerArgs & { name: string },
  queryOptions?: Parameters<typeof useGetAlertmanagerConfigurationQuery>[1]
) => {
  return useGetAlertmanagerConfigurationQuery(alertmanager, {
    ...queryOptions,
    selectFromResult: (result) => {
      const matchedContactPoint = result.data?.alertmanager_config.receivers?.find(
        (receiver) => receiver.name === name
      );
      return {
        ...result,
        data: matchedContactPoint,
        currentData: matchedContactPoint,
      };
    },
  });
};

/**
 * Fetch single contact point via the k8s API, or the alertmanager config
 */
const useGetGrafanaContactPoint = (
  { name }: { name: string },
  queryOptions?: Parameters<typeof useReadNamespacedReceiverQuery>[1]
) => {
  const namespace = getAPINamespace();
  const useK8sApi = shouldUseK8sApi(GRAFANA_RULES_SOURCE_NAME);

  const k8sResponse = useReadNamespacedReceiverQuery(
    { namespace, name },
    {
      ...queryOptions,
      selectFromResult: (result) => {
        const data = result.data ? parseK8sReceiver(result.data) : undefined;
        return {
          ...result,
          data,
          currentData: data,
        };
      },
      skip: queryOptions?.skip || !useK8sApi,
    }
  );

  const grafanaResponse = useGetAlertmanagerContactPoint(
    { alertmanager: GRAFANA_RULES_SOURCE_NAME, name },
    { skip: queryOptions?.skip || useK8sApi }
  );

  return useK8sApi ? k8sResponse : grafanaResponse;
};

export const useGetContactPoint = ({ alertmanager, name }: { alertmanager: string; name: string }) => {
  const isGrafana = alertmanager === GRAFANA_RULES_SOURCE_NAME;

  const grafanaResponse = useGetGrafanaContactPoint({ name }, { skip: !isGrafana });
  const alertmanagerResponse = useGetAlertmanagerContactPoint({ alertmanager, name }, { skip: isGrafana });

  return isGrafana ? grafanaResponse : alertmanagerResponse;
};

export function useContactPointsWithStatus({
  alertmanager,
  fetchStatuses,
  fetchPolicies,
  skip,
}: GrafanaFetchOptions & BaseAlertmanagerArgs & Skippable) {
  const isGrafanaAlertmanager = alertmanager === GRAFANA_RULES_SOURCE_NAME;
  const grafanaResponse = useGrafanaContactPoints({
    skip: skip || !isGrafanaAlertmanager,
    fetchStatuses,
    fetchPolicies,
  });

  const alertmanagerConfigResponse = useGetAlertmanagerConfigurationQuery(alertmanager, {
    ...defaultOptions,
    selectFromResult: (result) => ({
      ...result,
      contactPoints: result.data
        ? enhanceContactPointsWithMetadata({
            notifiers: cloudNotifierTypes,
            contactPoints: result.data.alertmanager_config.receivers ?? [],
            alertmanagerConfiguration: result.data,
          })
        : [],
    }),
    skip: skip || isGrafanaAlertmanager,
  });

  return isGrafanaAlertmanager ? grafanaResponse : alertmanagerConfigResponse;
}

type DeleteContactPointArgs = { name: string; resourceVersion?: string };
export function useDeleteContactPoint({ alertmanager }: BaseAlertmanagerArgs) {
  const useK8sApi = shouldUseK8sApi(alertmanager);

  const [produceNewAlertmanagerConfiguration] = useProduceNewAlertmanagerConfiguration();
  const [deleteReceiver] = useDeleteNamespacedReceiverMutation();

  const deleteFromK8sAPI = useAsync(async ({ name, resourceVersion }: DeleteContactPointArgs) => {
    const namespace = getAPINamespace();
    await deleteReceiver({
      name,
      namespace,
      ioK8SApimachineryPkgApisMetaV1DeleteOptions: { preconditions: { resourceVersion } },
    }).unwrap();
  });

  const deleteFromAlertmanagerConfiguration = useAsync(async ({ name }: DeleteContactPointArgs) => {
    const action = deleteReceiverAction(name);
    return produceNewAlertmanagerConfiguration(action);
  });

  return useK8sApi ? deleteFromK8sAPI : deleteFromAlertmanagerConfiguration;
}

/**
 * Turns a Grafana Managed receiver config into a format that can be sent to the k8s API
 *
 * When updating secure settings, we need to send a value of `true` for any secure setting that we want to keep the same.
 *
 * Any other setting that has a value in `secureSettings` will correspond to a new value for that setting -
 * so we should not tell the API that we want to preserve it. Those values will instead be sent within `settings`
 */
const mapIntegrationSettingsForK8s = (integration: GrafanaManagedReceiverConfig): GrafanaManagedReceiverConfig => {
  const { secureSettings, settings, ...restOfIntegration } = integration;
  const secureFields = Object.entries(secureSettings || {}).reduce((acc, [key, value]) => {
    // If a secure field has no (changed) value, then we tell the backend to persist it
    if (value === undefined) {
      return {
        ...acc,
        [key]: true,
      };
    }
    return acc;
  }, {});

  const mappedSecureSettings = Object.entries(secureSettings || {}).reduce((acc, [key, value]) => {
    // If the value is an empty string/falsy value, then we need to omit it from the payload
    // so the backend knows to remove it
    if (!value) {
      return acc;
    }

    // Otherwise, we send the value of the secure field
    return set(acc, key, value);
  }, {});

  // Merge settings properly with lodash so we don't lose any information from nested keys/secure settings
  const mergedSettings = merge({}, settings, mappedSecureSettings);

  return {
    ...restOfIntegration,
    secureFields,
    settings: mergedSettings,
  };
};
const grafanaContactPointToK8sReceiver = (
  contactPoint: GrafanaManagedContactPoint,
  id?: string,
  resourceVersion?: string
): K8sReceiver => {
  return {
    metadata: {
      ...(id && { name: id }),
      resourceVersion,
    },
    spec: {
      title: contactPoint.name,
      integrations: (contactPoint.grafana_managed_receiver_configs || []).map(mapIntegrationSettingsForK8s),
    },
  };
};

type ContactPointOperationArgs = {
  contactPoint: Receiver;
};

type CreateContactPointArgs = ContactPointOperationArgs;

export const useCreateContactPoint = ({ alertmanager }: BaseAlertmanagerArgs) => {
  const isGrafanaAlertmanager = alertmanager === GRAFANA_RULES_SOURCE_NAME;
  const useK8sApi = shouldUseK8sApi(alertmanager);

  const { createOnCallIntegrations } = useOnCallIntegration();
  const [createGrafanaContactPoint] = useCreateNamespacedReceiverMutation();
  const [produceNewAlertmanagerConfiguration] = useProduceNewAlertmanagerConfiguration();

  const updateK8sAPI = useAsync(async ({ contactPoint }: CreateContactPointArgs) => {
    const contactPointWithMaybeOnCall = isGrafanaAlertmanager
      ? await createOnCallIntegrations(contactPoint)
      : contactPoint;

    const namespace = getAPINamespace();
    const contactPointToUse = grafanaContactPointToK8sReceiver(contactPointWithMaybeOnCall);

    return createGrafanaContactPoint({
      namespace,
      comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver: contactPointToUse,
    }).unwrap();
  });

  const updateAlertmanagerConfiguration = useAsync(async ({ contactPoint }: CreateContactPointArgs) => {
    const contactPointWithMaybeOnCall = isGrafanaAlertmanager
      ? await createOnCallIntegrations(contactPoint)
      : contactPoint;

    const action = addReceiverAction(contactPointWithMaybeOnCall);
    return produceNewAlertmanagerConfiguration(action);
  });

  return useK8sApi ? updateK8sAPI : updateAlertmanagerConfiguration;
};

type UpdateContactPointArgsK8s = ContactPointOperationArgs & {
  /** ID of existing contact point to update - used when updating via k8s API */
  id: string;
  resourceVersion?: string;
};
type UpdateContactPointArgsConfig = ContactPointOperationArgs & {
  /** Name of the existing contact point - used for checking uniqueness of name when not using k8s API*/
  originalName: string;
};
type UpdateContactpointArgs = UpdateContactPointArgsK8s | UpdateContactPointArgsConfig;

export const useUpdateContactPoint = ({ alertmanager }: BaseAlertmanagerArgs) => {
  const isGrafanaAlertmanager = alertmanager === GRAFANA_RULES_SOURCE_NAME;
  const useK8sApi = shouldUseK8sApi(alertmanager);

  const { createOnCallIntegrations } = useOnCallIntegration();
  const [replaceGrafanaContactPoint] = useReplaceNamespacedReceiverMutation();
  const [produceNewAlertmanagerConfiguration] = useProduceNewAlertmanagerConfiguration();

  const updateContactPoint = useAsync(async (args: UpdateContactpointArgs) => {
    if ('resourceVersion' in args && useK8sApi) {
      const { contactPoint, id, resourceVersion } = args;

      const receiverWithPotentialOnCall = isGrafanaAlertmanager
        ? await createOnCallIntegrations(contactPoint)
        : contactPoint;

      const namespace = getAPINamespace();
      const contactPointToUse = grafanaContactPointToK8sReceiver(receiverWithPotentialOnCall, id, resourceVersion);

      return replaceGrafanaContactPoint({
        name: id,
        namespace,
        comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver: contactPointToUse,
      }).unwrap();
    } else if ('originalName' in args) {
      const { contactPoint, originalName } = args;
      const receiverWithPotentialOnCall = isGrafanaAlertmanager
        ? await createOnCallIntegrations(contactPoint)
        : contactPoint;

      const action = updateReceiverAction({ name: originalName, receiver: receiverWithPotentialOnCall });
      return produceNewAlertmanagerConfiguration(action);
    }
  });

  return updateContactPoint;
};

export const useValidateContactPoint = ({ alertmanager }: BaseAlertmanagerArgs) => {
  const useK8sApi = shouldUseK8sApi(alertmanager);

  const [getConfig] = useLazyGetAlertmanagerConfigurationQuery();

  // If we're using the kubernetes API, then we let the API response handle the validation instead
  // as we don't expect to be able to fetch the intervals via the AM config
  if (useK8sApi) {
    return () => undefined;
  }

  return async (value: string, existingValue?: string) => {
    // If we've been given an existing value, and the name has not changed,
    // we can skip validation
    // (as we don't want to incorrectly flag the existing name as matching itself)
    if (existingValue && value === existingValue) {
      return;
    }
    return getConfig(alertmanager)
      .unwrap()
      .then((config) => {
        const { alertmanager_config } = config;
        const duplicated = Boolean(alertmanager_config.receivers?.find((contactPoint) => contactPoint.name === value));
        return duplicated ? `Contact point already exists with name "${value}"` : undefined;
      });
  };
};
