import { CombinedRule, Rule, RuleIdentifier, RuleWithLocation } from 'app/types/unified-alerting';
import { Annotations, Labels, RulerRuleDTO } from 'app/types/unified-alerting-dto';
import { GRAFANA_RULES_SOURCE_NAME } from './datasource';
import {
  isAlertingRule,
  isAlertingRulerRule,
  isCloudRuleIdentifier,
  isGrafanaRuleIdentifier,
  isGrafanaRulerRule,
  isPrometheusRuleIdentifier,
  isRecordingRule,
  isRecordingRulerRule,
} from './rules';

export function fromRulerRule(
  ruleSourceName: string,
  namespace: string,
  groupName: string,
  rule: RulerRuleDTO
): RuleIdentifier {
  if (isGrafanaRulerRule(rule)) {
    return { uid: rule.grafana_alert.uid! };
  }
  return {
    ruleSourceName,
    namespace,
    groupName,
    rulerRuleHash: hashRulerRule(rule),
  };
}

export function fromRule(ruleSourceName: string, namespace: string, groupName: string, rule: Rule): RuleIdentifier {
  return {
    ruleSourceName,
    namespace,
    groupName,
    ruleHash: hashRule(rule),
  };
}

export function fromCombinedRule(ruleSourceName: string, rule: CombinedRule): RuleIdentifier {
  const namespaceName = rule.namespace.name;
  const groupName = rule.group.name;

  if (rule.rulerRule) {
    return fromRulerRule(ruleSourceName, namespaceName, groupName, rule.rulerRule);
  }

  if (rule.promRule) {
    return fromRule(ruleSourceName, namespaceName, groupName, rule.promRule);
  }

  throw new Error('Could not create an id for a rule that is missing both `rulerRule` and `promRule`.');
}

export function fromRuleWithLocation(rule: RuleWithLocation): RuleIdentifier {
  return fromRulerRule(rule.ruleSourceName, rule.namespace, rule.group.name, rule.rule);
}

export function equal(a: RuleIdentifier, b: RuleIdentifier) {
  if (isGrafanaRuleIdentifier(a) && isGrafanaRuleIdentifier(b)) {
    return a.uid === b.uid;
  }

  if (isCloudRuleIdentifier(a) && isCloudRuleIdentifier(b)) {
    return (
      a.groupName === b.groupName &&
      a.namespace === b.namespace &&
      a.rulerRuleHash === b.rulerRuleHash &&
      a.ruleSourceName === b.ruleSourceName
    );
  }

  if (isPrometheusRuleIdentifier(a) && isPrometheusRuleIdentifier(b)) {
    return (
      a.groupName === b.groupName &&
      a.namespace === b.namespace &&
      a.ruleHash === b.ruleHash &&
      a.ruleSourceName === b.ruleSourceName
    );
  }

  return false;
}

const cloudRuleIdentifierPrefix = 'cri';
const prometheusRuleIdentifierPrefix = 'pri';

function escapeDollars(value: string): string {
  return value.replace(/\$/g, '_DOLLAR_');
}

function unesacapeDollars(value: string): string {
  return value.replace(/\_DOLLAR\_/g, '$');
}

export function parse(value: string, decodeFromUri = false): RuleIdentifier {
  const source = decodeFromUri ? decodeURIComponent(value) : value;
  const parts = source.split('$');

  if (parts.length === 1) {
    return { uid: value };
  }

  if (parts.length === 5) {
    const [prefix, ruleSourceName, namespace, groupName, hash] = parts.map(unesacapeDollars);

    if (prefix === cloudRuleIdentifierPrefix) {
      return { ruleSourceName, namespace, groupName, rulerRuleHash: Number(hash) };
    }

    if (prefix === prometheusRuleIdentifierPrefix) {
      return { ruleSourceName, namespace, groupName, ruleHash: Number(hash) };
    }
  }

  throw new Error(`Failed to parse rule location: ${value}`);
}

export function tryParse(value: string | undefined, decodeFromUri = false): RuleIdentifier | undefined {
  if (!value) {
    return;
  }

  try {
    return parse(value, decodeFromUri);
  } catch (error) {
    return;
  }
}

export function stringifyIdentifier(identifier: RuleIdentifier): string {
  if (isGrafanaRuleIdentifier(identifier)) {
    return identifier.uid;
  }

  if (isCloudRuleIdentifier(identifier)) {
    return [
      cloudRuleIdentifierPrefix,
      identifier.ruleSourceName,
      identifier.namespace,
      identifier.groupName,
      identifier.rulerRuleHash,
    ]
      .map(String)
      .map(escapeDollars)
      .join('$');
  }

  return [
    prometheusRuleIdentifierPrefix,
    identifier.ruleSourceName,
    identifier.namespace,
    identifier.groupName,
    identifier.ruleHash,
  ]
    .map(String)
    .map(escapeDollars)
    .join('$');
}

function hash(value: string): number {
  let hash = 0;
  if (value.length === 0) {
    return hash;
  }
  for (var i = 0; i < value.length; i++) {
    var char = value.charCodeAt(i);
    hash = (hash << 5) - hash + char;
    hash = hash & hash; // Convert to 32bit integer
  }
  return hash;
}

// this is used to identify lotex rules, as they do not have a unique identifier
function hashRulerRule(rule: RulerRuleDTO): number {
  if (isRecordingRulerRule(rule)) {
    return hash(JSON.stringify([rule.record, rule.expr, hashLabelsOrAnnotations(rule.labels)]));
  } else if (isAlertingRulerRule(rule)) {
    return hash(
      JSON.stringify([
        rule.alert,
        rule.expr,
        hashLabelsOrAnnotations(rule.annotations),
        hashLabelsOrAnnotations(rule.labels),
      ])
    );
  } else {
    throw new Error('only recording and alerting ruler rules can be hashed');
  }
}

function hashRule(rule: Rule): number {
  if (isRecordingRule(rule)) {
    return hash(JSON.stringify([rule.type, rule.query, hashLabelsOrAnnotations(rule.labels)]));
  }

  if (isAlertingRule(rule)) {
    return hash(
      JSON.stringify([
        rule.type,
        rule.query,
        hashLabelsOrAnnotations(rule.annotations),
        hashLabelsOrAnnotations(rule.labels),
      ])
    );
  }

  throw new Error('only recording and alerting rules can be hashed');
}

function hashLabelsOrAnnotations(item: Labels | Annotations | undefined): string {
  return JSON.stringify(Object.entries(item || {}).sort((a, b) => a[0].localeCompare(b[0])));
}

export function ruleIdentifierToRuleSourceName(identifier: RuleIdentifier): string {
  return isGrafanaRuleIdentifier(identifier) ? GRAFANA_RULES_SOURCE_NAME : identifier.ruleSourceName;
}
