/*
 * Copyright Daytona Platforms Inc.
 * SPDX-License-Identifier: AGPL-3.0
 */

import { TimeRangeSelector } from '@/components/telemetry/TimeRangeSelector'
import { Button } from '@/components/ui/button'
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Skeleton } from '@/components/ui/skeleton'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
import { DAYTONA_DOCS_URL } from '@/constants/ExternalLinks'
import { getMetricDisplayName } from '@/constants/metrics'
import { MetricsQueryParams, useSandboxMetrics } from '@/hooks/useSandboxMetrics'
import { MetricSeries } from '@daytona/api-client'
import { format, subHours } from 'date-fns'
import { BarChart3, RefreshCw } from 'lucide-react'
import { useQueryStates } from 'nuqs'
import React, { useCallback, useMemo, useState } from 'react'
import { CartesianGrid, Legend, Line, LineChart, XAxis, YAxis } from 'recharts'
import { timeRangeSearchParams } from './SearchParams'

const CHART_COLORS = [
  'hsl(var(--chart-1))',
  'hsl(var(--chart-2))',
  'hsl(var(--chart-3))',
  'hsl(var(--chart-4))',
  'hsl(var(--chart-5))',
]

const BYTES_TO_GIB = 1024 * 1024 * 1024
type ViewMode = '%' | 'GiB'

const METRIC_GROUPS = [
  { key: 'cpu', title: 'CPU (cores)', prefix: '.cpu.', hasToggle: false },
  { key: 'memory', title: 'Memory', prefix: '.memory.', hasToggle: true },
  { key: 'filesystem', title: 'Filesystem', prefix: '.filesystem.', hasToggle: true },
]

function isByteMetric(metricName: string): boolean {
  return !metricName.endsWith('.utilization')
}

function buildChartData(series: MetricSeries[], convertToGiB: boolean): Record<string, unknown>[] {
  const timestampSet = new Set<string>()
  const indexed = series.map((s) => {
    const byTimestamp = new Map<string, number | null>()
    for (const p of s.dataPoints) {
      timestampSet.add(p.timestamp)
      byTimestamp.set(p.timestamp, p.value ?? null)
    }
    return { metricName: s.metricName, byTimestamp, convertMetric: convertToGiB && isByteMetric(s.metricName) }
  })
  const timestamps = Array.from(timestampSet).sort()

  return timestamps.map((timestamp) => {
    const point: Record<string, unknown> = { timestamp }
    for (const { metricName, byTimestamp, convertMetric } of indexed) {
      const value = byTimestamp.get(timestamp)
      if (value == null) {
        point[metricName] = null
      } else if (convertMetric) {
        point[metricName] = Math.round((value / BYTES_TO_GIB) * 100) / 100
      } else {
        point[metricName] = value
      }
    }
    return point
  })
}

function buildChartConfig(series: MetricSeries[]): ChartConfig {
  const config: ChartConfig = {}
  series.forEach((s, index) => {
    config[s.metricName] = {
      label: getMetricDisplayName(s.metricName),
      color: CHART_COLORS[index % CHART_COLORS.length],
    }
  })
  return config
}

const formatXAxis = (timestamp: string) => {
  try {
    return format(new Date(timestamp), 'HH:mm')
  } catch {
    return timestamp
  }
}

