import { omitBy, pickBy, isNil, isNumber, isString } from 'lodash';

import {
  type ConfigOverrideRule,
  type DynamicConfigValue,
  FieldColorModeId,
  type FieldConfig,
  FieldConfigProperty,
  type FieldConfigSource,
  FieldMatcherID,
  fieldReducers,
  FieldType,
  NullValueMode,
  type PanelTypeChangedHandler,
  ReducerID,
  type Threshold,
  ThresholdsMode,
} from '@grafana/data';
import {
  LegendDisplayMode,
  TooltipDisplayMode,
  AxisPlacement,
  GraphDrawStyle,
  type GraphFieldConfig,
  GraphGradientMode,
  GraphThresholdsStyleMode,
  LineInterpolation,
  type LineStyle,
  VisibilityMode,
  ScaleDistribution,
  StackingMode,
  SortOrder,
  GraphTransform,
  type AnnotationQuery,
  ComparisonOperation,
} from '@grafana/schema';
import { type TimeRegionConfig } from 'app/core/utils/timeRegions';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { DashboardAnnotationsDataLayer } from 'app/features/dashboard-scene/scene/DashboardAnnotationsDataLayer';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { dashboardSceneGraph } from 'app/features/dashboard-scene/utils/dashboardSceneGraph';
import { type GrafanaQuery, GrafanaQueryType } from 'app/plugins/datasource/grafana/types';

import { defaultGraphConfig } from './config';
import { type Options } from './panelcfg.gen';

let dashboardRefreshDebouncer: ReturnType<typeof setTimeout> | null = null;

/**
 * This is called when the panel changes from another panel
 */
export const graphPanelChangedHandler: PanelTypeChangedHandler = (
  panel,
  prevPluginId,
  prevOptions,
  prevFieldConfig
) => {
  // Changing from angular/flot panel to react/uPlot
  if (prevPluginId === 'graph' && prevOptions.angular) {
    const { fieldConfig, options, annotations } = graphToTimeseriesOptions({
      ...prevOptions.angular,
      fieldConfig: prevFieldConfig,
      panel: panel,
    });

    if (annotations?.length > 0) {
      addAnnotationsToDashboard(annotations);
    }

    panel.fieldConfig = fieldConfig; // Mutates the incoming panel
    panel.alert = prevOptions.angular.alert;
    return options;
  }

  //fixes graph -> viz renaming in custom.hideFrom field config by mutation.
  migrateHideFrom(panel);

  return {};
};

