import { variableAdapters } from '../adapters';
import { DashboardModel, PanelModel } from '../../dashboard/state';
import { isAdHoc } from '../guard';
import { safeStringifyValue } from '../../../core/utils/explore';
import { VariableModel } from '../types';
import { containsVariable, variableRegex, variableRegexExec } from '../utils';
import { DataLinkBuiltInVars } from '@grafana/data';

export interface GraphNode {
  id: string;
  label: string;
}

export interface GraphEdge {
  from: string;
  to: string;
}

export const createDependencyNodes = (variables: VariableModel[]): GraphNode[] => {
  const nodes: GraphNode[] = [];

  for (const variable of variables) {
    nodes.push({ id: variable.id, label: `${variable.id}` });
  }

  return nodes;
};

export const filterNodesWithDependencies = (nodes: GraphNode[], edges: GraphEdge[]): GraphNode[] => {
  return nodes.filter((node) => edges.some((edge) => edge.from === node.id || edge.to === node.id));
};

export const createDependencyEdges = (variables: VariableModel[]): GraphEdge[] => {
  const edges: GraphEdge[] = [];

  for (const variable of variables) {
    for (const other of variables) {
      if (variable === other) {
        continue;
      }

      const dependsOn = variableAdapters.get(variable.type).dependsOn(variable, other);

      if (dependsOn) {
        edges.push({ from: variable.id, to: other.id });
      }
    }
  }

  return edges;
};

function getVariableName(expression: string) {
  const match = variableRegexExec(expression);
  if (!match) {
    return null;
  }
  const variableName = match.slice(1).find((match) => match !== undefined);
  return variableName;
}

export const getUnknownVariableStrings = (variables: VariableModel[], model: any) => {
  variableRegex.lastIndex = 0;
  const unknownVariableNames: string[] = [];
  const modelAsString = safeStringifyValue(model, 2);
  const matches = modelAsString.match(variableRegex);

  if (!matches) {
    return unknownVariableNames;
  }

  for (const match of matches) {
    if (!match) {
      continue;
    }

    if (match.indexOf('$__') !== -1) {
      // ignore builtin variables
      continue;
    }

    if (match.indexOf('${__') !== -1) {
      // ignore builtin variables
      continue;
    }

    if (match.indexOf('$hashKey') !== -1) {
      // ignore Angular props
      continue;
    }

    const variableName = getVariableName(match);

    if (variables.some((variable) => variable.id === variableName)) {
      // ignore defined variables
      continue;
    }

    if (unknownVariableNames.find((name) => name === variableName)) {
      continue;
    }

    if (variableName) {
      unknownVariableNames.push(variableName);
    }
  }

  return unknownVariableNames;
};

const validVariableNames: Record<string, RegExp[]> = {
  alias: [/^m$/, /^measurement$/, /^col$/, /^tag_(\w+|\d+)$/],
  query: [/^timeFilter$/],
};

export const getPropsWithVariable = (variableId: string, parent: { key: string; value: any }, result: any) => {
  const stringValues = Object.keys(parent.value).reduce((all, key) => {
    const value = parent.value[key];
    if (!value || typeof value !== 'string') {
      return all;
    }

    const isValidName = validVariableNames[key]
      ? validVariableNames[key].find((regex: RegExp) => regex.test(variableId))
      : undefined;

    let hasVariable = containsVariable(value, variableId);
    if (key === 'repeat' && value === variableId) {
      // repeat stores value without variable format
      hasVariable = true;
    }

    if (!isValidName && hasVariable) {
      all = {
        ...all,
        [key]: value,
      };
    }

    return all;
  }, {} as Record<string, any>);

  const objectValues = Object.keys(parent.value).reduce((all, key) => {
    const value = parent.value[key];
    if (value && typeof value === 'object' && Object.keys(value).length) {
      let id = value.title || value.name || value.id || key;
      if (Array.isArray(parent.value) && parent.key === 'panels') {
        id = `${id}[${value.id}]`;
      }

      const newResult = getPropsWithVariable(variableId, { key, value }, {});

      if (Object.keys(newResult).length) {
        all = {
          ...all,
          [id]: newResult,
        };
      }
    }

    return all;
  }, {} as Record<string, any>);

  if (Object.keys(stringValues).length || Object.keys(objectValues).length) {
    result = {
      ...result,
      ...stringValues,
      ...objectValues,
    };
  }

  return result;
};

export interface VariableUsageTree {
  variable: VariableModel;
  tree: any;
}

export interface VariableUsages {
  unUsed: VariableModel[];
  usages: VariableUsageTree[];
}

export const createUsagesNetwork = (variables: VariableModel[], dashboard: DashboardModel | null): VariableUsages => {
  if (!dashboard) {
    return { unUsed: [], usages: [] };
  }

  const unUsed: VariableModel[] = [];
  let usages: VariableUsageTree[] = [];
  const model = dashboard.getSaveModelClone();

  for (const variable of variables) {
    const variableId = variable.id;
    const props = getPropsWithVariable(variableId, { key: 'model', value: model }, {});
    if (!Object.keys(props).length && !isAdHoc(variable)) {
      unUsed.push(variable);
    }

    if (Object.keys(props).length) {
      usages.push({ variable, tree: props });
    }
  }

  return { unUsed, usages };
};

export async function getUnknownsNetwork(
  variables: VariableModel[],
  dashboard: DashboardModel | null
): Promise<UsagesToNetwork[]> {
  return new Promise((resolve, reject) => {
    // can be an expensive call so we avoid blocking the main thread
    setTimeout(() => {
      try {
        const unknowns = createUnknownsNetwork(variables, dashboard);
        resolve(transformUsagesToNetwork(unknowns));
      } catch (e) {
        reject(e);
      }
    }, 200);
  });
}