function MetricGroupChart({
  title,
  series,
  convertToGiB,
  viewMode,
  onViewModeChange,
}: {
  title: string
  series: MetricSeries[]
  convertToGiB: boolean
  viewMode?: ViewMode
  onViewModeChange?: (mode: ViewMode) => void
}) {
  const chartData = React.useMemo(() => buildChartData(series, convertToGiB), [series, convertToGiB])
  const chartConfig = React.useMemo(() => buildChartConfig(series), [series])
  const displayTitle = viewMode ? `${title} (${viewMode})` : title

  return (
    <div className="rounded-md border border-border">
      <div className="flex items-center gap-2 px-4 py-3">
        <h3 className="text-sm font-medium">{displayTitle}</h3>
        {viewMode && onViewModeChange && (
          <ToggleGroup
            type="single"
            value={viewMode}
            onValueChange={(value) => {
              if (value) onViewModeChange(value as ViewMode)
            }}
            variant="outline"
            size="sm"
          >
            <ToggleGroupItem value="%" className="text-xs px-2 h-6">
              %
            </ToggleGroupItem>
            <ToggleGroupItem value="GiB" className="text-xs px-2 h-6">
              GiB
            </ToggleGroupItem>
          </ToggleGroup>
        )}
      </div>
      <div className="border-t border-border px-4 py-3">
        <ChartContainer config={chartConfig} className="h-[220px] w-full">
          <LineChart data={chartData} margin={{ top: 10, right: 10, left: 0, bottom: 5 }}>
            <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
            <XAxis
              dataKey="timestamp"
              tickFormatter={formatXAxis}
              className="text-xs"
              tick={{ fill: 'hsl(var(--muted-foreground))' }}
            />
            <YAxis
              width={35}
              className="text-xs"
              tick={{ fill: 'hsl(var(--muted-foreground))' }}
              tickFormatter={convertToGiB ? (value: number) => value.toFixed(2) : undefined}
              domain={viewMode === '%' ? [0, 100] : undefined}
            />
            <ChartTooltip
              content={
                <ChartTooltipContent
                  labelFormatter={(label) => {
                    try {
                      return format(new Date(label as string), 'yyyy-MM-dd HH:mm:ss')
                    } catch {
                      return String(label)
                    }
                  }}
                />
              }
            />
            <Legend />
            {series.map((s, index) => {
              const isLimit = s.metricName.endsWith('.limit') || s.metricName.endsWith('.total')
              return (
                <Line
                  key={s.metricName}
                  type="monotone"
                  dataKey={s.metricName}
                  name={getMetricDisplayName(s.metricName)}
                  stroke={CHART_COLORS[index % CHART_COLORS.length]}
                  strokeWidth={isLimit ? 1.5 : 2}
                  strokeDasharray={isLimit ? '6 3' : undefined}
                  dot={false}
                  connectNulls
                />
              )
            })}
          </LineChart>
        </ChartContainer>
      </div>
    </div>
  )
}

function MetricsChartsSkeleton() {
  return (
    <div className="flex flex-col gap-6">
      {['CPU', 'Memory', 'Filesystem'].map((title) => (
        <div key={title} className="min-h-[250px]">
          <div className="flex items-center gap-2 mb-2">
            <Skeleton className="h-4 w-20" />
          </div>
          <Skeleton className="h-[220px] w-full rounded-md" />
        </div>
      ))}
    </div>
  )
}

function MetricsErrorState({ onRetry }: { onRetry: () => void }) {
  return (
    <Empty className="flex-1 border-0">
      <EmptyHeader>
        <EmptyTitle>Failed to load metrics</EmptyTitle>
        <EmptyDescription>Something went wrong while fetching metrics.</EmptyDescription>
      </EmptyHeader>
      <Button variant="outline" size="sm" onClick={onRetry}>
        <RefreshCw className="size-4" />
        Retry
      </Button>
    </Empty>
  )
}

function MetricsEmptyState({ hasFilters, onClearFilters }: { hasFilters: boolean; onClearFilters: () => void }) {
  return (
    <Empty className="flex-1 border-0">
      <EmptyHeader>
        <EmptyMedia variant="icon">
          <BarChart3 className="size-4" />
        </EmptyMedia>
        <EmptyTitle>{hasFilters ? 'No matching metrics found' : 'No metrics yet'}</EmptyTitle>
        {hasFilters ? (
          <EmptyDescription>No metrics matched your current filters.</EmptyDescription>
        ) : (
          <EmptyDescription>
            Metrics may take a moment to appear after the sandbox starts.{' '}
            <a href={`${DAYTONA_DOCS_URL}/en/observability/otel-collection`} target="_blank" rel="noopener noreferrer">
              Learn more about observability
            </a>
            .
          </EmptyDescription>
        )}
      </EmptyHeader>
      {hasFilters ? (
        <Button variant="outline" size="sm" onClick={onClearFilters}>
          Clear filters
        </Button>
      ) : null}
    </Empty>
  )
}

