import { css, cx } from '@emotion/css';
import { useMemo } from 'react';

import { getTimeZoneInfo, GrafanaTheme2, InternalTimeZones, TIME_FORMAT } from '@grafana/data';
import { convertRawToRange } from '@grafana/data/src/datetime/rangeutil';
import { config } from '@grafana/runtime';
import {
  SceneComponentProps,
  SceneObjectBase,
  SceneObjectState,
  SceneObjectStateChangedEvent,
  SceneObjectUrlValue,
  SceneObjectUrlValues,
  SceneTimeRange,
  sceneUtils,
  SceneVariableValueChangedEvent,
} from '@grafana/scenes';
import { Stack, Tooltip, useStyles2 } from '@grafana/ui';
import { appEvents } from 'app/core/app_events';
import { RecordHistoryEntryEvent } from 'app/types/events';

import { DataTrail, DataTrailState, getTopSceneFor } from './DataTrail';
import { SerializedTrailHistory } from './TrailStore/TrailStore';
import { reportExploreMetrics } from './interactions';
import { VAR_FILTERS, VAR_OTEL_DEPLOYMENT_ENV, VAR_OTEL_RESOURCES } from './shared';
import { getTrailFor, isSceneTimeRangeState } from './utils';

export interface DataTrailsHistoryState extends SceneObjectState {
  currentStep: number;
  steps: DataTrailHistoryStep[];
  filtersApplied: string[];
  otelResources: string[];
  otelDepEnvs: string[];
}

export function isDataTrailsHistoryState(state: SceneObjectState): state is DataTrailsHistoryState {
  return 'currentStep' in state && 'steps' in state;
}

export function isDataTrailHistoryFilter(filter?: SceneObjectUrlValue): filter is string[] {
  return !!filter;
}

const isString = (value: unknown): value is string => typeof value === 'string';

export interface DataTrailHistoryStep {
  description: string;
  detail: string;
  type: TrailStepType;
  trailState: DataTrailState;
  parentIndex: number;
}

export type TrailStepType = 'filters' | 'time' | 'metric' | 'start' | 'metric_page' | 'dep_env' | 'resource';

const filterSubst = ` $2 `;
const filterPipeRegex = /(\|)(=|=~|!=|>|<|!~)(\|)/g;
const stepDescriptionMap: Record<TrailStepType, string> = {
  start: 'Start of history',
  metric: 'Metric selected:',
  metric_page: 'Metric select page',
  filters: 'Filter applied:',
  time: 'Time range changed:',
  dep_env: 'Deployment environment selected:',
  resource: 'Resource attribute selected:',
};

export class DataTrailHistory extends SceneObjectBase<DataTrailsHistoryState> {
  public constructor(state: Partial<DataTrailsHistoryState>) {
    super({
      steps: state.steps ?? [],
      currentStep: state.currentStep ?? 0,
      filtersApplied: [],
      otelResources: [],
      otelDepEnvs: [],
    });

    this.addActivationHandler(this._onActivate.bind(this));
  }

  private stepTransitionInProgress = false;