function createUnknownsNetwork(variables: VariableModel[], dashboard: DashboardModel | null): VariableUsageTree[] {
  if (!dashboard) {
    return [];
  }

  let unknown: VariableUsageTree[] = [];
  const model = dashboard.getSaveModelClone();

  const unknownVariables = getUnknownVariableStrings(variables, model);
  for (const unknownVariable of unknownVariables) {
    const props = getPropsWithVariable(unknownVariable, { key: 'model', value: model }, {});
    if (Object.keys(props).length) {
      const variable = { id: unknownVariable, name: unknownVariable } as unknown as VariableModel;
      unknown.push({ variable, tree: props });
    }
  }

  return unknown;
}

/*
  getAllAffectedPanelIdsForVariableChange is a function that extracts all the panel ids that are affected by a single variable
  change. It will traverse all chained variables to identify all cascading changes too.

  This is done entirely by parsing the current dashboard json and doesn't take under consideration a user cancelling
  a variable query or any faulty variable queries.

  This doesn't take circular dependencies in consideration.
 */
export function getAllAffectedPanelIdsForVariableChange(
  variableId: string,
  variables: VariableModel[],
  panels: PanelModel[]
): number[] {
  const flattenedPanels = flattenPanels(panels);
  let affectedPanelIds: number[] = getAffectedPanelIdsForVariable(variableId, flattenedPanels);
  const affectedPanelIdsForAllVariables = getAffectedPanelIdsForVariable(
    DataLinkBuiltInVars.includeVars,
    flattenedPanels
  );
  affectedPanelIds = [...new Set([...affectedPanelIdsForAllVariables, ...affectedPanelIds])];

  const dependencies = getDependenciesForVariable(variableId, variables, new Set());
  for (const dependency of dependencies) {
    const affectedPanelIdsForDependency = getAffectedPanelIdsForVariable(dependency, flattenedPanels);
    affectedPanelIds = [...new Set([...affectedPanelIdsForDependency, ...affectedPanelIds])];
  }

  return affectedPanelIds;
}

export function getDependenciesForVariable(
  variableId: string,
  variables: VariableModel[],
  deps: Set<string>
): Set<string> {
  if (!variables.length) {
    return deps;
  }

  for (const variable of variables) {
    if (variable.name === variableId) {
      continue;
    }

    const depends = variableAdapters.get(variable.type).dependsOn(variable, { name: variableId });
    if (!depends) {
      continue;
    }

    deps.add(variable.name);
    deps = getDependenciesForVariable(variable.name, variables, deps);
  }

  return deps;
}

export function getAffectedPanelIdsForVariable(variableId: string, panels: PanelModel[]): number[] {
  if (!panels.length) {
    return [];
  }

  const affectedPanelIds: number[] = [];
  const repeatRegex = new RegExp(`"repeat":"${variableId}"`);
  for (const panel of panels) {
    const panelAsJson = safeStringifyValue(panel.getSaveModel());

    // check for repeats that don't use variableRegex
    const repeatMatches = panelAsJson.match(repeatRegex);
    if (repeatMatches?.length) {
      affectedPanelIds.push(panel.id);
      continue;
    }

    const matches = panelAsJson.match(variableRegex);
    if (!matches) {
      continue;
    }

    for (const match of matches) {
      const variableName = getVariableName(match);
      if (variableName === variableId) {
        affectedPanelIds.push(panel.id);
        break;
      }
    }
  }

  return affectedPanelIds;
}

export interface UsagesToNetwork {
  variable: VariableModel;
  nodes: GraphNode[];
  edges: GraphEdge[];
  showGraph: boolean;
}

export const traverseTree = (usage: UsagesToNetwork, parent: { id: string; value: any }): UsagesToNetwork => {
  const { id, value } = parent;
  const { nodes, edges } = usage;

  if (value && typeof value === 'string') {
    const leafId = `${parent.id}-${value}`;
    nodes.push({ id: leafId, label: value });
    edges.push({ from: leafId, to: id });

    return usage;
  }

  if (value && typeof value === 'object') {
    const keys = Object.keys(value);
    for (const key of keys) {
      const leafId = `${parent.id}-${key}`;
      nodes.push({ id: leafId, label: key });
      edges.push({ from: leafId, to: id });
      usage = traverseTree(usage, { id: leafId, value: value[key] });
    }

    return usage;
  }

  return usage;
};

export const transformUsagesToNetwork = (usages: VariableUsageTree[]): UsagesToNetwork[] => {
  const results: UsagesToNetwork[] = [];

  for (const usage of usages) {
    const { variable, tree } = usage;
    const result: UsagesToNetwork = {
      variable,
      nodes: [{ id: 'dashboard', label: 'dashboard' }],
      edges: [],
      showGraph: false,
    };
    results.push(traverseTree(result, { id: 'dashboard', value: tree }));
  }

  return results;
};

const countLeaves = (object: any): number => {
  const total = Object.values(object).reduce((count: number, value: any) => {
    if (typeof value === 'object') {
      return count + countLeaves(value);
    }

    return count + 1;
  }, 0);

  return total as unknown as number;
};

export const getVariableUsages = (variableId: string, usages: VariableUsageTree[]): number => {
  const usage = usages.find((usage) => usage.variable.id === variableId);
  if (!usage) {
    return 0;
  }

  return countLeaves(usage.tree);
};

export function flattenPanels(panels: PanelModel[]): PanelModel[] {
  const result: PanelModel[] = [];

  for (const panel of panels) {
    result.push(panel);
    if (panel.panels?.length) {
      result.push(...flattenPanels(panel.panels.map((p: PanelModel) => new PanelModel(p))));
    }
  }

  return result;
}