export function SandboxMetricsTab({ sandboxId }: { sandboxId: string }) {
  const [timeRange, setTimeRange] = useQueryStates(timeRangeSearchParams)
  const [viewModes, setViewModes] = useState<Record<string, ViewMode>>({ memory: '%', filesystem: '%' })
  const [timeRangeSelectorKey, setTimeRangeSelectorKey] = useState(0)

  const resolvedFrom = useMemo(() => timeRange.from ?? subHours(new Date(), 1), [timeRange.from])
  const resolvedTo = useMemo(() => timeRange.to ?? new Date(), [timeRange.to])

  const queryParams: MetricsQueryParams = { from: resolvedFrom, to: resolvedTo }
  const { data, isLoading, isError, refetch } = useSandboxMetrics(sandboxId, queryParams)

  const handleTimeRangeChange = useCallback(
    (from: Date, to: Date) => {
      setTimeRange({ from, to })
    },
    [setTimeRange],
  )

  const handleTimeRangeClear = useCallback(() => {
    setTimeRange({ from: null, to: null })
  }, [setTimeRange])

  const handleViewModeChange = useCallback((groupKey: string, mode: ViewMode) => {
    setViewModes((prev) => ({ ...prev, [groupKey]: mode }))
  }, [])

  const hasFilters = Boolean(timeRange.from || timeRange.to)

  const handleClearFilters = useCallback(() => {
    setTimeRange({ from: null, to: null })
    setTimeRangeSelectorKey((key) => key + 1)
  }, [setTimeRange])

  const groupedSeries = React.useMemo(() => {
    if (!data?.series?.length) return []

    return METRIC_GROUPS.map((group) => {
      const allSeries = data.series.filter((s) => s.metricName.includes(group.prefix))
      const mode = group.hasToggle ? viewModes[group.key] : undefined

      let filteredSeries = allSeries.filter((s) => s.metricName !== 'system.memory.utilization')
      if (mode === '%') {
        filteredSeries = filteredSeries.filter((s) => s.metricName.endsWith('.utilization'))
      } else if (mode === 'GiB') {
        filteredSeries = filteredSeries.filter((s) => !s.metricName.endsWith('.utilization'))
      }

      // Convert CPU utilization from percentage to cores using the limit value
      if (group.key === 'cpu') {
        const limitSeries = allSeries.find((s) => s.metricName.endsWith('.cpu.limit'))
        const utilizationSeries = filteredSeries.find((s) => s.metricName.endsWith('.cpu.utilization'))
        if (limitSeries && utilizationSeries) {
          const limitByTimestamp = new Map<string, number>()
          const sortedLimits = [...limitSeries.dataPoints]
            .filter((dp) => dp.value != null)
            .sort((a, b) => a.timestamp.localeCompare(b.timestamp))
          for (const dp of sortedLimits) {
            limitByTimestamp.set(dp.timestamp, dp.value!)
          }

          const convertedUtilization: MetricSeries = {
            ...utilizationSeries,
            dataPoints: utilizationSeries.dataPoints.flatMap((dp) => {
              if (dp.value == null) return [dp]
              let limitValue = limitByTimestamp.get(dp.timestamp)
              if (limitValue == null) {
                for (const lp of sortedLimits) {
                  if (lp.timestamp > dp.timestamp) break
                  limitValue = lp.value!
                }
              }
              if (limitValue == null) return []
              return [
                {
                  ...dp,
                  value: Math.round((dp.value / 100) * limitValue * 100) / 100,
                },
              ]
            }),
          }
          filteredSeries = filteredSeries.map((s) =>
            s.metricName === utilizationSeries.metricName ? convertedUtilization : s,
          )
        }
      }

      return {
        key: group.key,
        title: group.title,
        series: filteredSeries,
        convertToGiB: mode === 'GiB',
        hasToggle: group.hasToggle,
        viewMode: mode,
      }
    }).filter((group) => group.series.length > 0)
  }, [data, viewModes])

  return (
    <div className="flex flex-col h-full gap-4 p-4">
      <div className="flex flex-wrap items-center gap-3 shrink-0">
        <TimeRangeSelector
          key={timeRangeSelectorKey}
          onChange={handleTimeRangeChange}
          onClear={handleTimeRangeClear}
          defaultRange={timeRange.from && timeRange.to ? { from: timeRange.from, to: timeRange.to } : undefined}
          className="w-auto"
        />
        <Button variant="ghost" size="icon-sm" onClick={() => refetch()} className="ml-auto">
          <RefreshCw className="size-4" />
        </Button>
      </div>

      {isLoading ? (
        <div className="flex-1 min-h-0 rounded-md border border-border p-4">
          <MetricsChartsSkeleton />
        </div>
      ) : isError ? (
        <div className="flex-1 min-h-0 rounded-md border border-border flex">
          <MetricsErrorState onRetry={() => refetch()} />
        </div>
      ) : !data?.series?.length ? (
        <div className="flex-1 min-h-0 rounded-md border border-border flex">
          <MetricsEmptyState hasFilters={hasFilters} onClearFilters={handleClearFilters} />
        </div>
      ) : (
        <ScrollArea fade="mask" className="flex-1 min-h-0">
          <div className="flex flex-col gap-4">
            {groupedSeries.map((group) => (
              <MetricGroupChart
                key={group.key}
                title={group.title}
                series={group.series}
                convertToGiB={group.convertToGiB}
                viewMode={group.hasToggle ? group.viewMode : undefined}
                onViewModeChange={group.hasToggle ? (mode) => handleViewModeChange(group.key, mode) : undefined}
              />
            ))}
          </div>
        </ScrollArea>
      )}
    </div>
  )
}