  public _onActivate() {
    const trail = getTrailFor(this);

    if (this.state.steps.length === 0) {
      // We always want to ensure in initial 'start' step
      this.addTrailStep(trail, 'start');

      if (trail.state.metric) {
        // But if our current trail has a metric, we want to remove it and the topScene,
        // so that the "start" step always displays a metric select screen.

        // So we remove the metric and update the topscene for the "start" step
        const { metric, ...startState } = trail.state;
        startState.topScene = getTopSceneFor(undefined);
        this.state.steps[0].trailState = startState;

        // But must add a secondary step to represent the selection of the metric
        // for this restored trail state
        this.addTrailStep(trail, 'metric', trail.state.metric);
      } else {
        this.addTrailStep(trail, 'metric_page');
      }
    }

    trail.subscribeToState((newState, oldState) => {
      if (newState.metric !== oldState.metric) {
        if (this.state.steps.length === 1) {
          // For the first step we want to update the starting state so that it contains data
          this.state.steps[0].trailState = sceneUtils.cloneSceneObjectState(oldState, { history: this });
        }

        if (!newState.metric) {
          this.addTrailStep(trail, 'metric_page');
        } else {
          this.addTrailStep(trail, 'metric', newState.metric);
        }
      }
    });

    trail.subscribeToEvent(SceneVariableValueChangedEvent, (evt) => {
      if (evt.payload.state.name === VAR_FILTERS) {
        const filtersApplied = this.state.filtersApplied;
        const urlState = sceneUtils.getUrlState(trail);
        this.addTrailStep(trail, 'filters', parseFilterTooltip(urlState, filtersApplied));
        this.setState({ filtersApplied });
      }

      // TEST THE MIGRATION OF REMOVING THE VAR_OTEL_DEPLOYMENT_ENV
      if (evt.payload.state.name === VAR_OTEL_DEPLOYMENT_ENV) {
        const otelDepEnvs = this.state.otelDepEnvs;
        const urlState = sceneUtils.getUrlState(trail);
        this.addTrailStep(trail, 'dep_env', parseDepEnvTooltip(urlState, otelDepEnvs));
        this.setState({ otelDepEnvs });
      }

      if (evt.payload.state.name === VAR_OTEL_RESOURCES) {
        const otelResources = this.state.otelResources;
        const urlState = sceneUtils.getUrlState(trail);
        this.addTrailStep(trail, 'resource', parseOtelResourcesTooltip(urlState, otelResources));
        this.setState({ otelResources });
      }
    });

    trail.subscribeToEvent(SceneObjectStateChangedEvent, (evt) => {
      if (evt.payload.changedObject instanceof SceneTimeRange) {
        const { prevState, newState } = evt.payload;

        if (isSceneTimeRangeState(prevState) && isSceneTimeRangeState(newState)) {
          if (prevState.from === newState.from && prevState.to === newState.to) {
            return;
          }

          const tooltip = parseTimeTooltip({
            from: newState.from,
            to: newState.to,
            timeZone: newState.timeZone,
          });

          this.addTrailStep(trail, 'time', tooltip);

          if (config.featureToggles.unifiedHistory) {
            appEvents.publish(
              new RecordHistoryEntryEvent({
                name: 'Time range changed',
                description: tooltip,
                url: window.location.href,
                time: Date.now(),
              })
            );
          }
        }
      }
    });
  }

  public addTrailStep(trail: DataTrail, type: TrailStepType, detail = '') {
    if (this.stepTransitionInProgress) {
      // Do not add trail steps when step transition is in progress
      return;
    }

    const stepIndex = this.state.steps.length;
    const parentIndex = type === 'start' ? -1 : this.state.currentStep;

    this.setState({
      currentStep: stepIndex,
      steps: [
        ...this.state.steps,
        {
          type,
          detail,
          description: stepDescriptionMap[type],
          trailState: sceneUtils.cloneSceneObjectState(trail.state, { history: this }),
          parentIndex,
        },
      ],
    });
  }

  public addTrailStepFromStorage(trail: DataTrail, step: SerializedTrailHistory) {
    if (this.stepTransitionInProgress) {
      // Do not add trail steps when step transition is in progress
      return;
    }

    const type = step.type;
    const stepIndex = this.state.steps.length;
    const parentIndex = type === 'start' ? -1 : this.state.currentStep;
    const filtersApplied = this.state.filtersApplied;
    const otelResources = this.state.otelResources;
    const otelDepEnvs = this.state.otelDepEnvs;
    let detail = '';

    switch (step.type) {
      case 'metric':
        detail = step.urlValues.metric?.toString() ?? '';
        break;
      case 'filters':
        detail = parseFilterTooltip(step.urlValues, filtersApplied);
        break;
      case 'time':
        detail = parseTimeTooltip(step.urlValues);
        break;
      case 'dep_env':
        detail = parseDepEnvTooltip(step.urlValues, otelDepEnvs);
      case 'resource':
        detail = parseOtelResourcesTooltip(step.urlValues, otelResources);
    }

    this.setState({
      filtersApplied,
      otelDepEnvs,
      otelResources,
      currentStep: stepIndex,
      steps: [
        ...this.state.steps,
        {
          type,
          detail,
          description: stepDescriptionMap[type],
          trailState: sceneUtils.cloneSceneObjectState(trail.state, { history: this }),
          parentIndex,
        },
      ],
    });
  }

