import { observable, action, computed } from "mobx";

import { parseISO } from "date-fns/parseISO";
import { addHours } from "date-fns/addHours";
import { addMinutes } from "date-fns/addMinutes";
import { subMinutes } from "date-fns/subMinutes";
import { differenceInDays } from "date-fns/differenceInDays";
import { differenceInHours } from "date-fns/differenceInHours";
import { differenceInMinutes } from "date-fns/differenceInMinutes";

import type {
  APIAlertT,
  APIAlertGroupT,
  APIAlertmanagerUpstreamT,
  AlertmanagerSilencePayloadT,
  AlertmanagerSilenceMatcherT,
} from "Models/APITypes";
import { StringToOption, OptionT, MultiValueOptionT } from "Common/Select";
import { QueryOperators } from "Common/Query";

let idCounter = 0;
const uniqueId = (): string => String(++idCounter);

export interface MatcherT {
  name: string;
  values: OptionT[];
  isEqual: boolean;
  isRegex: boolean;
}

export interface MatcherWithIDT extends MatcherT {
  id: string;
}

interface SimplifiedMatcherT {
  n: string;
  v: string[];
  r: boolean;
  e: boolean;
}

interface SilenceFormDataFromBase64 {
  am: MultiValueOptionT[];
  m: SimplifiedMatcherT[];
  d: number;
  c: string;
}

const NewEmptyMatcher = (): MatcherWithIDT => {
  return {
    id: uniqueId(),
    name: "",
    values: [],
    isRegex: false,
    isEqual: true,
  };
};

const MatcherToOperator = (
  matcher: MatcherT | MatcherWithIDT | AlertmanagerSilenceMatcherT,
): string => {
  if (matcher.isRegex) {
    return matcher.isEqual === false
      ? QueryOperators.NegativeRegex
      : QueryOperators.Regex;
  }
  return matcher.isEqual === false
    ? QueryOperators.NotEqual
    : QueryOperators.Equal;
};

const AlertmanagerClustersToOption = (clusterDict: {
  [key: string]: string[];
}): MultiValueOptionT[] =>
  Object.entries(clusterDict).map(([clusterID, clusterMembers]) => ({
    label:
      clusterMembers.length > 1 ? `Cluster: ${clusterID}` : clusterMembers[0],
    value: clusterMembers,
  }));

export const EscapeRegex = (v: string): string => {
  return v.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
};

const UnescapeRegex = (v: string): string => {
  return v.replaceAll("\\", "");
};

const MatchersFromGroup = (
  group: APIAlertGroupT,
  stripLabels: string[],
  onlyActive?: boolean,
): MatcherWithIDT[] => {
  const matchers: MatcherWithIDT[] = [];

  const allLabels: { [key: string]: string[] } = {};
  for (const [state, labels] of Object.entries(group.allLabels)) {
    if (onlyActive === true && state !== "active") {
      continue;
    }
    for (const [key, values] of Object.entries(labels).filter(
      ([key, _]) => !stripLabels.includes(key),
    )) {
      allLabels[key] = Array.from(
        new Set([...(allLabels[key] || []), ...values]),
      );
    }
  }
  for (const [key, values] of Object.entries(allLabels)) {
    matchers.push({
      id: uniqueId(),
      name: key,
      values: values.map((value) => StringToOption(value)),
      isRegex: values.length > 1,
      isEqual: true,
    });
  }

  return matchers;
};

