import { capitalize } from 'lodash';

import { USER_DEFINED_TREE_NAME } from '@grafana/alerting';
import { AlertState } from '@grafana/data';
import {
  type Alert,
  type AlertingRule,
  type CloudRuleIdentifier,
  type CombinedRule,
  type CombinedRuleGroup,
  type CombinedRuleWithLocation,
  type EditableRuleIdentifier,
  type GrafanaRuleIdentifier,
  type PrometheusRuleIdentifier,
  type RecordingRule,
  type Rule,
  type RuleGroupIdentifier,
  type RuleIdentifier,
  type RuleWithLocation,
} from 'app/types/unified-alerting';
import {
  type Annotations,
  GrafanaAlertState,
  type GrafanaAlertStateWithReason,
  type GrafanaAlertingRuleDefinition,
  type GrafanaNotificationSettings,
  type GrafanaPromAlertingRuleDTO,
  type GrafanaPromRecordingRuleDTO,
  type GrafanaRecordingRuleDefinition,
  type Labels,
  type PostableRuleDTO,
  PromAlertingRuleState,
  type PromRuleDTO,
  type PromRuleGroupDTO,
  PromRuleType,
  type RulerAlertingRuleDTO,
  type RulerCloudRuleDTO,
  type RulerGrafanaRuleDTO,
  type RulerRecordingRuleDTO,
  type RulerRuleDTO,
  type RulerRuleGroupDTO,
  mapStateWithReasonToBaseState,
} from 'app/types/unified-alerting-dto';

import { type CombinedRuleNamespace } from '../../../../types/unified-alerting';
import { type State } from '../components/StateTag';
import { RuleHealth, RuleSource } from '../search/rulesSearchParser';
import { RuleFormType, type RuleFormValues } from '../types/rule-form';

import { GRAFANA_FOLDER_LABEL, MATCHER_ALERT_RULE_UID, RULER_NOT_SUPPORTED_MSG } from './constants';
import { getRulesSourceName, isGrafanaRulesSource } from './datasource';
import { GRAFANA_ORIGIN_LABEL } from './labels';
import { type AsyncRequestState } from './redux';
import { formatPrometheusDuration, safeParsePrometheusDuration } from './time';

/* Grafana managed rules */

function isGrafanaRulerRule(rule?: RulerRuleDTO | PostableRuleDTO): rule is RulerGrafanaRuleDTO {
  return typeof rule === 'object' && 'grafana_alert' in rule;
}

function isGrafanaAlertingRule(rule?: RulerRuleDTO): rule is RulerGrafanaRuleDTO<GrafanaAlertingRuleDefinition> {
  return isGrafanaRulerRule(rule) && !isGrafanaRecordingRule(rule);
}

function isGrafanaRecordingRule(rule?: RulerRuleDTO): rule is RulerGrafanaRuleDTO<GrafanaRecordingRuleDefinition> {
  return isGrafanaRulerRule(rule) && 'record' in rule.grafana_alert;
}

export function ruleUsesDefaultPolicy(notificationSettings?: GrafanaNotificationSettings): boolean {
  if (notificationSettings?.receiver) {
    return false;
  }
  if (notificationSettings?.policy && notificationSettings.policy !== USER_DEFINED_TREE_NAME) {
    return false;
  }
  return true;
}

export function isPausedRule(rule: RulerGrafanaRuleDTO) {
  return Boolean(rule.grafana_alert.is_paused);
}

/* Data source managed rules */

function isAlertingRulerRule(rule?: RulerRuleDTO): rule is RulerAlertingRuleDTO {
  return typeof rule === 'object' && 'alert' in rule;
}

function isCloudRulerRule(rule?: RulerRuleDTO | PostableRuleDTO): rule is RulerCloudRuleDTO {
  return typeof rule === 'object' && !isGrafanaRulerRule(rule);
}

function isCloudRecordingRulerRule(rule?: RulerRuleDTO): rule is RulerRecordingRuleDTO {
  return typeof rule === 'object' && 'record' in rule;
}
export function isCloudRulerGroup(
  rulerRuleGroup: RulerRuleGroupDTO
): rulerRuleGroup is RulerRuleGroupDTO<RulerCloudRuleDTO> {
  return rulerRuleGroup.rules.every((r) => isCloudRulerRule(r));
}

/* Prometheus rules */

function isAlertingRule(rule?: Rule): rule is AlertingRule {
  return typeof rule === 'object' && rule.type === PromRuleType.Alerting;
}

