import type { ReactNode } from 'react';

import type { ChatMessage } from '../../types/bot';
import { normalizeAssistantMessageText, normalizeUserMessageText } from './messageParser';
import {
  AUDIO_PREVIEW_EXTENSIONS,
  HTML_PREVIEW_EXTENSIONS,
  IMAGE_PREVIEW_EXTENSIONS,
  MEDIA_UPLOAD_EXTENSIONS,
  SYSTEM_FALLBACK_TOPIC_KEYS,
  TEXT_PREVIEW_EXTENSIONS,
  VIDEO_PREVIEW_EXTENSIONS,
} from './constants';
import type {
  BotTopic,
  CronJob,
  MCPConfigResponse,
  MCPServerDraft,
  TopicPresetTemplate,
  WorkspaceNode,
} from './types';
import { normalizeDashboardAttachmentPath } from './shared/workspaceMarkdown';

const COMPOSER_DRAFT_STORAGE_PREFIX = 'nanobot-dashboard-composer-draft:v1:';
const EMPTY_WORKSPACE_DOWNLOAD_EXTENSIONS: readonly string[] = [];
const EMPTY_WORKSPACE_DOWNLOAD_EXTENSION_SET: ReadonlySet<string> = new Set<string>();

export function formatClock(ts: number) {
  const d = new Date(ts);
  const hh = String(d.getHours()).padStart(2, '0');
  const mm = String(d.getMinutes()).padStart(2, '0');
  const ss = String(d.getSeconds()).padStart(2, '0');
  return `${hh}:${mm}:${ss}`;
}

export function formatConversationDate(ts: number, isZh: boolean) {
  const d = new Date(ts);
  try {
    return d.toLocaleDateString(isZh ? 'zh-CN' : 'en-US', {
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
      weekday: 'short',
    });
  } catch {
    const y = d.getFullYear();
    const m = String(d.getMonth() + 1).padStart(2, '0');
    const day = String(d.getDate()).padStart(2, '0');
    return `${y}-${m}-${day}`;
  }
}

