// Copyright 2022 The Parca Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {useEffect, useMemo, useRef} from 'react';

import {createParser, useQueryState} from 'nuqs';

import {LabelSet, QueryRequest_ReportType, QueryServiceClient} from '@parca/client';
import {Button, useParcaContext} from '@parca/components';
import {Matcher, MatcherTypes, ProfileType, Query} from '@parca/parser';
import {TimeUnits, formatDate, formatDuration} from '@parca/utilities';

import ProfileFlameGraph, {validateFlameChartQuery} from '../ProfileFlameGraph';
import {boundsFromProfileSource} from '../ProfileFlameGraph/FlameGraphArrow/utils';
import {
  MergedProfileSource,
  ProfileSource,
  isMergedProfileSource,
  timeFormat,
} from '../ProfileSource';
import {useProfileFilters} from '../ProfileView/components/ProfileFilters/useProfileFilters';
import type {SamplesData} from '../ProfileView/types/visualization';
import {flamechartDimensionParser} from '../hooks/urlParsers';
import {useQuery} from '../useQuery';
import {NumberDuo} from '../utils';
import {SamplesStrip} from './SamplesStrips';

interface SelectedTimeframe {
  labels: LabelSet;
  bounds: NumberDuo;
}

const timeframeParser = createParser<SelectedTimeframe>({
  parse: (value: string) => {
    try {
      const [labelPart, boundsPart] = value.split('|');
      if (labelPart != null && boundsPart != null) {
        const labels = labelPart.split(',').map(labelStr => {
          const [name, ...rest] = labelStr.split(':');
          return {name, value: rest.join(':')};
        });
        const [startMs, endMs] = boundsPart.split(',').map(Number);
        if (labels.length > 0 && !isNaN(startMs) && !isNaN(endMs)) {
          return {
            labels: {labels},
            bounds: [startMs, endMs] as NumberDuo,
          };
        }
      }
    } catch {
      // Ignore parsing errors
    }
    return null;
  },
  serialize: (value: SelectedTimeframe) => {
    const labelsStr = value.labels.labels.map(l => `${l.name}:${l.value}`).join(',');
    return `${labelsStr}|${value.bounds[0]},${value.bounds[1]}`;
  },
}).withOptions({history: 'replace'});

interface ProfileFlameChartProps {
  samplesData?: SamplesData;
  queryClient: QueryServiceClient;
  profileSource: ProfileSource;
  width: number;
  total: bigint;
  filtered: bigint;
  profileType?: ProfileType;
  isHalfScreen: boolean;
  metadataMappingFiles?: string[];
  metadataLoading?: boolean;
  onSwitchToFifteenMinutes?: () => void;
}

// Helper to create a filtered profile source with narrowed time bounds
// and dimension label matchers from the selected strip.
const createFilteredProfileSource = (
  profileSource: ProfileSource,
  selectedTimeframe: {labels: LabelSet; bounds: NumberDuo}
): ProfileSource | null => {
  if (!isMergedProfileSource(profileSource)) {
    return null;
  }

  // The bounds are in milliseconds, convert to nanoseconds for the profile source
  // Round to integers since BigInt requires integer values
  const mergeFrom = BigInt(Math.round(selectedTimeframe.bounds[0])) * 1_000_000n;
  const mergeTo = BigInt(Math.round(selectedTimeframe.bounds[1])) * 1_000_000n;

  // Add dimension labels as additional matchers to the query
  const dimensionMatchers = selectedTimeframe.labels.labels.map(
    l => new Matcher(l.name, MatcherTypes.MatchEqual, l.value)
  );

  const query = new Query(
    profileSource.query.profType,
    [...profileSource.query.matchers, ...dimensionMatchers],
    ''
  );

  return new MergedProfileSource(mergeFrom, mergeTo, query);
};