function isRecordingRule(rule?: Rule): rule is RecordingRule {
  return typeof rule === 'object' && rule.type === PromRuleType.Recording;
}

function isGrafanaPromAlertingRule(rule?: Rule): rule is GrafanaPromAlertingRuleDTO {
  return isAlertingRule(rule) && 'folderUid' in rule && 'uid' in rule;
}

function isGrafanaPromRecordingRule(rule?: Rule): rule is GrafanaPromRecordingRuleDTO {
  return isRecordingRule(rule) && 'folderUid' in rule && 'uid' in rule;
}

export const rulerRuleType = {
  grafana: {
    rule: isGrafanaRulerRule,
    alertingRule: isGrafanaAlertingRule,
    recordingRule: isGrafanaRecordingRule,
  },
  dataSource: {
    rule: isCloudRulerRule,
    alertingRule: isAlertingRulerRule,
    recordingRule: isCloudRecordingRulerRule,
  },
  any: {
    rule: (rule?: RulerRuleDTO) => isCloudRulerRule(rule) || isGrafanaRulerRule(rule),
    recordingRule: (rule?: RulerRuleDTO) => isCloudRecordingRulerRule(rule) || isGrafanaRecordingRule(rule),
    alertingRule: (rule?: RulerRuleDTO) => isAlertingRulerRule(rule) || isGrafanaAlertingRule(rule),
  },
};

export const prometheusRuleType = {
  rule: (rule?: Rule) => isAlertingRule(rule) || isRecordingRule(rule),
  alertingRule: isAlertingRule,
  recordingRule: isRecordingRule,
  grafana: {
    rule: (rule?: Rule) => isGrafanaPromAlertingRule(rule) || isGrafanaPromRecordingRule(rule),
    alertingRule: isGrafanaPromAlertingRule,
    recordingRule: isGrafanaPromRecordingRule,
  },
};

export function alertInstanceKey(alert: Alert): string {
  return JSON.stringify(alert.labels);
}

export function isRulerNotSupportedResponse(resp: AsyncRequestState<any>) {
  return resp.error && resp.error?.message?.includes(RULER_NOT_SUPPORTED_MSG);
}

export function isGrafanaRuleIdentifier(identifier: RuleIdentifier): identifier is GrafanaRuleIdentifier {
  return 'uid' in identifier;
}

export function isCloudRuleIdentifier(identifier: RuleIdentifier): identifier is CloudRuleIdentifier {
  return 'rulerRuleHash' in identifier;
}

export function isPromRuleType(ruleType: string): ruleType is PromRuleType {
  return Object.values<string>(PromRuleType).includes(ruleType);
}

export function isPrometheusRuleIdentifier(identifier: RuleIdentifier): identifier is PrometheusRuleIdentifier {
  return 'ruleHash' in identifier;
}

export function isEditableRuleIdentifier(identifier: RuleIdentifier): identifier is EditableRuleIdentifier {
  return isGrafanaRuleIdentifier(identifier) || isCloudRuleIdentifier(identifier);
}

export function isProvisionedRule(rulerRule: RulerRuleDTO): boolean {
  return isGrafanaRulerRule(rulerRule) && Boolean(rulerRule.grafana_alert.provenance);
}

export function isProvisionedPromRule(promRule: PromRuleDTO): boolean {
  return prometheusRuleType.grafana.rule(promRule) && Boolean(promRule.provenance);
}

export function isProvisionedRuleGroup(group: RulerRuleGroupDTO): boolean {
  return group.rules.some(isProvisionedRule);
}

export function getRuleHealth(health: string): RuleHealth | undefined {
  switch (health) {
    case 'ok':
      return RuleHealth.Ok;
    case 'nodata':
      return RuleHealth.NoData;
    case 'error':
    case 'err': // Prometheus-compat data sources
      return RuleHealth.Error;
    case 'unknown':
      return RuleHealth.Unknown;
    default:
      return undefined;
  }
}

export function getRuleSource(source: string): RuleSource | undefined {
  if (source === 'grafana') {
    return RuleSource.Grafana;
  }
  if (source === 'datasource') {
    return RuleSource.DataSource;
  }
  return undefined;
}