const MatchersFromAlerts = (
  group: APIAlertGroupT,
  stripLabels: string[],
  alerts: APIAlertT[],
): MatcherWithIDT[] => {
  const matchers: MatcherWithIDT[] = [];

  // add matchers for all shared labels in this group
  for (const [key, value] of Object.entries(
    // eslint-disable-next-line @eslint-react/purity -- false positive: called from MobX action, not from render
    Object.assign(
      {},
      Object.fromEntries(group.labels.map((l) => [l.name, l.value])),
      Object.fromEntries(group.shared.labels.map((l) => [l.name, l.value])),
    ),
  )) {
    if (!stripLabels.includes(key)) {
      const matcher = NewEmptyMatcher();
      matcher.name = key;
      matcher.values = [StringToOption(value)];
      matchers.push(matcher);
    }
  }

  // array of arrays with label keys for each alert
  const allLabelKeys = alerts
    .map((alert) => alert.labels.map((l) => l.name))
    .filter((a) => a.length > 0);

  // this is the list of label key that are shared across all alerts in the group
  // https://stackoverflow.com/a/34498210/1154047
  const sharedLabelKeys = allLabelKeys.length
    ? allLabelKeys.reduce(function (r, a) {
        const last: { [key: string]: number } = {};
        return r.filter(function (b) {
          const p = a.indexOf(b, last[b] || 0);
          if (~p) {
            last[b] = p + 1;
            return true;
          }
          return false;
        });
      })
    : [];

  // add matchers for all unique labels in this group
  const labels: { [key: string]: Set<string> } = {};
  for (const alert of alerts) {
    for (const label of alert.labels) {
      if (
        sharedLabelKeys.includes(label.name) &&
        !stripLabels.includes(label.name)
      ) {
        if (!labels[label.name]) {
          labels[label.name] = new Set();
        }
        labels[label.name].add(label.value);
      }
    }
  }
  for (const [key, values] of Object.entries(labels)) {
    matchers.push({
      id: uniqueId(),
      name: key,
      values: Array.from(values)
        .sort()
        .map((value) => StringToOption(value)),
      isRegex: values.size > 1,
      isEqual: true,
    });
  }

  return matchers;
};

export interface ClusterRequestT {
  cluster: string;
  members: string[];
  isDone: boolean;
  silenceID: undefined | string;
  silenceLink: undefined | string;
  error: null | string;
}

const NewClusterRequest = (
  cluster: string,
  members: string[],
): ClusterRequestT => ({
  cluster: cluster,
  members: members,
  isDone: false,
  silenceID: undefined,
  silenceLink: undefined,
  error: null,
});

const GenerateAlertmanagerSilenceData = (
  startsAt: Date,
  endsAt: Date,
  matchers: MatcherT[],
  author: string,
  comment: string,
  silenceID: string | null = null,
): AlertmanagerSilencePayloadT => {
  const payload: AlertmanagerSilencePayloadT = {
    matchers: matchers.map((m) => ({
      name: m.name,
      value:
        m.values.length > 1
          ? `(${m.values
              .map((v) =>
                v.wasCreated || !m.isRegex ? v.value : EscapeRegex(v.value),
              )
              .join("|")})`
          : m.values.length === 1
            ? m.values[0].wasCreated || !m.isRegex
              ? m.values[0].value
              : EscapeRegex(m.values[0].value)
            : "",
      isRegex: m.isRegex,
      isEqual: m.isEqual,
    })),
    startsAt: startsAt.toISOString(),
    endsAt: endsAt.toISOString(),
    createdBy: author,
    comment: comment,
  };
  if (silenceID !== null) {
    payload.id = silenceID;
  }
  return payload;
};

const UnpackRegexMatcherValues = (isRegex: boolean, value: string) => {
  let val: string = value;
  if (isRegex) {
    val = UnescapeRegex(val);
  }
  if (isRegex && val.match(/^\(([a-zA-Z0-9_\-. ]+\|)+[a-zA-Z0-9_\-. ]+\)$/)) {
    return val
      .slice(1, -1)
      .split("|")
      .map((v) => StringToOption(v));
  } else if (
    isRegex &&
    val.match(/^([a-zA-Z0-9_\-. ]+\|)+[a-zA-Z0-9_\-. ]+$/)
  ) {
    return val.split("|").map((v) => StringToOption(v));
  } else {
    return [{ ...StringToOption(val), wasCreated: true }];
  }
};

type SilenceFormTabT = "editor" | "browser";
type SilenceFormStageT = "form" | "preview" | "submit";

interface SilenceFormStoreToggleT {
  visible: boolean;
  blurred: boolean;
  toggle: () => void;
  hide: () => void;
  show: () => void;
  setBlur: (val: boolean) => void;
}

interface SilenceFormStoreTabT {
  current: SilenceFormTabT;
  setTab: (value: SilenceFormTabT) => void;
}

