'use client';

import { useEffect, useMemo, useState } from 'react';
import { Box, Card, Group, ScrollArea, Stack, Text, Badge, Title } from '@tidbcloud/uikit';
import { fetchWithAuth } from '@/lib/auth';
import { WorkspaceFile } from '@/lib/workspace';
import MetricChart from '@/components/InsightReport/MetricChart';
import { ChartDataPoint } from '@/lib/types';
import { dirname, normalizePath, evidenceTypeLabel, workspaceEvidenceInfo } from './evidencePaths';
import { SyncChartPointer as MetricsChartSyncProvider, TransformNullValue } from 'metrics-chart';
import { useTranslation, type TranslationKey } from '@/lib/i18n';

const API = process.env.NEXT_PUBLIC_API_URL ?? '';

interface DiagnosisEvidencePanelProps {
  workspaceId: string;
  reportPath: string;
}

interface EvidenceMetric {
  name?: string;
  component?: string;
  metric_group_id?: string;
  instance?: string;
  peak?: number;
  mean?: number;
  quota?: number;
  utilization?: string;
  unit?: string;
  severity?: string;
  link?: string;
  promql?: string;
  chart_data?: {
    name?: string;
    promql?: string;
    series?: Array<{
      instance?: string;
      values?: [number, string][];
    }>;
  } | null;
}

interface GroupedMetricChart {
  key: string;
  title: string;
  metricName: string;
  timeRange: [number, number];
  data: Record<string, ChartDataPoint[]>;
  metrics: EvidenceMetric[];
  windowLabel?: string;
}

export interface FocusedMetricChartData {
  key: string;
  title: string;
  metricName: string;
  timeRange: [number, number];
  data: Record<string, ChartDataPoint[]>;
  metrics: EvidenceMetric[];
  windowLabel?: string;
}

export interface FocusedMetricEvidence {
  name?: string;
  instance?: string;
  metric_ref?: {
    metric_group_id?: string;
    instances?: string[];
  };
  reason?: string;
}

export type JsonValue = Record<string, any> | any[] | null;