export function getPendingPeriod(rule: CombinedRule): string | undefined {
  if (rulerRuleType.any.recordingRule(rule.rulerRule)) {
    return undefined;
  }

  // We prefer the for duration from the ruler rule because it is formatted as a duration string
  // Prometheus duration is in seconds and we need to format it as a duration string
  // Additionally, due to eventual consistency of the Prometheus endpoint the ruler data might be newer
  if (isAlertingRulerRule(rule.rulerRule)) {
    return rule.rulerRule.for;
  }

  if (isAlertingRule(rule.promRule)) {
    const durationInMilliseconds = (rule.promRule.duration ?? 0) * 1000;
    return formatPrometheusDuration(durationInMilliseconds);
  }

  return undefined;
}

export function getAnnotations(rule?: AlertingRule): Annotations {
  return rule?.annotations ?? {};
}

export interface RulePluginOrigin {
  pluginId: string;
}

export function getRulePluginOrigin(rule?: Rule | PromRuleDTO | RulerRuleDTO): RulePluginOrigin | undefined {
  if (!rule) {
    return undefined;
  }

  const origin = rule.labels?.[GRAFANA_ORIGIN_LABEL];
  if (!origin) {
    return undefined;
  }

  const match = origin.match(/^plugin\/(?<pluginId>.+)$/);
  if (!match?.groups) {
    return undefined;
  }

  const pluginId = match.groups.pluginId;

  return { pluginId };
}

export function isPluginProvidedRule(rule?: Rule | PromRuleDTO | RulerRuleDTO): boolean {
  return Boolean(getRulePluginOrigin(rule));
}

type GroupReadOnlyReason = 'plugin' | 'provisioned' | 'federated';

export type GroupReadOnlyStatus = { readOnly: true; reason: GroupReadOnlyReason } | { readOnly: false };

/**
 * Reports whether a Ruler-API rule group cannot be edited from Grafana, and why.
 * Precedence: plugin > provisioned > federated.
 */
export function getRulerGroupReadOnlyStatus(
  group: Pick<RulerRuleGroupDTO, 'rules' | 'source_tenants'>
): GroupReadOnlyStatus {
  if (group.rules.some((rule) => isPluginProvidedRule(rule))) {
    return { readOnly: true, reason: 'plugin' };
  }
  if (group.rules.some(isProvisionedRule)) {
    return { readOnly: true, reason: 'provisioned' };
  }
  if (Array.isArray(group.source_tenants)) {
    return { readOnly: true, reason: 'federated' };
  }
  return { readOnly: false };
}

/**
 * Reports whether a Prom-API rule group cannot be edited from Grafana, and why.
 * Precedence: plugin > provisioned. The Prom API doesn't carry `source_tenants`,
 * so federated detection lives only on the Ruler-API counterpart.
 */
export function getPromGroupReadOnlyStatus(group: Pick<PromRuleGroupDTO, 'rules'>): GroupReadOnlyStatus {
  if (group.rules.some((rule) => isPluginProvidedRule(rule))) {
    return { readOnly: true, reason: 'plugin' };
  }
  if (group.rules.some(isProvisionedPromRule)) {
    return { readOnly: true, reason: 'provisioned' };
  }
  return { readOnly: false };
}

export function alertStateToReadable(state: PromAlertingRuleState | GrafanaAlertStateWithReason | AlertState): string {
  if (state === PromAlertingRuleState.Inactive) {
    return 'Normal';
  }
  return capitalize(state);
}

export const getAlertingRule = (rule: CombinedRuleWithLocation) =>
  isAlertingRule(rule.promRule) ? rule.promRule : null;

export const flattenCombinedRules = (rules: CombinedRuleNamespace[]) => {
  return rules.reduce<CombinedRuleWithLocation[]>((acc, { rulesSource, name: namespaceName, groups }) => {
    groups.forEach(({ name: groupName, rules }) => {
      rules.forEach((rule) => {
        if (rule.promRule && isAlertingRule(rule.promRule)) {
          acc.push({
            dataSourceName: getRulesSourceName(rulesSource),
            namespaceName,
            groupName,
            ...rule,
            namespace: { ...rule.namespace, uid: rule.promRule.folderUid },
          });
        }
      });
    });
    return acc;
  }, []);
};

export function alertStateToState(state: PromAlertingRuleState | GrafanaAlertStateWithReason | AlertState): State {
  let key: PromAlertingRuleState | GrafanaAlertState | AlertState;
  if (Object.values(AlertState).includes(state as AlertState)) {
    key = state as AlertState;
  } else {
    key = mapStateWithReasonToBaseState(state as GrafanaAlertStateWithReason | PromAlertingRuleState);
  }

  return alertStateToStateMap[key];
}