export function graphToTimeseriesOptions(angular: any): {
  fieldConfig: FieldConfigSource;
  options: Options;
  annotations: AnnotationQuery[];
} {
  let annotations: AnnotationQuery[] = [];

  const overrides: ConfigOverrideRule[] = angular.fieldConfig?.overrides ?? [];
  const yaxes = angular.yaxes ?? [];
  let y1 = getFieldConfigFromOldAxis(yaxes[0]);
  if (angular.fieldConfig?.defaults) {
    y1 = {
      ...angular.fieldConfig?.defaults,
      ...y1, // Keep the y-axis unit and custom
    };
  }

  // Dashes
  const dash: LineStyle = {
    fill: angular.dashes ? 'dash' : 'solid',
    dash: [angular.dashLength ?? 10, angular.spaceLength ?? 10],
  };

  // "seriesOverrides": [
  //   {
  //     "$$hashKey": "object:183",
  //     "alias": "B-series",
  //     "fill": 3,
  //     "nullPointMode": "null as zero",
  //     "lines": true,
  //     "linewidth": 2
  //   }
  // ],

  if (angular.aliasColors) {
    for (const alias of Object.keys(angular.aliasColors)) {
      const color = angular.aliasColors[alias];
      if (color) {
        overrides.push({
          matcher: {
            id: FieldMatcherID.byName,
            options: alias,
          },
          properties: [
            {
              id: FieldConfigProperty.Color,
              value: {
                mode: FieldColorModeId.Fixed,
                fixedColor: color,
              },
            },
          ],
        });
      }
    }
  }

  let hasFillBelowTo = false;

  if (angular.seriesOverrides?.length) {
    for (const seriesOverride of angular.seriesOverrides) {
      if (!seriesOverride.alias) {
        continue; // the matcher config
      }
      const aliasIsRegex = /^([/~@;%#'])(.*?)\1([gimsuy]*)$/.test(seriesOverride.alias);
      const rule: ConfigOverrideRule = {
        matcher: {
          id: aliasIsRegex ? FieldMatcherID.byRegexp : FieldMatcherID.byName,
          options: seriesOverride.alias,
        },
        properties: [],
      };
      let dashOverride: LineStyle | undefined = undefined;

      for (const p of Object.keys(seriesOverride)) {
        const v = seriesOverride[p];
        switch (p) {
          // Ignore
          case 'alias':
          case '$$hashKey':
            break;
          // Link to y axis settings
          case 'yaxis':
            if (2 === v) {
              const y2 = getFieldConfigFromOldAxis(yaxes[1]);
              fillY2DynamicValues(y1, y2, rule.properties);
            }
            break;
          case 'fill':
            rule.properties.push({
              id: 'custom.fillOpacity',
              value: v * 10, // was 0-10, new graph is 0 - 100
            });
            break;
          case 'fillBelowTo':
            hasFillBelowTo = true;
            rule.properties.push({
              id: 'custom.fillBelowTo',
              value: v,
            });
            break;
          case 'fillGradient':
            if (v) {
              rule.properties.push({
                id: 'custom.fillGradient',
                value: 'opacity', // was 0-10
              });
              rule.properties.push({
                id: 'custom.fillOpacity',
                value: v * 10, // was 0-10, new graph is 0 - 100
              });
            }
            break;
          case 'points':
            rule.properties.push({
              id: 'custom.showPoints',
              value: v ? VisibilityMode.Always : VisibilityMode.Never,
            });
            break;
          case 'bars':
            if (v) {
              rule.properties.push({
                id: 'custom.drawStyle',
                value: GraphDrawStyle.Bars,
              });
              rule.properties.push({
                id: 'custom.fillOpacity',
                value: 100, // solid bars
              });
            } else {
              rule.properties.push({
                id: 'custom.drawStyle',
                value: GraphDrawStyle.Line, // Change from bars
              });
            }
            break;
          case 'lines':
            if (v) {
              rule.properties.push({
                id: 'custom.drawStyle',
                value: 'line',
              });
            } else {
              rule.properties.push({
                id: 'custom.lineWidth',
                value: 0,
              });
            }
            break;
          case 'linewidth':
            rule.properties.push({
              id: 'custom.lineWidth',
              value: v,
            });
            break;
          case 'pointradius':
            rule.properties.push({
              id: 'custom.pointSize',
              value: 2 + v * 2,
            });
            break;
          case 'dashLength':
          case 'spaceLength':
          case 'dashes':
            if (!dashOverride) {
              dashOverride = {
                fill: dash.fill,
                dash: [...dash.dash!],
              };
            }
            switch (p) {
              case 'dashLength':
                dashOverride.dash![0] = v;
                break;
              case 'spaceLength':
                dashOverride.dash![1] = v;
                break;
              case 'dashes':
                dashOverride.fill = v ? 'dash' : 'solid';
                break;
            }
            break;
          case 'stack':
            rule.properties.push({
              id: 'custom.stacking',
              value: getStackingFromOverrides(v),
            });
            break;
          case 'color':
            rule.properties.push({
              id: 'color',
              value: {
                fixedColor: v,
                mode: FieldColorModeId.Fixed,
              },
            });
            break;
          case 'transform':
            rule.properties.push({
              id: 'custom.transform',
              value: v === 'negative-Y' ? GraphTransform.NegativeY : GraphTransform.Constant,
            });
            break;
          default:
            console.log('Ignore override migration:', seriesOverride.alias, p, v);
        }
      }
      if (dashOverride) {
        rule.properties.push({
          id: 'custom.lineStyle',
          value: dashOverride,
        });
      }
      if (rule.properties.length) {
        overrides.push(rule);
      }
    }
  }

  const graph: GraphFieldConfig = y1.custom ?? {};
  graph.drawStyle = angular.bars ? GraphDrawStyle.Bars : angular.lines ? GraphDrawStyle.Line : GraphDrawStyle.Points;

  if (angular.points) {
    graph.showPoints = VisibilityMode.Always;

    if (isNumber(angular.pointradius)) {
      graph.pointSize = 2 + angular.pointradius * 2;
    }
  } else if (graph.drawStyle !== GraphDrawStyle.Points) {
    graph.showPoints = VisibilityMode.Never;
  }

  graph.lineWidth = angular.linewidth;
  if (dash.fill !== 'solid') {
    graph.lineStyle = dash;
  }

  if (hasFillBelowTo) {
    graph.fillOpacity = 35; // bands are hardcoded in flot
  } else if (isNumber(angular.fill)) {
    graph.fillOpacity = angular.fill * 10; // fill was 0 - 10, new is 0 to 100
  }

  if (isNumber(angular.fillGradient) && angular.fillGradient > 0) {
    graph.gradientMode = GraphGradientMode.Opacity;
    graph.fillOpacity = angular.fillGradient * 10; // fill is 0-10
  }

  graph.spanNulls = angular.nullPointMode === NullValueMode.Ignore;

  if (angular.steppedLine) {
    graph.lineInterpolation = LineInterpolation.StepAfter;
  }

  if (graph.drawStyle === GraphDrawStyle.Bars) {
    graph.fillOpacity = 100; // bars were always
  }

  if (angular.stack) {
    graph.stacking = {
      mode: angular.percentage ? StackingMode.Percent : StackingMode.Normal,
      group: defaultGraphConfig.stacking!.group,
    };

    if (angular.percentage) {
      if (angular.yaxis) {
        delete y1.min;
        delete y1.max;

        // TimeSeries currently uses 0-1 for percent, so allowing zero leaves only top and bottom ticks.
        // removing it feels better. probably should fix in TimeSeries, but let's kick it down the road
        if (y1.decimals === 0) {
          delete y1.decimals;
        }
      }
    }
  }

  y1.custom = omitBy(graph, isNil);
  y1.nullValueMode = angular.nullPointMode;

  const options: Options = {
    legend: {
      displayMode: LegendDisplayMode.List,
      showLegend: true,
      placement: 'bottom',
      calcs: [],
    },
    tooltip: {
      mode: TooltipDisplayMode.Single,
      sort: SortOrder.None,
    },
  };

  // Legend config migration
  const legendConfig = angular.legend;
  if (legendConfig) {
    if (legendConfig.show) {
      options.legend.displayMode = legendConfig.alignAsTable ? LegendDisplayMode.Table : LegendDisplayMode.List;
    } else {
      options.legend.showLegend = false;
    }

    if (legendConfig.rightSide) {
      options.legend.placement = 'right';
    }

    if (angular.legend.values) {
      const enabledLegendValues = pickBy(angular.legend);
      options.legend.calcs = getReducersFromLegend(enabledLegendValues);
    }

    if (angular.legend.sideWidth) {
      options.legend.width = angular.legend.sideWidth;
    }

    if (legendConfig.hideZero) {
      overrides.push(getLegendHideFromOverride(ReducerID.allIsZero));
    }

    if (legendConfig.hideEmpty) {
      overrides.push(getLegendHideFromOverride(ReducerID.allIsNull));
    }
  }

  // timeRegions migration
  if (angular.timeRegions?.length) {
    let regions = angular.timeRegions.map((old: GraphTimeRegionConfig, idx: number) => ({
      name: `T${idx}`,
      color: old.colorMode !== 'custom' ? old.colorMode : old.fillColor,
      line: old.line,
      fill: old.fill,
      fromDayOfWeek: old.fromDayOfWeek,
      toDayOfWeek: old.toDayOfWeek,
      from: old.from,
      to: old.to,
    }));

    regions.forEach((region: GraphTimeRegionConfig, idx: number) => {
      const anno: AnnotationQuery<GrafanaQuery> = {
        datasource: {
          type: 'datasource',
          uid: 'grafana',
        },
        enable: true,
        hide: true, // don't show the toggle at the top of the dashboard
        filter: {
          exclude: false,
          ids: [angular.panel.id],
        },
        iconColor: region.fillColor ?? (region as any).color,
        name: `Time region for panel ${angular.panel.title}${idx > 0 ? ` ${idx}` : ''}`,
        target: {
          queryType: GrafanaQueryType.TimeRegions,
          refId: 'Anno',
          timeRegion: {
            fromDayOfWeek: region.fromDayOfWeek,
            toDayOfWeek: region.toDayOfWeek,
            from: region.from,
            to: region.to,
            timezone: 'utc', // graph panel was always UTC
          },
        },
      };

      if (region.fill) {
        annotations.push(anno);
      } else if (region.line) {
        anno.iconColor = region.lineColor ?? 'white';
        annotations.push(anno);
      }
    });
  }

  const tooltipConfig = angular.tooltip;
  if (tooltipConfig) {
    if (tooltipConfig.shared !== undefined) {
      options.tooltip.mode = tooltipConfig.shared ? TooltipDisplayMode.Multi : TooltipDisplayMode.Single;
    }

    if (tooltipConfig.sort !== undefined && tooltipConfig.shared) {
      switch (tooltipConfig.sort) {
        case 1:
          options.tooltip.sort = SortOrder.Ascending;
          break;
        case 2:
          options.tooltip.sort = SortOrder.Descending;
          break;
        default:
          options.tooltip.sort = SortOrder.None;
      }
    }
  }

  if (angular.thresholds && angular.thresholds.length > 0) {
    let steps: Threshold[] = [];
    let area = false;
    let line = false;

    const sorted = (angular.thresholds as AngularThreshold[]).sort((a, b) => (a.value > b.value ? 1 : -1));

    for (let idx = 0; idx < sorted.length; idx++) {
      const threshold = sorted[idx];
      const next = sorted.length > idx + 1 ? sorted[idx + 1] : null;

      if (threshold.fill) {
        area = true;
      }

      if (threshold.line) {
        line = true;
      }

      if (threshold.op === 'gt') {
        steps.push({
          value: threshold.value,
          color: getThresholdColor(threshold),
        });
      }

      if (threshold.op === 'lt') {
        if (steps.length === 0) {
          steps.push({
            value: -Infinity,
            color: getThresholdColor(threshold),
          });
        }

        // next op is gt and there is a gap set color to transparent
        if (next && next.op === 'gt' && next.value > threshold.value) {
          steps.push({
            value: threshold.value,
            color: 'transparent',
          });
          // if next is a lt we need to use its color
        } else if (next && next.op === 'lt') {
          steps.push({
            value: threshold.value,
            color: getThresholdColor(next),
          });
        } else {
          steps.push({
            value: threshold.value,
            color: 'transparent',
          });
        }
      }
    }

    // if now less then threshold add an -Infinity base that is transparent
    if (steps.length > 0 && steps[0].value !== -Infinity) {
      steps.unshift({
        color: 'transparent',
        value: -Infinity,
      });
    }

    let displayMode = area ? GraphThresholdsStyleMode.Area : GraphThresholdsStyleMode.Line;
    if (line && area) {
      displayMode = GraphThresholdsStyleMode.LineAndArea;
    }

    // TODO move into standard ThresholdConfig ?
    y1.custom.thresholdsStyle = { mode: displayMode };

    y1.thresholds = {
      mode: ThresholdsMode.Absolute,
      steps,
    };
  }

  if (angular.xaxis && angular.xaxis.show === false && angular.xaxis.mode === 'time') {
    overrides.push({
      matcher: {
        id: FieldMatcherID.byType,
        options: FieldType.time,
      },
      properties: [
        {
          id: 'custom.axisPlacement',
          value: AxisPlacement.Hidden,
        },
      ],
    });
  }
  return {
    fieldConfig: {
      defaults: omitBy(y1, isNil),
      overrides,
    },
    options,
    annotations,
  };
}

interface GraphTimeRegionConfig extends TimeRegionConfig {
  colorMode: string;
  fill: boolean;
  fillColor: string;
  line: boolean;
  lineColor: string;
}

function getThresholdColor(threshold: AngularThreshold): string {
  if (threshold.colorMode === 'critical') {
    return 'red';
  }

  if (threshold.colorMode === 'warning') {
    return 'orange';
  }

  if (threshold.colorMode === 'custom') {
    return threshold.fillColor || threshold.lineColor;
  }

  return 'red';
}

interface AngularThreshold {
  op: string;
  fill: boolean;
  line: boolean;
  value: number;
  colorMode: 'critical' | 'warning' | 'custom';
  yaxis?: 'left' | 'right';
  fillColor: string;
  lineColor: string;
}

// {
//   "label": "Y111",
//   "show": true,
//   "logBase": 10,
//   "min": "0",
//   "max": "1000",
//   "format": "areaMI2",
//   "$$hashKey": "object:19",
//   "decimals": 3
// },
function getFieldConfigFromOldAxis(obj: any): FieldConfig<GraphFieldConfig> {
  if (!obj) {
    return {};
  }
  const graph: GraphFieldConfig = {
    axisPlacement: obj.show ? AxisPlacement.Auto : AxisPlacement.Hidden,
  };
  if (obj.label) {
    graph.axisLabel = obj.label;
  }
  if (obj.logBase) {
    const log: number = obj.logBase;
    if (log === 2 || log === 10) {
      graph.scaleDistribution = {
        type: ScaleDistribution.Log,
        log,
      };
    }
  }
  return omitBy(
    {
      unit: obj.format,
      decimals: validNumber(obj.decimals),
      min: validNumber(obj.min),
      max: validNumber(obj.max),
      custom: graph,
    },
    isNil
  );
}

function fillY2DynamicValues(
  y1: FieldConfig<GraphFieldConfig>,
  y2: FieldConfig<GraphFieldConfig>,
  props: DynamicConfigValue[]
) {
  // The standard properties
  for (const [key, value] of Object.entries(y2)) {
    if (key !== 'custom' && value !== (y1 as any)[key]) {
      props.push({
        id: key,
        value,
      });
    }
  }

  props.push({
    id: `custom.axisPlacement`,
    value: AxisPlacement.Right,
  });

  // Add any custom property
  const y1G = y1.custom ?? {};
  const y2G = y2.custom ?? {};
  for (const [key, value] of Object.entries(y2G)) {
    if (value !== (y1G as any)[key]) {
      props.push({
        id: `custom.${key}`,
        value,
      });
    }
  }
}

function validNumber(val: unknown): number | undefined {
  if (isNumber(val)) {
    return val;
  }
  if (isString(val)) {
    const num = Number(val);
    if (!isNaN(num)) {
      return num;
    }
  }
  return undefined;
}

function getReducersFromLegend(obj: Record<string, unknown>): string[] {
  const ids: string[] = [];
  for (const key in obj) {
    const r = fieldReducers.getIfExists(key);
    if (r) {
      ids.push(r.id);
    }
  }
  return ids;
}

function migrateHideFrom(panel: {
  fieldConfig?: { defaults?: { custom?: { hideFrom?: any } }; overrides: ConfigOverrideRule[] };
}) {
  if (panel.fieldConfig?.defaults?.custom?.hideFrom?.graph !== undefined) {
    panel.fieldConfig.defaults.custom.hideFrom.viz = panel.fieldConfig.defaults.custom.hideFrom.graph;
    delete panel.fieldConfig.defaults.custom.hideFrom.graph;
  }
  if (panel.fieldConfig?.overrides) {
    panel.fieldConfig.overrides = panel.fieldConfig.overrides.map((fr) => {
      fr.properties = fr.properties.map((p) => {
        if (p.id === 'custom.hideFrom' && p.value.graph) {
          p.value.viz = p.value.graph;
          delete p.value.graph;
        }
        return p;
      });
      return fr;
    });
  }
}

function getLegendHideFromOverride(reducer: ReducerID.allIsZero | ReducerID.allIsNull) {
  return {
    matcher: {
      id: FieldMatcherID.byValue,
      options: {
        reducer: reducer,
        op: ComparisonOperation.GTE,
        value: 0,
      },
    },
    properties: [
      {
        id: 'custom.hideFrom',
        value: {
          tooltip: true,
          viz: false,
          legend: true,
        },
      },
    ],
  };
}

function getStackingFromOverrides(value: Boolean | string) {
  const defaultGroupName = defaultGraphConfig.stacking?.group;
  return {
    mode: value ? StackingMode.Normal : StackingMode.None,
    group: isString(value) ? value : defaultGroupName,
  };
}

function addAnnotationsToDashboard(annotations: AnnotationQuery[]) {
  const scene = window.__grafanaSceneContext;

  if (scene instanceof DashboardScene) {
    const dataLayers = dashboardSceneGraph.getDataLayers(scene);
    const annotationLayers = [...dataLayers.state.annotationLayers];

    for (let annotation of annotations) {
      const newAnnotation = new DashboardAnnotationsDataLayer({
        key: `annotations-${annotation.name}`,
        query: annotation,
        name: annotation.name,
        isEnabled: annotation.enable,
        isHidden: annotation.hide,
      });

      annotationLayers.push(newAnnotation);
    }

    dataLayers.setState({ annotationLayers });
    return;
  }

  const dashboard = getDashboardSrv().getCurrent();
  if (dashboard) {
    dashboard.annotations.list = [...dashboard.annotations.list, ...annotations];

    // Trigger a full dashboard refresh when annotations change
    if (dashboardRefreshDebouncer == null) {
      dashboardRefreshDebouncer = setTimeout(() => {
        dashboardRefreshDebouncer = null;
        getTimeSrv().refreshTimeModel();
      });
    }
  }
}