async function fetchWorkspaceFiles(workspaceId: string): Promise<WorkspaceFile[]> {
  const res = await fetchWithAuth(`${API}/api/workspace/${encodeURIComponent(workspaceId)}/files`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const data = await res.json();
  return data.files ?? [];
}

async function fetchFileData(workspaceId: string, path: string): Promise<JsonValue | string> {
  const res = await fetchWithAuth(
    `${API}/api/workspace/${encodeURIComponent(workspaceId)}/file?path=${encodeURIComponent(path)}`
  );
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const ct = (res.headers.get('Content-Type') || '').split(';')[0].trim().toLowerCase();
  if (ct === 'application/json' || path.endsWith('.json')) {
    const text = await res.text();
    try {
      return JSON.parse(text) as JsonValue;
    } catch {
      return text;
    }
  }
  return res.text();
}

function findNode(files: WorkspaceFile[], targetPath: string): WorkspaceFile | null {
  for (const file of files) {
    if (file.path === targetPath) return file;
    if (file.type === 'directory' && file.children?.length) {
      const found = findNode(file.children, targetPath);
      if (found) return found;
    }
  }
  return null;
}

function findDirectoryByPath(files: WorkspaceFile[], targetPath: string): WorkspaceFile | null {
  const node = findNode(files, targetPath);
  return node?.type === 'directory' ? node : null;
}

function findFirstDirectory(files: WorkspaceFile[], predicate: (file: WorkspaceFile) => boolean): WorkspaceFile | null {
  for (const file of files) {
    if (file.type === 'directory' && predicate(file)) return file;
    if (file.type === 'directory' && file.children?.length) {
      const found = findFirstDirectory(file.children, predicate);
      if (found) return found;
    }
  }
  return null;
}

function findWorkspaceEvidenceRoot(files: WorkspaceFile[]): WorkspaceFile | null {
  return (
    findDirectoryByPath(files, 'evidences') ||
    findFirstDirectory(
      files,
      file => file.path === 'evidences' || file.path.endsWith('/evidences')
    )
  );
}

function findWorkspaceEvidenceRootForPath(files: WorkspaceFile[], path: string): WorkspaceFile | null {
  const info = workspaceEvidenceInfo(path);
  if (!info?.root) return findWorkspaceEvidenceRoot(files);
  return findDirectoryByPath(files, info.root) || findWorkspaceEvidenceRoot(files);
}

function flattenFiles(files: WorkspaceFile[]): WorkspaceFile[] {
  const out: WorkspaceFile[] = [];
  for (const file of files) {
    if (file.type === 'file') out.push(file);
    if (file.children?.length) out.push(...flattenFiles(file.children));
  }
  return out;
}

function preferredEvidenceFile(files: WorkspaceFile[]): string | null {
  const candidates = flattenFiles(files).map(file => file.path);
  return (
    candidates.find(path => path.endsWith('/workspace_evidence_report.json')) ||
    candidates.find(path => path.endsWith('/evidence_summary_for_agent.json')) ||
    candidates.find(path => path.endsWith('/metrics.json')) ||
    candidates.find(path => path.endsWith('/queries.json')) ||
    candidates.find(path => path.endsWith('/logs.json')) ||
    candidates[0] ||
    null
  );
}

function severityColor(severity?: string): string {
  const value = String(severity || '').toLowerCase();
  if (value === 'critical') return 'red';
  if (value === 'warning' || value === 'high') return 'orange';
  if (value === 'normal' || value === 'low') return 'green';
  return 'gray';
}

function chartPayload(metric: EvidenceMetric): {
  data: Record<string, ChartDataPoint[]>;
  metricName: string;
  title: string;
  timeRange: [number, number];
} | null {
  const chartData = metric.chart_data;
  const series = chartData?.series ?? [];
  if (!series.length) return null;

  const data: Record<string, ChartDataPoint[]> = {};
  const timestamps: number[] = [];
  for (const item of series) {
    const instance = item.instance || metric.instance || 'series';
    const values = Array.isArray(item.values) ? item.values : [];
    const points: ChartDataPoint[] = [];
    for (const raw of values) {
      if (!Array.isArray(raw) || raw.length < 2) continue;
      const timestamp = Number(raw[0]);
      const value = Number(raw[1]);
      if (!Number.isFinite(timestamp) || !Number.isFinite(value)) continue;
      timestamps.push(timestamp);
      points.push({ timestamp, value, instance });
    }
    if (points.length) data[instance] = points;
  }

  if (!Object.keys(data).length || !timestamps.length) return null;
  return {
    data,
    metricName: chartData?.promql || metric.promql || metric.name || 'metric',
    title: metric.name || chartData?.name || 'Metric',
    timeRange: [Math.min(...timestamps), Math.max(...timestamps)],
  };
}

function metricSummary(metric: EvidenceMetric): string {
  const unit = metric.unit ? ` ${metric.unit}` : '';
  if (metric.peak != null) return `${metric.peak}${unit}`;
  if (metric.mean != null) return `${metric.mean}${unit}`;
  return metric.utilization || '-';
}

function severityRank(severity?: string): number {
  const value = String(severity || '').toLowerCase();
  if (value === 'critical') return 3;
  if (value === 'warning' || value === 'high') return 2;
  if (value === 'normal' || value === 'low') return 1;
  return 0;
}

// Two hours in milliseconds — gap threshold for treating time windows as disjoint
const DISJOINT_WINDOW_GAP_MS = 2 * 60 * 60 * 1000;

function formatWindowTs(ts: number): string {
  // Prometheus timestamps are in seconds; JS Date expects ms
  const ms = ts > 1e10 ? ts : ts * 1000;
  return new Date(ms).toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
}

function groupedMetricCharts(metrics: EvidenceMetric[]): GroupedMetricChart[] {
  // windowCounters tracks how many disjoint windows we've created per base key
  const windowCounters = new Map<string, number>();
  // groups maps the current (possibly suffixed) key to its chart entry
  const groups = new Map<string, GroupedMetricChart>();
  // latestKeyForBase maps a base key to the most recently created window key
  const latestKeyForBase = new Map<string, string>();

  for (const metric of metrics) {
    const chart = chartPayload(metric);
    if (!chart) continue;
    const baseKey = metric.metric_group_id
      || [
        metric.component || '',
        metric.name || chart.title || '',
        metric.promql || chart.metricName || '',
      ].join('|');

    const currentKey = latestKeyForBase.get(baseKey);
    const existing = currentKey ? groups.get(currentKey) : undefined;

    if (!existing) {
      const newKey = baseKey;
      groups.set(newKey, {
        key: newKey,
        title: chart.title,
        metricName: chart.metricName,
        timeRange: chart.timeRange,
        data: { ...chart.data },
        metrics: [metric],
      });
      latestKeyForBase.set(baseKey, newKey);
      windowCounters.set(baseKey, 1);
      continue;
    }

    // Detect disjoint window: new data starts more than 2h after current window ends
    const tsToMs = (ts: number) => ts > 1e10 ? ts : ts * 1000;
    const existingEndMs = tsToMs(existing.timeRange[1]);
    const newStartMs = tsToMs(chart.timeRange[0]);
    const isDisjoint = newStartMs - existingEndMs > DISJOINT_WINDOW_GAP_MS;

    if (isDisjoint) {
      // Assign windowLabel to the existing group (retroactively label it as window 1)
      const count = windowCounters.get(baseKey) ?? 1;
      if (count === 1) {
        const start = formatWindowTs(existing.timeRange[0]);
        const end = formatWindowTs(existing.timeRange[1]);
        existing.windowLabel = `1|${start}|${end}`;
      }

      // Create a new group for this disjoint window
      const newCount = count + 1;
      windowCounters.set(baseKey, newCount);
      const newKey = `${baseKey}:w${newCount}`;
      const start = formatWindowTs(chart.timeRange[0]);
      const end = formatWindowTs(chart.timeRange[1]);
      groups.set(newKey, {
        key: newKey,
        title: chart.title,
        metricName: chart.metricName,
        timeRange: chart.timeRange,
        data: { ...chart.data },
        metrics: [metric],
        windowLabel: `${newCount}|${start}|${end}`,
      });
      latestKeyForBase.set(baseKey, newKey);
      continue;
    }

    // Contiguous window — merge as before
    existing.timeRange = [
      Math.min(existing.timeRange[0], chart.timeRange[0]),
      Math.max(existing.timeRange[1], chart.timeRange[1]),
    ];
    for (const [instance, points] of Object.entries(chart.data)) {
      existing.data[instance] = points;
    }
    existing.metrics.push(metric);
  }

  return Array.from(groups.values()).sort((a, b) => a.title.localeCompare(b.title));
}

// windowLabel is stored as "index|start|end" — parse it for display
function parseWindowLabel(raw: string | undefined, t: (key: any, vars?: any) => string): string | null {
  if (!raw) return null;
  const parts = raw.split('|');
  if (parts.length < 3) return null;
  const [index, start, end] = parts;
  return t('studio.metricWindowLabel', { index, start, end });
}

function findMetricGroupByRef(groups: GroupedMetricChart[], focus?: FocusedMetricEvidence | null): GroupedMetricChart | null {
  const metricGroupId = focus?.metric_ref?.metric_group_id;
  if (!metricGroupId) return null;
  return groups.find(group => group.metrics.some(metric => metric.metric_group_id === metricGroupId)) || null;
}

const MAX_VISIBLE_INSTANCES = 10;

function focusInstances(focus?: FocusedMetricEvidence | null): string[] {
  const refInstances = focus?.metric_ref?.instances || [];
  if (refInstances.length) return refInstances;
  const raw = String(focus?.instance || '');
  if (!raw) return [];
  return raw
    .replace(/^\[/, '')
    .replace(/\]$/, '')
    .split(',')
    .map(item => item.trim())
    .filter(Boolean);
}

function normalizeMetricText(value?: string): string {
  return String(value || '')
    .toLowerCase()
    .replace(/\butilization\b/g, '')
    .replace(/[^a-z0-9]+/g, ' ')
    .trim();
}

function metricFocusScore(group: GroupedMetricChart, focus?: FocusedMetricEvidence | null): number {
  if (!focus) return 0;
  let score = 0;
  const focusName = normalizeMetricText(focus.name);
  const groupName = normalizeMetricText(group.title);
  if (focusName && groupName) {
    if (focusName === groupName) score += 100;
    else if (focusName.includes(groupName) || groupName.includes(focusName)) score += 80;
    else {
      const focusWords = new Set(focusName.split(' ').filter(Boolean));
      const overlap = groupName.split(' ').filter(word => focusWords.has(word)).length;
      score += overlap * 20;
    }
  }

  const instances = focusInstances(focus);
  if (instances.length) {
    const groupInstances = group.metrics.map(metric => String(metric.instance || ''));
    for (const target of instances) {
      if (groupInstances.some(instance => instance === target || instance.includes(target) || target.includes(instance))) {
        score += 30;
      }
    }
  }
  return score;
}

function MetricInstanceList({ metrics, focus }: { metrics: EvidenceMetric[]; focus?: FocusedMetricEvidence | null }) {
  const { t } = useTranslation();
  const [expanded, setExpanded] = useState(false);
  const focusedInstances = focusInstances(focus);
  const isFocused = (metric: EvidenceMetric) => {
    const instance = String(metric.instance || '');
    return focusedInstances.some(target => instance === target || instance.includes(target) || target.includes(instance));
  };
  const sorted = [...metrics].sort((a, b) => {
    const focusDelta = Number(isFocused(b)) - Number(isFocused(a));
    if (focusDelta) return focusDelta;
    return severityRank(b.severity) - severityRank(a.severity);
  });
  const visible = expanded ? sorted : sorted.slice(0, MAX_VISIBLE_INSTANCES);
  const hidden = sorted.length - MAX_VISIBLE_INSTANCES;

  return (
    <Stack gap={6}>
      {visible.map((metric, index) => {
        const focused = isFocused(metric);
        return (
        <Group
          key={`${metric.instance || 'metric'}-${index}`}
          justify="space-between"
          gap="sm"
          wrap="nowrap"
          style={{
            backgroundColor: focused ? '#ecfdf5' : undefined,
            border: focused ? '1px solid #bbf7d0' : undefined,
            borderRadius: 6,
            padding: focused ? '3px 6px' : undefined,
          }}
        >
          <Box style={{ minWidth: 0, flex: 1 }}>
            <Text size="xs" fw={500} truncate>
              {metric.instance || `series-${index + 1}`}
            </Text>
          </Box>
          <Group gap="xs" wrap="nowrap">
            {focused && <Badge size="xs" variant="light" color="green">{t('studio.badgeSelected')}</Badge>}
            {metric.severity && (
              <Badge size="xs" variant="light" color={severityColor(metric.severity) as any}>
                {metric.severity}
              </Badge>
            )}
            <Text size="xs" c="carbon.7">
              {metricSummary(metric)}
            </Text>
          </Group>
        </Group>
      )})}
      {hidden > 0 && (
        <Text
          size="xs"
          c="blue.6"
          style={{ cursor: 'pointer' }}
          onClick={() => setExpanded(e => !e)}
        >
          {expanded ? t('studio.showLess') : t('studio.showMore', { count: hidden })}
        </Text>
      )}
    </Stack>
  );
}

export function FocusedMetricChart({ chart, focus, badge }: { chart: FocusedMetricChartData; focus?: FocusedMetricEvidence | null; badge?: string }) {
  const { t } = useTranslation();
  const badgeLabel = badge ?? t('studio.badgeReferenced');
  const windowLabel = parseWindowLabel(chart.windowLabel, t);
  return (
    <Card withBorder p="sm" style={{ backgroundColor: '#f8fafc' }}>
      <Stack gap="xs">
        <Group justify="space-between" align="flex-start">
          <Box style={{ flex: 1, minWidth: 0 }}>
            <Text fw={600} size="xs">{chart.title || t('studio.metricFallback')}</Text>
            {windowLabel && (
              <Text size="xs" c="blue.6" fw={500}>{windowLabel}</Text>
            )}
            <Text size="xs" c="carbon.6" style={{ fontFamily: 'monospace' }}>
              {chart.metricName}
            </Text>
          </Box>
          <Badge size="xs" variant="light" color="green">{badgeLabel}</Badge>
        </Group>
        <MetricsChartSyncProvider>
          <MetricChart
            data={chart.data}
            metricName={chart.metricName}
            title={chart.title}
            timeRange={chart.timeRange}
            nullValue={TransformNullValue.NULL}
          />
        </MetricsChartSyncProvider>
        <MetricInstanceList metrics={chart.metrics} focus={focus} />
      </Stack>
    </Card>
  );
}

export function MetricEvidenceChart({ data, focus }: { data: Record<string, any>; focus?: FocusedMetricEvidence | null }) {
  const { t } = useTranslation();
  const metrics: EvidenceMetric[] = Array.isArray(data?.metrics) ? data.metrics : [];
  const groups = groupedMetricCharts(metrics);
  if (!groups.length) return <Text size="xs" c="carbon.6">{t('studio.noMetricChartAvailable')}</Text>;

  const referencedGroup = findMetricGroupByRef(groups, focus);
  const best = groups
    .map(group => ({ group, score: metricFocusScore(group, focus) }))
    .sort((a, b) => b.score - a.score)[0];
  if (!referencedGroup && (!best || best.score <= 0)) {
    return <Text size="xs" c="carbon.6">{t('studio.noMetricMatch')}</Text>;
  }

  const group = referencedGroup || best.group;
  return <FocusedMetricChart chart={group} focus={focus} badge={referencedGroup ? t('studio.badgeReferenced') : t('studio.badgeMatched')} />;
}

function MetricsPreview({ data }: { data: Record<string, any> }) {
  const { t } = useTranslation();
  const metrics: EvidenceMetric[] = Array.isArray(data?.metrics) ? data.metrics : [];
  if (!metrics.length) return <Text size="sm" c="carbon.6">{t('studio.noMetricsInFile')}</Text>;
  const groups = groupedMetricCharts(metrics);

  return (
    <Stack gap="sm">
      {groups.map((group) => {
        const highestSeverity = group.metrics
          .map(metric => metric.severity)
          .sort((a, b) => severityRank(b) - severityRank(a))[0];
        const windowLabel = parseWindowLabel(group.windowLabel, t);
        return (
          <Card key={group.key} withBorder p="md">
            <Stack gap="sm">
              <Group justify="space-between" align="flex-start">
                <Box style={{ flex: 1, minWidth: 0 }}>
                  <Group gap="xs">
                    <Text fw={600} size="sm">{group.title || t('studio.metricFallback')}</Text>
                    {highestSeverity && (
                      <Badge size="sm" variant="light" color={severityColor(highestSeverity) as any}>
                        {highestSeverity}
                      </Badge>
                    )}
                    <Badge size="sm" variant="light" color="blue">
                      {t('studio.seriesCount', { count: Object.keys(group.data).length })}
                    </Badge>
                  </Group>
                  {windowLabel && (
                    <Text size="xs" c="blue.6" fw={500}>{windowLabel}</Text>
                  )}
                  <Text size="xs" c="carbon.6" style={{ fontFamily: 'monospace' }}>
                    {group.metricName}
                  </Text>
                </Box>
                <Text size="sm" fw={600} c="carbon.8">
                  {t('studio.entriesCount', { count: group.metrics.length })}
                </Text>
              </Group>
              <MetricChart
                data={group.data}
                metricName={group.metricName}
                title={group.title}
                timeRange={group.timeRange}
                nullValue={TransformNullValue.NULL}
              />
              <MetricInstanceList metrics={group.metrics} />
            </Stack>
          </Card>
        );
      })}
    </Stack>
  );
}

function pickString(entry: Record<string, any>, keys: string[]): string {
  for (const key of keys) {
    const value = entry?.[key];
    if (typeof value === 'string' && value.trim()) return value;
  }
  return '';
}

const LEVEL_COLORS: Record<string, string> = { error: 'red', warn: 'orange', info: 'blue' };

function LogStatsPreview({ data }: { data: Record<string, any> }) {
  const { t } = useTranslation();
  const summary: Record<string, Record<string, number>> = data?.summary ?? {};
  const timeline: Record<string, Record<string, Array<{ window_start: number; count: number }>>> = data?.timeline ?? {};
  const loki_url: string = data?.loki_url ?? '';
  const components = ['tidb', 'tikv', 'pd'];
  const levels = ['error', 'warn', 'info'];

  const chartsByComponent = useMemo(() => {
    return components.reduce<Record<string, { timeRange: [number, number]; chartData: Record<string, ChartDataPoint[]> }>>((acc, comp) => {
      const chartData: Record<string, ChartDataPoint[]> = {};
      let minTs = Infinity, maxTs = -Infinity;
      for (const level of levels) {
        const buckets = timeline[comp]?.[level] ?? [];
        chartData[level] = buckets.map(b => ({ timestamp: b.window_start, value: b.count }));
        for (const b of buckets) {
          if (b.window_start < minTs) minTs = b.window_start;
          if (b.window_start > maxTs) maxTs = b.window_start;
        }
      }
      const timeRange: [number, number] = minTs === Infinity ? [0, 0] : [minTs, maxTs];
      acc[comp] = { timeRange, chartData };
      return acc;
    }, {});
  }, [timeline]);

  if (!Object.keys(summary).length) {
    return <Text size="sm" c="carbon.6">{t('studio.noLogStats')}</Text>;
  }

  return (
    <MetricsChartSyncProvider>
      <Stack gap="md">
        <Card withBorder p="md">
          <Text fw={600} size="sm" mb="sm">{t('studio.logCountSummary')}</Text>
          <Box style={{ overflowX: 'auto' }}>
            <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
              <thead>
                <tr>
                  <th style={{ textAlign: 'left', padding: '4px 12px 4px 0', borderBottom: '1px solid #e5e7eb' }}>{t('studio.componentCol')}</th>
                  {levels.map(l => (
                    <th key={l} style={{ textAlign: 'right', padding: '4px 12px', borderBottom: '1px solid #e5e7eb' }}>
                      <Badge size="sm" color={LEVEL_COLORS[l] as any} variant="light">{l.toUpperCase()}</Badge>
                    </th>
                  ))}
                </tr>
              </thead>
              <tbody>
                {components.map(comp => (
                  <tr key={comp}>
                    <td style={{ padding: '4px 12px 4px 0', fontWeight: 600 }}>{comp.toUpperCase()}</td>
                    {levels.map(l => {
                      const count = summary[comp]?.[l] ?? 0;
                      return (
                        <td key={l} style={{ textAlign: 'right', padding: '4px 12px', color: count > 0 && l === 'error' ? '#dc2626' : undefined }}>
                          {count.toLocaleString()}
                        </td>
                      );
                    })}
                  </tr>
                ))}
              </tbody>
            </table>
          </Box>
          {loki_url && (
            <Text size="xs" mt="sm">
              <a href={loki_url} target="_blank" rel="noreferrer" style={{ color: '#2563eb' }}>{t('studio.openLoki')}</a>
            </Text>
          )}
        </Card>
        {components.map(comp => {
          const { timeRange, chartData } = chartsByComponent[comp] ?? {};
          const hasData = Object.values(chartData ?? {}).some(pts => pts.length > 0);
          if (!hasData) return null;
          return (
            <Card key={comp} withBorder p="md">
              <Text fw={600} size="sm" mb="sm">{t('studio.componentLogs', { component: comp.toUpperCase() })}</Text>
              <MetricChart
                data={chartData}
                metricName={`${comp}_log_count`}
                title={t('studio.componentLogsTitle', { component: comp.toUpperCase() })}
                timeRange={timeRange}
                nullValue={TransformNullValue.AS_ZERO}
                chartType="bar_stacked"
                chartSetting={{ xDomain: { min: NaN, max: NaN, minInterval: 300000 } }}
              />
            </Card>
          );
        })}
      </Stack>
    </MetricsChartSyncProvider>
  );
}

function LogsPreview({ data }: { data: Record<string, any> }) {
  const { t } = useTranslation();
  const entries: Record<string, any>[] = Array.isArray(data?.entries)
    ? data.entries
    : Array.isArray(data?.log_counts)
      ? data.log_counts
      : [];
  if (!entries.length) return <Text size="sm" c="carbon.6">{t('studio.noLogEntries')}</Text>;

  return (
    <Stack gap="xs">
      {entries.map((entry, index) => {
        const level = pickString(entry, ['level', 'severity', 'lvl']) || 'INFO';
        const component = pickString(entry, ['component', 'module', 'logger']) || '-';
        const instance = pickString(entry, ['instance', 'host', 'pod']) || '';
        const instances: string[] = Array.isArray(entry?.instances) ? entry.instances : [];
        const time = pickString(entry, ['time', 'timestamp', 'ts']) || '';
        const message = pickString(entry, ['message', 'msg', 'log']) || JSON.stringify(entry);
        const count = typeof entry?.count === 'number' ? entry.count : 0;
        const isError = ['ERROR', 'FATAL'].includes(level.toUpperCase());
        const instanceLabel = instance || (instances.length ? instances.join(', ') : '');

        return (
          <Card
            key={`${time}-${index}`}
            withBorder
            p="sm"
            style={{ backgroundColor: isError ? '#fef2f2' : '#fff7ed' }}
          >
            <Stack gap={6}>
              <Group gap="xs" wrap="nowrap">
                <Badge size="sm" color={(isError ? 'red' : 'orange') as any} variant="filled" c="white">
                  {level}
                </Badge>
                <Text size="xs" fw={600}>{component}</Text>
                {count > 0 && (
                  <Badge size="xs" variant="light" color="gray">
                    x{count}
                  </Badge>
                )}
                {instanceLabel && (
                  <Text size="xs" c="carbon.6" style={{ fontFamily: 'monospace' }}>
                    {instanceLabel}
                  </Text>
                )}
              </Group>
              {time && <Text size="xs" c="carbon.6">{time}</Text>}
              <Text size="xs" c="carbon.8" style={{ fontFamily: 'monospace', whiteSpace: 'pre-wrap' }}>
                {message}
              </Text>
            </Stack>
          </Card>
        );
      })}
    </Stack>
  );
}

const LINK_LABEL_KEYS: Record<string, TranslationKey> = {
  clinic_overview: 'studio.linkClinicOverview',
  slow_queries: 'studio.linkSlowQueries',
  logs: 'studio.linkLogsLoki',
  topsql: 'studio.linkTopSql',
};

function LinksPreview({ data }: { data: Record<string, any> }) {
  const { t } = useTranslation();
  const entries = Object.entries(data).filter(([, v]) => typeof v === 'string' && v.startsWith('http'));
  if (!entries.length) return <Text size="sm" c="carbon.6">{t('studio.noLinks')}</Text>;

  return (
    <Stack gap="xs" p="xs">
      {entries.map(([key, url]) => (
        <Group key={key} gap="sm" wrap="nowrap" align="center"
          style={{
            padding: '8px 12px',
            borderRadius: 8,
            border: '1px solid #e5e7eb',
            backgroundColor: '#f8fafc',
          }}
        >
          <Box style={{ flex: 1, minWidth: 0 }}>
            <Text size="sm" fw={600} c="carbon.8">
              {LINK_LABEL_KEYS[key] ? t(LINK_LABEL_KEYS[key]) : key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}
            </Text>
            <Text size="xs" c="carbon.5" truncate style={{ fontFamily: 'monospace' }}>
              {url}
            </Text>
          </Box>
          <Text
            size="xs"
            fw={500}
            c="blue.6"
            component="a"
            href={url}
            target="_blank"
            rel="noopener noreferrer"
            style={{ textDecoration: 'none', whiteSpace: 'nowrap', flexShrink: 0 }}
          >
            {t('studio.openExternal')}
          </Text>
        </Group>
      ))}
    </Stack>
  );
}

function RawJsonPreview({ data }: { data: JsonValue }) {
  return (
    <Box
      p="md"
      style={{
        border: '1px solid #e5e7eb',
        borderRadius: 8,
        backgroundColor: '#fff',
        overflowX: 'auto',
      }}
    >
      <pre style={{ margin: 0, fontSize: 12, lineHeight: 1.6 }}>{JSON.stringify(data, null, 2)}</pre>
    </Box>
  );
}

export function EvidencePreviewPane({ filePath, data }: { filePath: string | null; data: JsonValue | string }) {
  const { t } = useTranslation();
  if (!filePath || data == null) {
    return <Text size="sm" c="carbon.6">{t('studio.selectEvidenceFile')}</Text>;
  }

  if (typeof data === 'string') {
    return (
      <Box p="md" style={{ border: '1px solid #e5e7eb', borderRadius: 8, backgroundColor: '#fff', overflowX: 'auto' }}>
        <pre style={{ margin: 0, fontSize: 12, lineHeight: 1.6, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{data}</pre>
      </Box>
    );
  }

  if (!Array.isArray(data) && typeof data === 'object') {
    if (
      filePath.endsWith('/metrics.json') ||
      Array.isArray((data as any).metrics)
    ) {
      return (
        <MetricsChartSyncProvider>
          <MetricsPreview data={data as Record<string, any>} />
        </MetricsChartSyncProvider>
      );
    }
    if (filePath.endsWith('/logs.json')) {
      if ((data as any).timeline != null) {
        return <LogStatsPreview data={data as Record<string, any>} />;
      }
      return <LogsPreview data={data as Record<string, any>} />;
    }
    if (filePath.endsWith('/links.json')) {
      return <LinksPreview data={data as Record<string, any>} />;
    }
  }

  return <RawJsonPreview data={data} />;
}


export default function DiagnosisEvidencePanel({ workspaceId, reportPath }: DiagnosisEvidencePanelProps) {
  const { t } = useTranslation();
  const reportDir = useMemo(() => dirname(reportPath), [reportPath]);
  const [treeLoading, setTreeLoading] = useState(false);
  const [treeError, setTreeError] = useState<string | null>(null);
  const [fileTree, setFileTree] = useState<WorkspaceFile[]>([]);
  const isDirectFile = reportPath.endsWith('.json') || reportPath.endsWith('.txt');
  const [selectedFilePath, setSelectedFilePath] = useState<string | null>(isDirectFile ? reportPath : null);
  const [fileData, setFileData] = useState<JsonValue | string>(null);
  const [fileLoading, setFileLoading] = useState(false);
  const [fileError, setFileError] = useState<string | null>(null);

  useEffect(() => {
    if (!workspaceId || !reportDir) return;
    setTreeLoading(true);
    setTreeError(null);
    fetchWorkspaceFiles(workspaceId)
      .then(files => {
        const workspaceInfo = workspaceEvidenceInfo(reportPath);
        const workspaceRoot = workspaceInfo?.root ? findWorkspaceEvidenceRootForPath(files, reportPath) : null;
        const exactNode = workspaceRoot || findDirectoryByPath(files, reportDir);
        const tree = exactNode ? [exactNode] : [];
        setFileTree(tree);
        if (!isDirectFile) {
          const allFiles = flattenFiles(tree).map(file => file.path);
          setSelectedFilePath(preferredEvidenceFile(tree) || allFiles[0] || null);
        }
      })
      .catch(err => setTreeError(String(err)))
      .finally(() => setTreeLoading(false));
  }, [workspaceId, reportDir, reportPath]);

  useEffect(() => {
    if (!workspaceId || !selectedFilePath) {
      setFileData(null);
      return;
    }
    setFileLoading(true);
    setFileError(null);
    fetchFileData(workspaceId, selectedFilePath)
      .then(setFileData)
      .catch(err => {
        setFileData(null);
        setFileError(String(err));
      })
      .finally(() => setFileLoading(false));
  }, [workspaceId, selectedFilePath]);

  return (
    <Box style={{ height: '100%' }}>
      <Stack gap="sm" p="md" style={{ height: '100%' }}>
        <Group justify="space-between" align="center">
          <Box>
            <Title order={5}>{t('studio.evidenceTitle')}</Title>
            <Text size="xs" c="carbon.6" style={{ fontFamily: 'monospace' }}>
              {reportDir || 'evidences'}
            </Text>
          </Box>
          <Badge variant="light" color="blue">{flattenFiles(fileTree).length} files</Badge>
        </Group>

        {treeLoading ? (
          <Text size="sm" c="carbon.6">Loading evidence files…</Text>
        ) : treeError ? (
          <Text size="sm" c="red">{treeError}</Text>
        ) : fileTree.length === 0 ? (
          <Card withBorder p="md">
            <Stack gap="xs">
              <Text size="sm" fw={600}>{t('studio.evidenceNoFilesTitle')}</Text>
              <Text size="xs" c="carbon.6" style={{ fontFamily: 'monospace' }}>
                expected under: {reportDir || 'evidences'}
              </Text>
              <Text size="sm" c="carbon.6">
                This usually means the evidence container has not written sidecar evidence files yet.
              </Text>
            </Stack>
          </Card>
        ) : (
          <Box style={{ flex: 1, minHeight: 0, minWidth: 0 }}>
            <ScrollArea style={{ height: '100%' }}>
              <Stack gap="sm">
                {selectedFilePath && (
                  <Text size="xs" c="carbon.6" style={{ fontFamily: 'monospace' }}>
                    {selectedFilePath}
                  </Text>
                )}
                {fileLoading ? (
                  <Text size="sm" c="carbon.6">Loading file…</Text>
                ) : fileError ? (
                  <Text size="sm" c="red">{fileError}</Text>
                ) : (
                  <EvidencePreviewPane filePath={selectedFilePath} data={fileData} />
                )}
              </Stack>
            </ScrollArea>
          </Box>
        )}
      </Stack>
    </Box>
  );
}