const alertStateToStateMap: Record<PromAlertingRuleState | GrafanaAlertState | AlertState, State> = {
  [PromAlertingRuleState.Inactive]: 'good',
  [PromAlertingRuleState.Firing]: 'bad',
  [PromAlertingRuleState.Pending]: 'warning',
  [PromAlertingRuleState.Recovering]: 'warning',
  [GrafanaAlertState.Alerting]: 'bad',
  [GrafanaAlertState.Error]: 'bad',
  [GrafanaAlertState.NoData]: 'info',
  [GrafanaAlertState.Normal]: 'good',
  [GrafanaAlertState.Pending]: 'warning',
  [GrafanaAlertState.Recovering]: 'warning',
  [AlertState.NoData]: 'info',
  [AlertState.Paused]: 'warning',
  [AlertState.Alerting]: 'bad',
  [AlertState.OK]: 'good',
  // AlertState.Pending is not included because the 'pending' value is already covered by `PromAlertingRuleState.Pending`
  // [AlertState.Pending]: 'warning',
  // same for AlertState.Recovering
  // [AlertState.Recovering]: 'warning',
  [AlertState.Unknown]: 'info',
};

export function getFirstActiveAt(promRule?: AlertingRule) {
  if (!promRule?.alerts) {
    return null;
  }
  return promRule.alerts.reduce<Date | null>((prev, alert) => {
    const isNotNormal = mapStateWithReasonToBaseState(alert.state) !== GrafanaAlertState.Normal;
    if (alert.activeAt && isNotNormal) {
      const activeAt = new Date(alert.activeAt);
      if (prev === null || prev.getTime() > activeAt.getTime()) {
        return activeAt;
      }
    }
    return prev;
  }, null);
}

/**
 * A rule group is "federated" when it has at least one "source_tenants" entry, federated rule groups will evaluate rules in multiple tenants
 * Non-federated rules do not have this property
 *
 * see https://grafana.com/docs/metrics-enterprise/latest/tenant-management/tenant-federation/#cross-tenant-alerting-and-recording-rule-federation
 */
export function isFederatedRuleGroup(group: CombinedRuleGroup | RulerRuleGroupDTO): boolean {
  return Array.isArray(group.source_tenants);
}

export function getRuleName(rule: RulerRuleDTO): string {
  if (rulerRuleType.grafana.rule(rule)) {
    return rule.grafana_alert.title;
  }

  if (rulerRuleType.dataSource.alertingRule(rule)) {
    return rule.alert;
  }

  if (rulerRuleType.dataSource.recordingRule(rule)) {
    return rule.record;
  }

  return '';
}

export interface AlertInfo {
  alertName: string;
  forDuration?: string;
  evaluationsToFire: number | null;
}

export const getAlertInfo = (alert: RulerRuleDTO, currentEvaluation: string): AlertInfo => {
  const emptyAlert: AlertInfo = {
    alertName: '',
    forDuration: '0s',
    evaluationsToFire: 0,
  };
  if (isGrafanaRulerRule(alert)) {
    return {
      alertName: alert.grafana_alert.title,
      forDuration: alert.for,
      evaluationsToFire: alert.for ? getNumberEvaluationsToStartAlerting(alert.for, currentEvaluation) : null,
    };
  }
  if (isAlertingRulerRule(alert)) {
    return {
      alertName: alert.alert,
      forDuration: alert.for ?? '0s',
      evaluationsToFire: getNumberEvaluationsToStartAlerting(alert.for ?? '0s', currentEvaluation),
    };
  }
  return emptyAlert;
};

export const getNumberEvaluationsToStartAlerting = (forDuration: string, currentEvaluation: string) => {
  const evalNumberMs = safeParsePrometheusDuration(currentEvaluation);
  const forNumber = safeParsePrometheusDuration(forDuration);
  if (forNumber === 0 && evalNumberMs !== 0) {
    return 1;
  }
  if (evalNumberMs === 0) {
    return 0;
  } else {
    const evaluationsBeforeCeil = forNumber / evalNumberMs;
    return evaluationsBeforeCeil < 1 ? 0 : Math.ceil(forNumber / evalNumberMs) + 1;
  }
};

/*
 * Extracts a rule group identifier from a CombinedRule
 */
