import { getValueFormat } from '@baurine/grafana-value-formats'
import { Position, SettingsProps, TooltipStickTo, TooltipType } from '@elastic/charts'
import { useChartColors, useChartThemeConfig } from '@tidb-cloud-uikit/theme/Chart'
import { Box, Card, CardProps, Group, Stack, Typography } from '@tidbcloud/uikit'
import { LabelTooltip } from '@tidbcloud/uikit/biz'
import dayjs from 'dayjs'
import { MetricsChart, TimeRangeValue } from 'metrics-chart'
import React, { memo, useCallback, useEffect, useImperativeHandle, useMemo, useState, forwardRef } from 'react'

import { log } from 'common/log'
import { useTimeFormatter } from 'dbaas/components/TimeComponent'
import { LoadingOverlay } from 'dbaas/screens/NextGen/premium/shared/diagnosis/LoadingOverlay'
import {
  CircuitBreakerConfig,
  useCircuitBreaker,
  pauseCircuitBreaker as pauseCircuitBreakerUtil,
  resumeCircuitBreaker as resumeCircuitBreakerUtil
} from 'dbaas/utils/circuit-breaker'
import { API_THROTTLED_ERROR_MSG } from 'dbaas/utils/errors'

import { getMetricData, MetricInfo, MetricRequestOptions } from './helpers'

const METRICS_CHART_RENDER_COMMIT_DELAY_MS = 16
const DEFAULT_CHART_SETTING: SettingsProps = {}

export interface ChartCardProps {
  title: string
  label?: string
  sourceURI: string
  timeRange: [number, number]
  timeFormat?: string
  metric: MetricInfo
  metricRequestOptions?: MetricRequestOptions
  height?: number
  chartSetting?: SettingsProps
  fixMinInterval?: boolean
  onLoading?: (loading: boolean) => void
  cardProps?: Omit<CardProps, 'children'>
  pendingOverlay?: React.ReactNode
  initialLoading?: boolean
  enableCircuitBreaker?: boolean
  onThrottleOpen?: () => void
  onRecoverySuccess?: (isForceRefresh: boolean) => void
  subTitle?: React.ReactNode
}

export interface ChartCardRef {
  pauseCircuitBreaker: () => void
  resumeCircuitBreaker: () => void
}

export function niceTimeFormatByDay(range: TimeRangeValue) {
  const minDate = dayjs(range[0] * 1000)
  const maxDate = dayjs(range[1] * 1000)
  const mins = maxDate.diff(minDate, 'minutes')

  if (mins > 5 * 60 * 24) return 'MM-DD'
  if (mins > 1 * 60 * 24) return 'MM-DD HH:mm'
  if (mins > 5) return 'HH:mm'
  return 'HH:mm:ss'
}

const ERROR_THRESHOLD = 50
const RECOVERY_MS = 1 * 60 * 1000 // 1 minute

interface FetchPromeData {
  (params: { startTimeSec: number; endTimeSec: number; stepSec: number; query: string }): Promise<any>
}