  public goBackToStep(stepIndex: number) {
    if (stepIndex === this.state.currentStep) {
      return;
    }

    const step = this.state.steps[stepIndex];
    const type = step.type === 'metric' && step.trailState.metric === undefined ? 'metric-clear' : step.type;

    reportExploreMetrics('history_step_clicked', { type, step: stepIndex, numberOfSteps: this.state.steps.length });

    this.stepTransitionInProgress = true;
    this.setState({ currentStep: stepIndex });

    getTrailFor(this).restoreFromHistoryStep(step.trailState);

    // The URL will update
    this.stepTransitionInProgress = false;
  }

  renderStepTooltip(step: DataTrailHistoryStep) {
    return (
      <Stack direction="column">
        <div>{step.description}</div>
        {step.detail !== '' && <div>{step.detail}</div>}
      </Stack>
    );
  }

  public static Component = ({ model }: SceneComponentProps<DataTrailHistory>) => {
    const { steps, currentStep } = model.useState();
    const styles = useStyles2(getStyles);

    const { ancestry, alternatePredecessorStyle } = useMemo(() => {
      const ancestry = new Set<number>();

      let cursor = currentStep;
      while (cursor >= 0) {
        const step = steps[cursor];
        if (!step) {
          break;
        }
        ancestry.add(cursor);
        cursor = step.parentIndex;
      }

      const alternatePredecessorStyle = new Map<number, string>();

      ancestry.forEach((index) => {
        const parent = steps[index].parentIndex;
        if (parent + 1 !== index) {
          alternatePredecessorStyle.set(index, createAlternatePredecessorStyle(index, parent));
        }
      });

      return { ancestry, alternatePredecessorStyle };
    }, [currentStep, steps]);

    return (
      <div className={styles.container}>
        <div className={styles.heading}>History</div>
        {steps.map((step, index) => {
          let stepType = step.type;

          if (stepType === 'metric' && step.trailState.metric === undefined) {
            // If we're resetting the metric, we want it to look like a start node
            stepType = 'start';
          }

          return (
            <Tooltip content={() => model.renderStepTooltip(step)} key={index}>
              <button
                className={cx(
                  // Base for all steps
                  styles.step,
                  // Specifics per step type
                  styles.stepTypes[stepType],
                  // To highlight selected step
                  currentStep === index ? styles.stepSelected : '',
                  // To alter the look of steps with distant non-directly preceding parent
                  alternatePredecessorStyle.get(index) ?? '',
                  // To remove direct link for steps that don't have a direct parent
                  index !== step.parentIndex + 1 ? styles.stepOmitsDirectLeftLink : '',
                  // To remove the direct parent link on the start node as well
                  index === 0 ? styles.stepOmitsDirectLeftLink : '',
                  // To darken steps that aren't the current step's ancesters
                  !ancestry.has(index) ? styles.stepIsNotAncestorOfCurrent : ''
                )}
                onClick={() => model.goBackToStep(index)}
              ></button>
            </Tooltip>
          );
        })}
      </div>
    );
  };
}

export function parseTimeTooltip(urlValues: SceneObjectUrlValues): string {
  if (!isSceneTimeRangeState(urlValues)) {
    return '';
  }

  const range = convertRawToRange({
    from: urlValues.from,
    to: urlValues.to,
  });

  const zone = isString(urlValues.timeZone) ? urlValues.timeZone : InternalTimeZones.localBrowserTime;
  const tzInfo = getTimeZoneInfo(zone, Date.now());

  const from = range.from.subtract(tzInfo?.offsetInMins ?? 0, 'minute').format(TIME_FORMAT);
  const to = range.to.subtract(tzInfo?.offsetInMins ?? 0, 'minute').format(TIME_FORMAT);

  return `${from} - ${to}`;
}

export function parseFilterTooltip(urlValues: SceneObjectUrlValues, filtersApplied: string[]): string {
  let detail = '';
  const varFilters = urlValues['var-filters'];
  if (isDataTrailHistoryFilter(varFilters)) {
    detail =
      varFilters.filter((f) => {
        if (f !== '' && !filtersApplied.includes(f)) {
          filtersApplied.push(f);
          return true;
        }
        return false;
      })[0] ?? '';
  }
  // filters saved as key|operator|value
  // we need to remove pipes (|)
  return detail.replace(filterPipeRegex, filterSubst);
}