export function formatDateInputValue(ts: number): string {
  const d = new Date(ts);
  if (Number.isNaN(d.getTime())) return '';
  const year = d.getFullYear();
  const month = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${year}-${month}-${day}`;
}

export function mapBotMessageResponseRow(row: any): ChatMessage {
  const roleRaw = String(row?.role || '').toLowerCase();
  const role: ChatMessage['role'] =
    roleRaw === 'user' || roleRaw === 'assistant' || roleRaw === 'system' ? roleRaw : 'assistant';
  const feedbackRaw = String(row?.feedback || '').trim().toLowerCase();
  const feedback: ChatMessage['feedback'] = feedbackRaw === 'up' || feedbackRaw === 'down' ? feedbackRaw : null;
  return {
    id: Number.isFinite(Number(row?.id)) ? Number(row.id) : undefined,
    role,
    text: String(row?.text || ''),
    attachments: normalizeAttachmentPaths(row?.media),
    ts: Number(row?.ts || Date.now()),
    feedback,
    kind: 'final',
  } as ChatMessage;
}

export function stateLabel(s?: string) {
  return (s || 'IDLE').toUpperCase();
}

export function resolvePresetText(raw: unknown, locale: 'zh-cn' | 'en'): string {
  if (typeof raw === 'string') return raw.trim();
  if (!raw || typeof raw !== 'object') return '';
  const bag = raw as Record<string, unknown>;
  const byLocale = String(bag[locale] || '').trim();
  if (byLocale) return byLocale;
  return String(bag['zh-cn'] || bag.en || '').trim();
}

export function normalizePresetTextList(raw: unknown): string[] {
  if (!Array.isArray(raw)) return [];
  const rows: string[] = [];
  raw.forEach((item) => {
    const text = String(item || '').trim();
    if (text) rows.push(text);
  });
  return rows;
}

export function parseTopicPresets(raw: unknown): TopicPresetTemplate[] {
  if (!Array.isArray(raw)) return [];
  const rows: TopicPresetTemplate[] = [];
  raw.forEach((item) => {
    if (!item || typeof item !== 'object') return;
    const record = item as Record<string, unknown>;
    const id = String(record.id || '').trim().toLowerCase();
    const topicKey = String(record.topic_key || '').trim().toLowerCase();
    if (!id || !topicKey) return;
    const priority = Number(record.routing_priority);
    rows.push({
      id,
      topic_key: topicKey,
      name: record.name,
      description: record.description,
      routing_purpose: record.routing_purpose,
      routing_include_when: record.routing_include_when,
      routing_exclude_when: record.routing_exclude_when,
      routing_examples_positive: record.routing_examples_positive,
      routing_examples_negative: record.routing_examples_negative,
      routing_priority: Number.isFinite(priority) ? Math.max(0, Math.min(100, Math.round(priority))) : undefined,
    });
  });
  return rows;
}

export function isSystemFallbackTopic(topic: Pick<BotTopic, 'topic_key' | 'name' | 'description' | 'routing'>): boolean {
  const key = String(topic.topic_key || '').trim().toLowerCase();
  if (!SYSTEM_FALLBACK_TOPIC_KEYS.has(key)) return false;
  const routing = topic.routing && typeof topic.routing === 'object' ? topic.routing : {};
  const purpose = String((routing as Record<string, unknown>).purpose || '').trim().toLowerCase();
  const desc = String(topic.description || '').trim().toLowerCase();
  const name = String(topic.name || '').trim().toLowerCase();
  const priority = Number((routing as Record<string, unknown>).priority);
  if (purpose.includes('fallback')) return true;
  if (desc.includes('default topic')) return true;
  if (name === 'inbox') return true;
  if (Number.isFinite(priority) && priority <= 1) return true;
  return false;
}

export function normalizeRuntimeState(s?: string) {
  const raw = stateLabel(s);
  if (raw.includes('ERROR') || raw.includes('FAIL')) return 'ERROR';
  if (raw.includes('TOOL') || raw.includes('EXEC') || raw.includes('ACTION')) return 'TOOL_CALL';
  if (raw.includes('THINK') || raw.includes('PLAN') || raw.includes('REASON') || raw === 'RUNNING') return 'THINKING';
  if (raw.includes('SUCCESS') || raw.includes('DONE') || raw.includes('COMPLETE')) return 'SUCCESS';
  if (raw.includes('IDLE') || raw.includes('STOP')) return 'IDLE';
  return raw;
}

export function parseBotTimestamp(raw?: string | number | null) {
  if (typeof raw === 'number' && Number.isFinite(raw)) return raw;
  const text = String(raw || '').trim();
  if (!text) return 0;
  const ms = Date.parse(text);
  return Number.isFinite(ms) ? ms : 0;
}

export function sortBotsByCreatedAtDesc<T extends { id?: string | number | null; created_at?: string | number | null }>(
  bots: readonly T[],
): T[] {
  return [...bots].sort((left, right) => {
    const leftCreated = parseBotTimestamp(left.created_at);
    const rightCreated = parseBotTimestamp(right.created_at);
    if (leftCreated !== rightCreated) return rightCreated - leftCreated;
    return String(left.id || '').localeCompare(String(right.id || ''));
  });
}

export function normalizeWorkspaceExtension(raw: unknown): string {
  const value = String(raw ?? '').trim().toLowerCase();
  if (!value) return '';
  const stripped = value.replace(/^\*\./, '');
  const normalized = stripped.startsWith('.') ? stripped : `.${stripped}`;
  return /^\.[a-z0-9][a-z0-9._+-]{0,31}$/.test(normalized) ? normalized : '';
}

export function parseWorkspaceDownloadExtensions(
  raw: unknown,
  fallback: readonly string[] = EMPTY_WORKSPACE_DOWNLOAD_EXTENSIONS,
): string[] {
  if (raw === null || raw === undefined) return [...fallback];
  if (Array.isArray(raw) && raw.length === 0) return [];
  if (typeof raw === 'string' && raw.trim() === '') return [];
  const source = Array.isArray(raw) ? raw : String(raw || '').split(/[,\s;]+/);
  const rows: string[] = [];
  source.forEach((item) => {
    const ext = normalizeWorkspaceExtension(item);
    if (ext && !rows.includes(ext)) rows.push(ext);
  });
  return rows;
}

export function parseAllowedAttachmentExtensions(raw: unknown): string[] {
  if (raw === null || raw === undefined) return [];
  if (Array.isArray(raw) && raw.length === 0) return [];
  if (typeof raw === 'string' && raw.trim() === '') return [];
  const source = Array.isArray(raw) ? raw : String(raw || '').split(/[,\s;]+/);
  const rows: string[] = [];
  source.forEach((item) => {
    const ext = normalizeWorkspaceExtension(item);
    if (ext && !rows.includes(ext)) rows.push(ext);
  });
  return rows;
}

export function pathHasExtension(path: string, extensions: ReadonlySet<string>): boolean {
  const normalized = String(path || '').trim().toLowerCase();
  if (!normalized) return false;
  for (const ext of extensions) {
    if (normalized.endsWith(ext)) return true;
  }
  return false;
}

export function isDownloadOnlyPath(
  path: string,
  downloadExtensions: ReadonlySet<string> = EMPTY_WORKSPACE_DOWNLOAD_EXTENSION_SET,
) {
  return pathHasExtension(path, downloadExtensions);
}

export function isPreviewableWorkspaceFile(
  node: WorkspaceNode,
  downloadExtensions: ReadonlySet<string> = EMPTY_WORKSPACE_DOWNLOAD_EXTENSION_SET,
) {
  if (node.type !== 'file') return false;
  return isPreviewableWorkspacePath(node.path, downloadExtensions);
}

export function isImagePath(path: string) {
  return pathHasExtension(path, IMAGE_PREVIEW_EXTENSIONS);
}

export function isVideoPath(path: string) {
  return pathHasExtension(path, VIDEO_PREVIEW_EXTENSIONS);
}

export function isAudioPath(path: string) {
  return pathHasExtension(path, AUDIO_PREVIEW_EXTENSIONS);
}

export function isMediaUploadFile(file: File): boolean {
  const mime = String(file.type || '').toLowerCase();
  if (mime.startsWith('image/') || mime.startsWith('audio/') || mime.startsWith('video/')) {
    return true;
  }
  const name = String(file.name || '').trim().toLowerCase();
  const dot = name.lastIndexOf('.');
  if (dot < 0) return false;
  return MEDIA_UPLOAD_EXTENSIONS.has(name.slice(dot));
}

export function isHtmlPath(path: string) {
  return pathHasExtension(path, HTML_PREVIEW_EXTENSIONS);
}

export function isPreviewableWorkspacePath(
  path: string,
  downloadExtensions: ReadonlySet<string> = EMPTY_WORKSPACE_DOWNLOAD_EXTENSION_SET,
) {
  if (isDownloadOnlyPath(path, downloadExtensions)) return true;
  return (
    pathHasExtension(path, TEXT_PREVIEW_EXTENSIONS) ||
    isHtmlPath(path) ||
    isImagePath(path) ||
    isAudioPath(path) ||
    isVideoPath(path)
  );
}

export function workspaceFileAction(
  path: string,
  downloadExtensions: ReadonlySet<string> = EMPTY_WORKSPACE_DOWNLOAD_EXTENSION_SET,
): 'preview' | 'download' | 'unsupported' {
  const normalized = String(path || '').trim();
  if (!normalized) return 'unsupported';
  if (isDownloadOnlyPath(normalized, downloadExtensions)) return 'download';
  if (isImagePath(normalized) || isHtmlPath(normalized) || isVideoPath(normalized) || isAudioPath(normalized)) return 'preview';
  if (pathHasExtension(normalized, TEXT_PREVIEW_EXTENSIONS)) return 'preview';
  return 'unsupported';
}

export function renderWorkspacePathSegments(pathRaw: string, keyPrefix: string): ReactNode[] {
  const path = String(pathRaw || '');
  if (!path) return ['-'];
  const normalized = path.replace(/\\/g, '/');
  const hasLeadingSlash = normalized.startsWith('/');
  const parts = normalized.split('/').filter((part) => part.length > 0);
  const nodes: ReactNode[] = [];

  if (hasLeadingSlash) {
    nodes.push(<span key={`${keyPrefix}-root`} className="workspace-path-separator">/</span>);
  }

  parts.forEach((part, index) => {
    if (index > 0) {
      nodes.push(<span key={`${keyPrefix}-sep-${index}`} className="workspace-path-separator">/</span>);
    }
    nodes.push(<span key={`${keyPrefix}-part-${index}`} className="workspace-path-segment">{part}</span>);
  });

  return nodes.length > 0 ? nodes : ['-'];
}

export function normalizeAttachmentPaths(raw: unknown): string[] {
  if (!Array.isArray(raw)) return [];
  return raw
    .map((v) => String(v || '').trim().replace(/\\/g, '/'))
    .filter((v) => v.length > 0);
}

export interface ComposerDraftStorage {
  command: string;
  attachments: string[];
  updated_at_ms: number;
}

export function getComposerDraftStorageKey(botId: string): string {
  return `${COMPOSER_DRAFT_STORAGE_PREFIX}${String(botId || '').trim()}`;
}

export function loadComposerDraft(botId: string): ComposerDraftStorage | null {
  const id = String(botId || '').trim();
  if (!id || typeof window === 'undefined') return null;
  try {
    const raw = window.localStorage.getItem(getComposerDraftStorageKey(id));
    if (!raw) return null;
    const parsed = JSON.parse(raw) as Partial<ComposerDraftStorage> | null;
    const command = String(parsed?.command || '');
    const attachments = normalizeAttachmentPaths(parsed?.attachments)
      .map(normalizeDashboardAttachmentPath)
      .filter(Boolean);
    return {
      command,
      attachments,
      updated_at_ms: Number(parsed?.updated_at_ms || Date.now()),
    };
  } catch {
    return null;
  }
}

export function persistComposerDraft(botId: string, commandRaw: string, attachmentsRaw: string[]): void {
  const id = String(botId || '').trim();
  if (!id || typeof window === 'undefined') return;
  const command = String(commandRaw || '');
  const attachments = normalizeAttachmentPaths(attachmentsRaw)
    .map(normalizeDashboardAttachmentPath)
    .filter(Boolean);
  const key = getComposerDraftStorageKey(id);
  try {
    if (!command.trim() && attachments.length === 0) {
      window.localStorage.removeItem(key);
      return;
    }
    const payload: ComposerDraftStorage = {
      command,
      attachments,
      updated_at_ms: Date.now(),
    };
    window.localStorage.setItem(key, JSON.stringify(payload));
  } catch {
    // ignore localStorage write failures
  }
}

export function parseQuotedReplyBlock(input: string): { quoted: string; body: string } {
  const source = String(input || '');
  const match = source.match(/\[Quoted Reply\]\s*([\s\S]*?)\s*\[\/Quoted Reply\]/i);
  const quoted = normalizeAssistantMessageText(match?.[1] || '');
  const body = source.replace(/\[Quoted Reply\][\s\S]*?\[\/Quoted Reply\]\s*/gi, '').trim();
  return { quoted, body };
}

export function mergeConversation(messages: ChatMessage[]) {
  const merged: ChatMessage[] = [];
  messages
    .filter((msg) => msg.role !== 'system' && (msg.text.trim().length > 0 || (msg.attachments || []).length > 0))
    .forEach((msg) => {
      const parsedUser = msg.role === 'user' ? parseQuotedReplyBlock(msg.text) : { quoted: '', body: msg.text };
      const userQuoted = parsedUser.quoted;
      const userBody = parsedUser.body;
      const cleanText = msg.role === 'user' ? normalizeUserMessageText(userBody) : normalizeAssistantMessageText(msg.text);
      const attachments = normalizeAttachmentPaths(msg.attachments).map(normalizeDashboardAttachmentPath).filter(Boolean);
      if (!cleanText && attachments.length === 0 && !userQuoted) return;
      const last = merged[merged.length - 1];
      if (last && last.role === msg.role) {
        const normalizedLast = last.role === 'user' ? normalizeUserMessageText(last.text) : normalizeAssistantMessageText(last.text);
        const normalizedCurrent = msg.role === 'user' ? normalizeUserMessageText(cleanText) : normalizeAssistantMessageText(cleanText);
        const lastKind = last.kind || 'final';
        const currentKind = msg.kind || 'final';
        const sameAttachmentSet =
          JSON.stringify(normalizeAttachmentPaths(last.attachments)) === JSON.stringify(attachments);
        const sameQuoted = normalizeAssistantMessageText(last.quoted_reply || '') === normalizeAssistantMessageText(userQuoted);
        if (lastKind === currentKind && normalizedLast === normalizedCurrent && sameAttachmentSet && sameQuoted && Math.abs(msg.ts - last.ts) < 15000) {
          last.ts = msg.ts;
          last.id = msg.id || last.id;
          if (typeof msg.feedback !== 'undefined') {
            last.feedback = msg.feedback;
          }
          return;
        }
      }
      merged.push({ ...msg, text: cleanText, quoted_reply: userQuoted || undefined, attachments });
    });
  return merged.slice(-120);
}

export function clampTemperature(value: number) {
  if (Number.isNaN(value)) return 0.2;
  return Math.min(1, Math.max(0, value));
}

export function clampMaxTokens(value: number) {
  if (Number.isNaN(value)) return 8192;
  return Math.min(32768, Math.max(256, Math.round(value)));
}

export function clampCpuCores(value: number) {
  if (Number.isNaN(value)) return 1;
  if (value === 0) return 0;
  return Math.min(16, Math.max(0.1, Math.round(value * 10) / 10));
}

export function clampMemoryMb(value: number) {
  if (Number.isNaN(value)) return 1024;
  if (value === 0) return 0;
  return Math.min(65536, Math.max(256, Math.round(value)));
}

export function clampStorageGb(value: number) {
  if (Number.isNaN(value)) return 10;
  if (value === 0) return 0;
  return Math.min(1024, Math.max(1, Math.round(value)));
}

export function formatBytes(bytes: number): string {
  const value = Number(bytes || 0);
  if (!Number.isFinite(value) || value <= 0) return '0 B';
  const units = ['B', 'KB', 'MB', 'GB', 'TB'];
  const idx = Math.min(units.length - 1, Math.floor(Math.log(value) / Math.log(1024)));
  const scaled = value / Math.pow(1024, idx);
  return `${scaled >= 10 ? scaled.toFixed(1) : scaled.toFixed(2)} ${units[idx]}`;
}

export function formatPercent(value: number): string {
  const n = Number(value || 0);
  if (!Number.isFinite(n)) return '0.00%';
  return `${Math.max(0, n).toFixed(2)}%`;
}

export function formatWorkspaceTime(raw: string | undefined, isZh: boolean): string {
  const text = String(raw || '').trim();
  if (!text) return '-';
  const dt = new Date(text);
  if (Number.isNaN(dt.getTime())) return '-';
  try {
    return dt.toLocaleString(isZh ? 'zh-CN' : 'en-US', {
      year: 'numeric',
      month: 'long',
      day: 'numeric',
      weekday: 'long',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false,
    });
  } catch {
    return dt.toLocaleString();
  }
}

export function formatCronSchedule(job: CronJob, isZh: boolean) {
  const s = job.schedule || {};
  if (s.kind === 'every' && Number(s.everyMs) > 0) {
    const sec = Math.round(Number(s.everyMs) / 1000);
    return isZh ? `每 ${sec}s` : `every ${sec}s`;
  }
  if (s.kind === 'cron') {
    if (s.tz) return `${s.expr || '-'} (${s.tz})`;
    return s.expr || '-';
  }
  if (s.kind === 'at' && Number(s.atMs) > 0) {
    return new Date(Number(s.atMs)).toLocaleString();
  }
  return '-';
}

export function mapMcpResponseToDrafts(payload?: MCPConfigResponse | null): MCPServerDraft[] {
  const rows = payload?.mcp_servers && typeof payload.mcp_servers === 'object' ? payload.mcp_servers : {};
  return Object.entries(rows).map(([name, cfg]) => {
    const rawHeaders = cfg?.headers && typeof cfg.headers === 'object' ? cfg.headers : {};
    const headers: Record<string, string> = {};
    Object.entries(rawHeaders).forEach(([k, v]) => {
      const key = String(k || '').trim();
      if (!key) return;
      headers[key] = String(v ?? '').trim();
    });
    const headerEntries = Object.entries(headers);
    const botIdHeader = headerEntries.find(([k]) => String(k || '').trim().toLowerCase() === 'x-bot-id');
    const botSecretHeader = headerEntries.find(([k]) => String(k || '').trim().toLowerCase() === 'x-bot-secret');
    const type: MCPServerDraft['type'] = String(cfg?.type || 'streamableHttp') === 'sse' ? 'sse' : 'streamableHttp';
    return {
      name: String(name || '').trim(),
      type,
      url: String(cfg?.url || '').trim(),
      botId: String(botIdHeader?.[1] || '').trim(),
      botSecret: String(botSecretHeader?.[1] || '').trim(),
      toolTimeout: String(Number(cfg?.toolTimeout || 60) || 60),
      headers,
      locked: Boolean(cfg?.locked),
      originName: String(name || '').trim(),
    };
  });
}