const ChartCard = memo(
  forwardRef<ChartCardRef, ChartCardProps>(
    (
      {
        title,
        label,
        sourceURI,
        timeRange,
        metric,
        metricRequestOptions,
        onLoading,
        height = 192,
        chartSetting = DEFAULT_CHART_SETTING,
        fixMinInterval = true,
        timeFormat = 'YYYY-MM-DD HH:mm:ss',
        cardProps,
        initialLoading,
        pendingOverlay,
        enableCircuitBreaker = false,
        onThrottleOpen,
        onRecoverySuccess,
        subTitle
      },
      ref
    ) => {
      const [subTitleValue, setSubTitleValue] = useState<string | null>(null)

      const startTime = timeRange[0]
      const endTime = timeRange[1]
      const subTitleQuery = metric.subTitleQuery
      const subTitleFormat = metric.subTitleFormat
      const metricUnit = metric.unit

      // Fetch subTitle summary value when metric.subTitleQuery is provided
      useEffect(() => {
        if (!subTitleQuery || !sourceURI || initialLoading || pendingOverlay) {
          setSubTitleValue(null)
          return
        }

        const fetchSubTitle = async () => {
          try {
            // Use step = entire time range to get a single aggregated value
            const stepSec = endTime - startTime
            // Replace $__range with the actual time range duration (e.g., "1800s" for 30 minutes)
            const promql = subTitleQuery.replace(/\$__range/g, `${stepSec}s`)
            const res = await getMetricData({
              sourceURI,
              startTime,
              endTime,
              timeStep: stepSec,
              promql,
              requestOptions: metricRequestOptions
            })

            // Extract value from Prometheus response
            // query_range returns multiple values, we need the last one (at end time)
            const result = res.data?.data?.result?.[0]
            const values = result?.values
            const value = values?.length ? values[values.length - 1]?.[1] : result?.value?.[1]

            if (value !== undefined && value !== null) {
              const numValue = parseFloat(value)
              if (!isNaN(numValue)) {
                // Format using custom formatter or default unit formatter
                const formatted = subTitleFormat ? subTitleFormat(numValue) : getValueFormat(metricUnit)(numValue, 0, 2)
                setSubTitleValue(formatted)
              } else {
                setSubTitleValue(null)
              }
            } else {
              setSubTitleValue(null)
            }
          } catch (error) {
            log.api.error('Failed to fetch subTitle metric', error as Error)
            setSubTitleValue(null)
          }
        }

        fetchSubTitle()
      }, [
        sourceURI,
        startTime,
        endTime,
        subTitleQuery,
        subTitleFormat,
        metricUnit,
        initialLoading,
        pendingOverlay,
        metricRequestOptions
      ])

      const originalFetchPromeData = useCallback(
        ((params) => {
          if (!sourceURI) {
            return Promise.resolve({
              data: []
            })
          }
          const { startTimeSec, endTimeSec, stepSec, query } = params
          return getMetricData({
            sourceURI,
            startTime: startTimeSec,
            endTime: endTimeSec,
            timeStep: stepSec,
            promql: query,
            requestOptions: metricRequestOptions
          }).then((res) => res.data)
        }) as FetchPromeData,
        [metricRequestOptions, sourceURI]
      )

      const circuitBreakerConfig = useMemo<CircuitBreakerConfig>(
        () => ({
          key: sourceURI,
          errorThreshold: ERROR_THRESHOLD,
          recoveryMs: RECOVERY_MS,
          enabled: enableCircuitBreaker,
          onThrottleOpen: () => {
            log.api.error(API_THROTTLED_ERROR_MSG, new Error('Circuit breaker opened'))
            onThrottleOpen?.()
          },
          onRecoverySuccess
        }),
        [sourceURI, enableCircuitBreaker, onThrottleOpen, onRecoverySuccess]
      )
      const [fetchPromeData, clearCircuitBreaker] = useCircuitBreaker(originalFetchPromeData, circuitBreakerConfig)

      useEffect(() => {
        return () => {
          clearCircuitBreaker()
        }
      }, [clearCircuitBreaker])

      useImperativeHandle(
        ref,
        () => ({
          pauseCircuitBreaker: () => {
            pauseCircuitBreakerUtil(sourceURI)
          },
          resumeCircuitBreaker: () => {
            resumeCircuitBreakerUtil(sourceURI)
          }
        }),
        [sourceURI]
      )

      // Determine which subTitle to display: auto-fetched value or passed prop
      const displaySubTitle = useMemo(
        () =>
          metric.subTitleQuery ? (
            subTitleValue ? (
              <Typography variant="title-lg">{subTitleValue}</Typography>
            ) : null
          ) : (
            subTitle
          ),
        [metric.subTitleQuery, subTitle, subTitleValue]
      )

      return (
        <Card p={16} bg="carbon.2" shadow="none" data-testid="metric-chart-card" {...cardProps}>
          <Stack gap={0}>
            <Group mb={displaySubTitle ? 0 : 'xs'} sx={{ justifyContent: 'center' }}>
              <Typography variant="title-md">
                {title}{' '}
                {!!label && (
                  <LabelTooltip
                    label={
                      <Typography
                        className="wd-whitespace-pre-line"
                        dangerouslySetInnerHTML={{ __html: label }}
                      ></Typography>
                    }
                  />
                )}
              </Typography>
            </Group>
            {displaySubTitle ? (
              <Group mb="xs" sx={{ justifyContent: 'center' }}>
                {displaySubTitle}
              </Group>
            ) : null}
          </Stack>
          <Box pos="relative">
            {initialLoading ? (
              <LoadingOverlay height={height} />
            ) : !!pendingOverlay ? (
              <>
                <Chart
                  key={`${title}_withdata`}
                  metric={metric}
                  height={height}
                  timeRange={timeRange}
                  chartSetting={chartSetting}
                  fixMinInterval={fixMinInterval}
                  onLoading={onLoading}
                  timeFormat={timeFormat}
                  fetchPromeData={() =>
                    Promise.resolve({
                      data: []
                    })
                  }
                />
                <Stack
                  align="center"
                  justify="center"
                  gap={4}
                  h={height}
                  pos="absolute"
                  top={-22}
                  left={0}
                  right={0}
                  bottom={0}
                >
                  {pendingOverlay}
                </Stack>
              </>
            ) : (
              <Chart
                key={`${title}_nodata`}
                metric={metric}
                height={height}
                timeRange={timeRange}
                chartSetting={chartSetting}
                fixMinInterval={fixMinInterval}
                onLoading={onLoading}
                timeFormat={timeFormat}
                fetchPromeData={fetchPromeData}
              />
            )}
          </Box>
        </Card>
      )
    }
  )
)