export function parseOtelResourcesTooltip(urlValues: SceneObjectUrlValues, otelResources: string[]): string {
  let detail = '';
  const varOtelResources = urlValues['var-otel_resources'];
  if (isDataTrailHistoryFilter(varOtelResources)) {
    detail =
      varOtelResources.filter((f) => {
        if (f !== '' && !otelResources.includes(f)) {
          otelResources.push(f);
          return true;
        }
        return false;
      })[0] ?? '';
  }
  // filters saved as key|operator|value
  // we need to remove pipes (|)
  return detail.replace(filterPipeRegex, filterSubst);
}

export function parseDepEnvTooltip(urlValues: SceneObjectUrlValues, otelDepEnvs: string[]): string {
  let detail = '';
  const varDepEnv = urlValues['var-deployment_environment'];

  if (typeof varDepEnv === 'string') {
    return varDepEnv;
  }

  if (isDataTrailHistoryFilter(varDepEnv)) {
    detail =
      varDepEnv?.filter((f) => {
        if (f !== '' && !otelDepEnvs.includes(f)) {
          otelDepEnvs.push(f);
          return true;
        }
        return false;
      })[0] ?? '';
  }

  return detail;
}

function getStyles(theme: GrafanaTheme2) {
  const visTheme = theme.visualization;

  return {
    container: css({
      display: 'flex',
      gap: 10,
      alignItems: 'center',
    }),
    heading: css({}),
    step: css({
      flexGrow: 0,
      cursor: 'pointer',
      border: 'none',
      boxShadow: 'none',
      padding: 0,
      margin: 0,
      width: 8,
      height: 8,
      opacity: 0.7,
      borderRadius: theme.shape.radius.circle,
      background: theme.colors.primary.main,
      position: 'relative',
      '&:hover': {
        opacity: 1,
      },
      '&:hover:before': {
        // We only want the node to hover, not its connection to its parent
        opacity: 0.7,
      },
      '&:before': {
        content: '""',
        position: 'absolute',
        width: 10,
        height: 2,
        left: -10,
        top: 3,
        background: theme.colors.primary.border,
        pointerEvents: 'none',
      },
    }),
    stepSelected: css({
      '&:after': {
        content: '""',
        borderStyle: `solid`,
        borderWidth: 2,
        borderRadius: '50%',
        position: 'absolute',
        width: 16,
        height: 16,
        left: -4,
        top: -4,
        boxShadow: `0px 0px 0px 2px inset ${theme.colors.background.canvas}`,
      },
    }),
    stepOmitsDirectLeftLink: css({
      '&:before': {
        background: 'none',
      },
    }),
    stepIsNotAncestorOfCurrent: css({
      opacity: 0.2,
      '&:hover:before': {
        opacity: 0.2,
      },
    }),
    stepTypes: {
      start: generateStepTypeStyle(visTheme.getColorByName('green')),
      filters: generateStepTypeStyle(visTheme.getColorByName('purple')),
      metric: generateStepTypeStyle(visTheme.getColorByName('orange')),
      metric_page: generateStepTypeStyle(visTheme.getColorByName('orange')),
      time: generateStepTypeStyle(theme.colors.primary.main),
      resource: generateStepTypeStyle(visTheme.getColorByName('purple')),
      dep_env: generateStepTypeStyle(visTheme.getColorByName('purple')),
    },
  };
}

function generateStepTypeStyle(color: string) {
  return css({
    background: color,
    '&:before': {
      background: color,
      borderColor: color,
    },
    '&:after': {
      borderColor: color,
    },
  });
}

function createAlternatePredecessorStyle(index: number, parent: number) {
  const difference = index - parent;

  const NODE_DISTANCE = 18;
  const distanceToParent = difference * NODE_DISTANCE;

  return css({
    '&:before': {
      content: '""',
      width: distanceToParent + 2,
      height: 10,
      borderStyle: 'solid',
      borderWidth: 2,
      borderBottom: 'none',
      borderTopLeftRadius: 8,
      borderTopRightRadius: 8,
      top: -10,
      left: 3 - distanceToParent,
      background: 'none',
    },
  });
}