interface DurationT {
  days: number;
  hours: number;
  minutes: number;
}

interface SilenceFormStoreDataT {
  currentStage: SilenceFormStageT;
  wasValidated: boolean;
  silenceID: null | undefined | string;
  alertmanagers: MultiValueOptionT[];
  matchers: MatcherWithIDT[];
  startsAt: Date;
  endsAt: Date;
  comment: string;
  author: string;
  requestsByCluster: { [key: string]: ClusterRequestT };
  autofillMatchers: boolean;
  resetInputs: boolean;
  readonly toBase64: string;
  fromBase64: (s: string) => boolean;
  readonly isValid: boolean;
  resetStartEnd: () => void;
  resetProgress: () => void;
  resetSilenceID: () => void;
  setSilenceID: (id: string | null) => void;
  setAlertmanagers: (val: MultiValueOptionT[]) => void;
  setAutofillMatchers: (v: boolean) => void;
  setResetInputs: (v: boolean) => void;
  setStage: (val: SilenceFormStageT) => void;
  setMatchers: (m: MatcherWithIDT[]) => void;
  addEmptyMatcher: () => void;
  addMatcherWithID: (m: MatcherWithIDT) => void;
  deleteMatcher: (id: string) => void;
  fillMatchersFromGroup: (
    group: APIAlertGroupT,
    stripLabels: string[],
    alertmanagers: MultiValueOptionT[],
    alerts?: APIAlertT[],
  ) => void;
  fillFormFromSilence: (
    alertmanager: APIAlertmanagerUpstreamT,
    silence: AlertmanagerSilencePayloadT,
  ) => void;
  setAuthor: (a: string) => void;
  setComment: (c: string) => void;
  verifyStarEnd: () => void;
  setStart: (startsAt: Date) => void;
  setEnd: (endsAt: Date) => void;
  incStart: (minutes: number) => void;
  decStart: (minutes: number) => void;
  incEnd: (minutes: number) => void;
  decEnd: (minutes: number) => void;
  setWasValidated: (v: boolean) => void;
  setRequestsByCluster: (val: { [key: string]: ClusterRequestT }) => void;
  setRequestsByClusterUpdate: (
    key: string,
    v: Partial<ClusterRequestT>,
  ) => void;
  readonly toAlertmanagerPayload: AlertmanagerSilencePayloadT;
  readonly toDuration: DurationT;
}

class SilenceFormStore {
  toggle: SilenceFormStoreToggleT;
  tab: SilenceFormStoreTabT;
  data: SilenceFormStoreDataT;