export const ProfileFlameChart = ({
  samplesData,
  queryClient,
  profileSource,
  width,
  total,
  filtered,
  profileType,
  isHalfScreen,
  metadataMappingFiles,
  metadataLoading,
  onSwitchToFifteenMinutes,
}: ProfileFlameChartProps): JSX.Element => {
  const {enableFlamechartFiltering} = useParcaContext();
  const {protoFilters} = useProfileFilters();
  const zoomControlsRef = useRef<HTMLDivElement>(null);

  const [selectedTimeframe, setSelectedTimeframe] = useQueryState(
    'flamechart_timeframe',
    timeframeParser
  );

  // Read flamechart dimension from URL state to detect changes
  const [flamechartDimension] = useQueryState(
    'flamechart_dimension',
    flamechartDimensionParser.withDefault([])
  );

  // Reset selection when the parent time range (profileSource) changes
  const timeBoundsKey = boundsFromProfileSource(profileSource).join(',');
  const prevTimeBoundsKey = useRef(timeBoundsKey);
  useEffect(() => {
    if (prevTimeBoundsKey.current !== timeBoundsKey) {
      prevTimeBoundsKey.current = timeBoundsKey;
      void setSelectedTimeframe(null);
    }
  }, [timeBoundsKey, setSelectedTimeframe]);

  // Reset selection when the dimension changes
  const dimensionKey = (flamechartDimension ?? []).join(',');
  const prevDimensionKey = useRef(dimensionKey);
  useEffect(() => {
    if (prevDimensionKey.current !== dimensionKey) {
      prevDimensionKey.current = dimensionKey;
      void setSelectedTimeframe(null);
    }
  }, [dimensionKey, setSelectedTimeframe]);

  // Handle timeframe selection from strips
  const handleSelectedTimeframe = (labels: LabelSet, bounds: NumberDuo | undefined): void => {
    if (bounds === undefined) {
      void setSelectedTimeframe(null);
    } else {
      void setSelectedTimeframe({labels, bounds});
    }
  };

  // Create filtered profile source when selection exists
  const filteredProfileSource = useMemo(() => {
    if (selectedTimeframe == null) return null;
    return createFilteredProfileSource(profileSource, selectedTimeframe);
  }, [profileSource, selectedTimeframe]);

  // Query flamechart data only when a strip selection exists
  const {
    isLoading: flamechartLoading,
    response: flamechartResponse,
    error: flamechartError,
  } = useQuery(
    queryClient,
    filteredProfileSource ?? profileSource,
    QueryRequest_ReportType.FLAMECHART,
    {
      skip: selectedTimeframe == null || filteredProfileSource == null,
      ...(enableFlamechartFiltering === true ? {protoFilters} : {}),
    }
  );

  const flamechartArrow =
    flamechartResponse?.report.oneofKind === 'flamegraphArrow'
      ? flamechartResponse.report.flamegraphArrow
      : undefined;
  const flamechartTotal = flamechartResponse != null ? BigInt(flamechartResponse.total) : total;
  const flamechartFiltered =
    flamechartResponse != null ? BigInt(flamechartResponse.filtered) : filtered;

  // Get time bounds from profile source for the strips
  const timeBounds = boundsFromProfileSource(profileSource);

  // Transform samples data for SamplesStrip component
  const stripsData = useMemo(() => {
    if (samplesData?.series == null) return {cpus: [], data: [], stepMs: 0};

    const cpus = samplesData.series.map(s => s.labelset);
    const data = samplesData.series.map(s => s.data);

    const stepMs = samplesData.stepMs ?? 0;

    return {cpus, data, stepMs};
  }, [samplesData?.series, samplesData?.stepMs]);

  const {isValid, isNonDelta, isDurationTooLong} = validateFlameChartQuery(
    profileSource as MergedProfileSource
  );

  if (!isValid) {
    if (isDurationTooLong) {
      return (
        <div className="flex flex-col justify-center items-center p-10 text-center gap-4 text-sm">
          <span>
            Flame chart is unavailable for queries longer than 15 minutes. Try reducing the time
            range to 15 minutes or selecting a point in the metrics graph.
          </span>
          {onSwitchToFifteenMinutes != null && (
            <Button variant="primary" onClick={onSwitchToFifteenMinutes}>
              Switch to last 15 minutes
            </Button>
          )}
        </div>
      );
    }

    const message = isNonDelta
      ? 'To use the Flame chart, please switch to a Delta profile.'
      : 'Flame chart is unavailable for this query.';
    return (
      <div className="flex flex-col justify-center p-10 text-center gap-6 text-sm">{message}</div>
    );
  }

  const hasDimension = (flamechartDimension ?? []).length > 0;

  const isStripsLoading =
    metadataLoading === true || !hasDimension || samplesData?.loading === true;

  if (!hasDimension && metadataLoading !== true) {
    return (
      <div className="flex justify-center items-center py-10 text-gray-500 dark:text-gray-400 text-sm">
        Select a label in the &quot;Samples group by&quot; dropdown above to view the samples
        strips.
      </div>
    );
  }

  return (
    <div>
      {(isStripsLoading || (stripsData.cpus.length > 0 && stripsData.data.length > 0)) && (
        <div className="mb-2">
          <SamplesStrip
            loading={isStripsLoading}
            cpus={stripsData.cpus}
            data={stripsData.data}
            selectedTimeframe={selectedTimeframe}
            onSelectedTimeframe={handleSelectedTimeframe}
            width={width}
            bounds={[Number(timeBounds[0] / 1_000_000n), Number(timeBounds[1] / 1_000_000n)]}
            stepMs={stripsData.stepMs}
          />
        </div>
      )}

      {/* Selected timeframe description + zoom controls */}
      {selectedTimeframe != null &&
        (() => {
          const labels = selectedTimeframe.labels.labels
            .map(l => `${l.name} = ${l.value}`)
            .join(', ');
          const durationMs = selectedTimeframe.bounds[1] - selectedTimeframe.bounds[0];
          const duration =
            durationMs < 5000
              ? `${(durationMs / 1000).toFixed(1)}s`
              : formatDuration({[TimeUnits.Milliseconds]: durationMs});
          const fmt = durationMs < 5000 ? "yyyy-MM-dd HH:mm:ss.SSS '(UTC)'" : timeFormat();
          return (
            <div className="flex items-center justify-between px-2 py-1">
              <div className="text-xs font-medium text-gray-500 dark:text-gray-400">
                Samples matching {labels} over {duration} from{' '}
                {formatDate(new Date(selectedTimeframe.bounds[0]), fmt)} to{' '}
                {formatDate(new Date(selectedTimeframe.bounds[1]), fmt)}
              </div>
              <div ref={zoomControlsRef} />
            </div>
          );
        })()}

      {/* Flamegraph visualization - only shown when a time range is selected in the strips */}
      {selectedTimeframe != null && filteredProfileSource != null ? (
        <ProfileFlameGraph
          arrow={flamechartArrow}
          loading={flamechartLoading}
          error={flamechartError}
          profileSource={filteredProfileSource}
          width={width}
          total={flamechartTotal}
          filtered={flamechartFiltered}
          profileType={profileType}
          isHalfScreen={isHalfScreen}
          metadataMappingFiles={metadataMappingFiles}
          metadataLoading={metadataLoading}
          isFlameChart={true}
          curPathArrow={[]}
          setNewCurPathArrow={() => {}}
          zoomControlsRef={zoomControlsRef}
        />
      ) : (
        <div className="flex justify-center items-center py-10 text-gray-500 dark:text-gray-400 text-sm">
          Select a time range in the samples strips above to view the flamechart.
        </div>
      )}
    </div>
  );
};

export default ProfileFlameChart;