ChartCard.displayName = 'ChartCard'

export const Chart = memo(
  ({
    metric,
    height,
    timeRange,
    chartSetting,
    fixMinInterval,
    onLoading,
    timeFormat,
    fetchPromeData
  }: {
    metric: MetricInfo
    height: number
    timeRange: TimeRangeValue
    chartSetting: SettingsProps
    fixMinInterval: boolean
    onLoading?: (loading: boolean) => void
    timeFormat: string
    fetchPromeData: FetchPromeData
  }) => {
    const [firstLoading, setFirstLoading] = useState(false)
    const { themeConfig } = useChartThemeConfig()
    const { chartColorsOpaque } = useChartColors()
    const tooltipFormatter = useTimeFormatter(timeFormat)
    const xAxisFormatter = useTimeFormatter(niceTimeFormatByDay(timeRange))

    // Dynamically calculate minInterval based on fixedBarCount
    // minInterval is in milliseconds, timeRange is in seconds
    const dynamicXDomain = useMemo(
      () =>
        metric.fixedBarCount
          ? ({
              minInterval: Math.floor((timeRange[1] - timeRange[0]) / metric.fixedBarCount) * 1000
            } as SettingsProps['xDomain'])
          : undefined,
      [metric.fixedBarCount, timeRange]
    )

    const isAllLines = useMemo(() => metric.queries.every((query) => query.type === 'line'), [metric.queries])
    const queries = useMemo(() => {
      if (!isAllLines) {
        return metric.queries
      }

      return metric.queries.map((query) => ({
        ...query,
        lineSeriesStyle: {
          line: { strokeWidth: 1 },
          ...query.lineSeriesStyle
        }
      }))
    }, [isAllLines, metric.queries])

    const tooltipHeaderFormatter = useCallback(
      ({ value }: { value: number }) => tooltipFormatter(value / 1000),
      [tooltipFormatter]
    )

    const xAxisFormat = useCallback((value: number) => xAxisFormatter(value / 1000), [xAxisFormatter])

    const mergedChartSetting = useMemo(
      () => ({
        legendPosition: Position.Right,
        legendSize: 140,
        showLegend: metric.showLegend ?? true,
        xDomain: dynamicXDomain,
        baseTheme: themeConfig,
        ...(isAllLines ? { theme: { colors: { vizColors: chartColorsOpaque } } } : {}),
        tooltip: {
          type: TooltipType.Crosshairs,
          headerFormatter: tooltipHeaderFormatter,
          stickTo: TooltipStickTo.MousePosition
        },
        ...chartSetting
      }),
      [
        chartColorsOpaque,
        chartSetting,
        dynamicXDomain,
        isAllLines,
        metric.showLegend,
        themeConfig,
        tooltipHeaderFormatter
      ]
    )

    const loadingComponent = useCallback(() => <LoadingOverlay height={height} />, [height])

    const handleLoading = useCallback(
      (isLoading: boolean) => {
        if (!isLoading) {
          setFirstLoading(true)
        }
        onLoading?.(isLoading)
      },
      [onLoading]
    )

    return (
      <MetricsChart
        queries={queries}
        nullValue={metric.nullValue}
        unit={metric.unit}
        height={height}
        range={timeRange}
        xAxisFormat={xAxisFormat}
        minBinWidth={isAllLines ? 1 : undefined}
        chartSetting={mergedChartSetting}
        fixMinInterval={fixMinInterval}
        renderMode="serialized"
        renderCommitDelayMs={METRICS_CHART_RENDER_COMMIT_DELAY_MS}
        loadingComponent={firstLoading ? undefined : loadingComponent}
        onLoading={handleLoading}
        yAxisFormat={metric.yAxisFormat}
        yAxisScaleType={metric.yScaleType}
        logBase={metric.yLogBase}
        ignoreErrorData={true}
        fetchPromeData={fetchPromeData}
      />
    )
  }
)

Chart.displayName = 'Chart'

export default ChartCard