export function getRuleGroupLocationFromCombinedRule(rule: CombinedRule): RuleGroupIdentifier {
  const ruleSourceName = isGrafanaRulesSource(rule.namespace.rulesSource)
    ? rule.namespace.rulesSource
    : rule.namespace.rulesSource.name;

  const namespace = isGrafanaRulerRule(rule.rulerRule)
    ? rule.rulerRule.grafana_alert.namespace_uid
    : rule.namespace.name;

  return {
    dataSourceName: ruleSourceName,
    namespaceName: namespace,
    groupName: rule.group.name,
  };
}

/**
 * Extracts a rule group identifier from a RuleWithLocation
 */
export function getRuleGroupLocationFromRuleWithLocation(rule: RuleWithLocation): RuleGroupIdentifier {
  const dataSourceName = rule.ruleSourceName;

  const namespaceName = isGrafanaRulerRule(rule.rule) ? rule.rule.grafana_alert.namespace_uid : rule.namespace;
  const groupName = rule.group.name;

  return {
    dataSourceName,
    namespaceName,
    groupName,
  };
}

export function getRuleGroupLocationFromFormValues(values: RuleFormValues): RuleGroupIdentifier {
  const dataSourceName = values.dataSourceName;
  const namespaceName = values.folder?.uid ?? values.namespace;
  const groupName = values.group;

  if (!dataSourceName) {
    throw new Error('no datasource name in form values');
  }

  return {
    dataSourceName,
    namespaceName,
    groupName,
  };
}

export function isGrafanaAlertingRuleByType(type?: RuleFormType) {
  return type === RuleFormType.grafana;
}

export function isGrafanaRecordingRuleByType(type?: RuleFormType) {
  return type === RuleFormType.grafanaRecording;
}

export function isCloudAlertingRuleByType(type?: RuleFormType) {
  return type === RuleFormType.cloudAlerting;
}

export function isCloudRecordingRuleByType(type?: RuleFormType) {
  return type === RuleFormType.cloudRecording;
}

export function isGrafanaManagedRuleByType(type?: RuleFormType) {
  return isGrafanaAlertingRuleByType(type) || isGrafanaRecordingRuleByType(type);
}

export function isRecordingRuleByType(type?: RuleFormType) {
  return isGrafanaRecordingRuleByType(type) || isCloudRecordingRuleByType(type);
}

export function isDataSourceManagedRuleByType(type?: RuleFormType) {
  return isCloudAlertingRuleByType(type) || isCloudRecordingRuleByType(type);
}

/*
 * Grab the UID from either a rulerRule definition or a Prometheus rule definition, only Grafana-managed rules will have a UID.
 */
export function getRuleUID(rule?: RulerRuleDTO | Rule) {
  if (!rule) {
    return;
  }

  let ruleUid: string | undefined;

  if ('grafana_alert' in rule && rulerRuleType.grafana.rule(rule)) {
    ruleUid = rule.grafana_alert.uid;
  } else if ('uid' in rule && prometheusRuleType.grafana.rule(rule)) {
    ruleUid = rule.uid;
  }

  return ruleUid;
}

export const NO_GROUP_PREFIX = 'no_group_for_rule_';
export const isUngroupedRuleGroup = (group: string): boolean => group.startsWith(NO_GROUP_PREFIX);

/**
 * Returns the full effective label set for a Grafana-managed alert rule, merging
 * user-defined labels with the system labels Grafana attaches to every fired
 * alert instance at evaluation time (alertname, grafana_folder, __alert_rule_uid__).
 *
 * User-defined labels take precedence over system labels if they conflict,
 * matching Go backend behavior (GetRuleExtraLabels).
 *
 * For non-Grafana rules sources, only the user-defined labels are returned since
 * system labels are not predictable from the rule definition alone.
 *
 * Note: labels derived from metric query results (e.g. `instance`, `job`) are
 * unknowable at rule-definition time and cannot be included.
 */
export function getEffectiveRuleLabels(rule: CombinedRule): Labels {
  if (!isGrafanaRulesSource(rule.namespace.rulesSource)) {
    return rule.labels;
  }

  const systemLabels: Labels = {
    alertname: rule.name,
    [GRAFANA_FOLDER_LABEL]: rule.namespace.name,
  };

  if (rule.uid) {
    systemLabels[MATCHER_ALERT_RULE_UID] = rule.uid;
  }

  // System labels are the base; user-defined labels override them, matching Go backend behavior.
  return { ...systemLabels, ...rule.labels };
}