  constructor() {
    this.toggle = observable(
      {
        visible: false as boolean,
        blurred: false as boolean,
        toggle() {
          this.visible = !this.visible;
        },
        hide() {
          this.visible = false;
        },
        show() {
          this.visible = true;
        },
        setBlur(val: boolean) {
          this.blurred = val;
        },
      },
      {
        toggle: action.bound,
        hide: action.bound,
        show: action.bound,
        setBlur: action.bound,
      },
    );

    this.tab = observable(
      {
        current: "editor" as SilenceFormTabT,
        setTab(value: SilenceFormTabT) {
          this.current = value;
        },
      },
      {
        setTab: action.bound,
      },
    );

    // form data is stored here, it's global (rather than attached to the form)
    // so it can be manipulated from other parts of the code
    // example: when user clicks a silence button on alert we should populate
    // this form from that alert so user can easily silence that alert
    this.data = observable(
      {
        currentStage: "form" as SilenceFormStageT,
        wasValidated: false as boolean,
        silenceID: null as null | undefined | string,
        alertmanagers: [] as MultiValueOptionT[],
        matchers: [] as MatcherWithIDT[],
        startsAt: new Date(),
        endsAt: addHours(new Date(), 1),
        comment: "",
        author: "",
        requestsByCluster: {} as { [key: string]: ClusterRequestT },
        autofillMatchers: true as boolean,
        resetInputs: true as boolean,

        get toBase64() {
          const json = JSON.stringify({
            am: this.alertmanagers,
            m: this.matchers.map((m: MatcherWithIDT) => ({
              n: m.name,
              r: m.isRegex,
              e: m.isEqual,
              v: m.values.map((v) => v.value),
            })),
            d: differenceInMinutes(this.endsAt, this.startsAt),
            c: this.comment,
          });
          return window.btoa(json);
        },

        fromBase64(s: string): boolean {
          let parsed: SilenceFormDataFromBase64;
          try {
            parsed = JSON.parse(window.atob(s));
          } catch (error) {
            console.error(`Failed to parse JSON: ${error}`);
            return false;
          }

          const matchers: MatcherWithIDT[] = [];
          parsed.m.forEach((m: SimplifiedMatcherT) => {
            const matcher = NewEmptyMatcher();
            matcher.name = m.n;
            matcher.isRegex = m.r;
            matcher.isEqual = m.e;
            matcher.values = m.v.map((v) => StringToOption(v));
            matchers.push(matcher);
          });

          if (matchers.length > 0) {
            this.alertmanagers = parsed.am;
            this.matchers = matchers;

            this.startsAt = new Date();
            this.endsAt = addMinutes(this.startsAt, parsed.d);
            this.comment = parsed.c;

            this.silenceID = null;
            this.autofillMatchers = false;
            this.resetInputs = false;
            return true;
          }

          return false;
        },

        get isValid() {
          if (this.alertmanagers.length === 0) return false;
          if (this.matchers.length === 0) return false;
          if (
            this.matchers.filter(
              (m) =>
                m.name === "" ||
                m.values.length === 0 ||
                m.values.filter((v) => v.value === "").length > 0,
            ).length > 0
          )
            return false;
          if (this.comment === "") return false;
          if (this.author === "") return false;
          return true;
        },

        resetStartEnd() {
          this.startsAt = new Date();
          this.endsAt = addHours(new Date(), 1);
        },

        resetProgress() {
          this.currentStage = "form";
          this.wasValidated = false;
        },

        resetSilenceID() {
          this.silenceID = null;
        },

        setSilenceID(id: string | null) {
          this.silenceID = id;
        },

        setAlertmanagers(val: MultiValueOptionT[]) {
          this.alertmanagers = val;
        },

        setAutofillMatchers(v: boolean) {
          this.autofillMatchers = v;
        },
        setResetInputs(v: boolean) {
          this.resetInputs = v;
        },

        setStage(val: SilenceFormStageT) {
          this.currentStage = val;
        },

        setMatchers(m: MatcherWithIDT[]) {
          this.matchers = m;
        },

        // append a new empty matcher to the list
        addEmptyMatcher() {
          this.matchers.push(NewEmptyMatcher());
        },
        addMatcherWithID(m: MatcherWithIDT) {
          this.matchers.push(m);
        },

        deleteMatcher(id: string) {
          // only delete matchers if we have more than 1
          if (this.matchers.length > 1) {
            this.matchers = this.matchers.filter((m) => m.id !== id);
          }
        },

        // if alerts argument is not passed all group alerts will be used
        fillMatchersFromGroup(
          group: APIAlertGroupT,
          stripLabels: string[],
          alertmanagers: MultiValueOptionT[],
          alerts?: APIAlertT[],
        ) {
          this.alertmanagers = alertmanagers;

          this.matchers = alerts
            ? MatchersFromAlerts(group, stripLabels, alerts)
            : MatchersFromGroup(group, stripLabels);
          // ensure that silenceID is nulled, since it's used to edit silences
          // and this is used to silence groups
          this.silenceID = null;
          // disable matcher autofill
          this.autofillMatchers = false;
          // disable alertmanager input reset
          this.resetInputs = false;
        },

        fillFormFromSilence(
          alertmanager: APIAlertmanagerUpstreamT,
          silence: AlertmanagerSilencePayloadT,
        ) {
          this.silenceID = silence.id;

          this.alertmanagers = AlertmanagerClustersToOption({
            [alertmanager.cluster]: alertmanager.clusterMembers,
          });

          const matchers: MatcherWithIDT[] = [];
          for (const m of silence.matchers) {
            const matcher = NewEmptyMatcher();
            matcher.name = m.name;
            matcher.values = UnpackRegexMatcherValues(m.isRegex, m.value);
            matcher.isRegex = m.isRegex;
            matcher.isEqual = m.isEqual === false ? false : true;
            matchers.push(matcher);
          }
          this.matchers = matchers;

          this.startsAt = parseISO(silence.startsAt);
          this.endsAt = parseISO(silence.endsAt);
          this.comment = silence.comment;
          this.author = silence.createdBy;

          // disable matcher autofill
          this.autofillMatchers = false;
        },

        setAuthor(a: string) {
          this.author = a;
        },

        setComment(c: string) {
          this.comment = c;
        },

        verifyStarEnd() {
          const now = new Date();
          now.setSeconds(0);
          if (this.startsAt < now) {
            this.startsAt = now;
          }

          if (this.endsAt <= this.startsAt) {
            this.endsAt = addMinutes(this.startsAt, 1);
          }
        },
        setStart(startsAt: Date) {
          this.startsAt = startsAt;
        },
        setEnd(endsAt: Date) {
          this.endsAt = endsAt;
        },
        incStart(minutes: number) {
          this.startsAt = addMinutes(this.startsAt, minutes);
          this.verifyStarEnd();
        },
        decStart(minutes: number) {
          this.startsAt = subMinutes(this.startsAt, minutes);
          this.verifyStarEnd();
        },

        incEnd(minutes: number) {
          this.endsAt = addMinutes(this.endsAt, minutes);
          this.verifyStarEnd();
        },
        decEnd(minutes: number) {
          this.endsAt = subMinutes(this.endsAt, minutes);
          this.verifyStarEnd();
        },

        setWasValidated(v: boolean) {
          this.wasValidated = v;
        },

        setRequestsByCluster(val: { [key: string]: ClusterRequestT }) {
          this.requestsByCluster = val;
        },
        setRequestsByClusterUpdate(key: string, v: Partial<ClusterRequestT>) {
          this.requestsByCluster[key] = {
            ...this.requestsByCluster[key],
            ...v,
          };
        },

        get toAlertmanagerPayload() {
          const startsAt = new Date(this.startsAt);
          startsAt.setSeconds(0);
          startsAt.setMilliseconds(0);
          const endsAt = new Date(this.endsAt);
          endsAt.setSeconds(0);
          endsAt.setMilliseconds(0);
          return GenerateAlertmanagerSilenceData(
            startsAt,
            endsAt,
            this.matchers,
            this.author,
            this.comment,
            this.silenceID,
          );
        },

        get toDuration() {
          const data: DurationT = {
            days: differenceInDays(this.endsAt, this.startsAt),
            hours: differenceInHours(this.endsAt, this.startsAt) % 24,
            minutes: differenceInMinutes(this.endsAt, this.startsAt) % 60,
          };
          return data;
        },
      },
      {
        toBase64: computed,
        fromBase64: action.bound,
        resetStartEnd: action.bound,
        resetProgress: action.bound,
        resetSilenceID: action.bound,
        setSilenceID: action.bound,
        setAlertmanagers: action.bound,
        setAutofillMatchers: action.bound,
        setResetInputs: action.bound,
        setStage: action.bound,
        setMatchers: action.bound,
        addEmptyMatcher: action.bound,
        addMatcherWithID: action.bound,
        deleteMatcher: action.bound,
        fillMatchersFromGroup: action.bound,
        fillFormFromSilence: action.bound,
        setAuthor: action.bound,
        setComment: action.bound,
        verifyStarEnd: action.bound,
        setStart: action.bound,
        setEnd: action.bound,
        incStart: action.bound,
        decStart: action.bound,
        incEnd: action.bound,
        decEnd: action.bound,
        isValid: computed,
        setWasValidated: action.bound,
        setRequestsByCluster: action.bound,
        setRequestsByClusterUpdate: action.bound,
        toAlertmanagerPayload: computed,
        toDuration: computed,
      },
      { name: "Silence form store" },
    );
  }
}

export {
  SilenceFormStore,
  NewEmptyMatcher,
  AlertmanagerClustersToOption,
  MatchersFromGroup,
  GenerateAlertmanagerSilenceData,
  NewClusterRequest,
  MatcherToOperator,
};
