import type {
  ChatDocumentAttachment,
  ChatImageAttachment,
} from '@easyflow/common-ui';

export type WorkflowFinalOutputKind =
  | 'documents'
  | 'images'
  | 'json'
  | 'key-value'
  | 'list'
  | 'media'
  | 'scalar'
  | 'table';

export interface WorkflowFinalOutputSection {
  documents: ChatDocumentAttachment[];
  images: ChatImageAttachment[];
  key: string;
  kind: WorkflowFinalOutputKind;
  label: string;
  listItems: string[];
  rawValue: unknown;
  scalarText: string;
  tableColumns: string[];
  tableRows: Record<string, unknown>[];
}

export interface WorkflowFinalOutputView {
  empty: boolean;
  sections: WorkflowFinalOutputSection[];
}

const IMAGE_EXTENSIONS = new Set([
  'bmp',
  'gif',
  'jpeg',
  'jpg',
  'png',
  'svg',
  'webp',
]);
const TABLE_COLUMN_LIMIT = 8;
const THINKING_TAG_PATTERN = /<\s*(?:(\/)\s*)?(?:thinking|think)\b[^>]*>/gi;

/**
 * 移除工作流展示文本中的旧思考标签内容块。
 */
export function sanitizeWorkflowOutputTextForDisplay(value: string): string {
  let cursor = 0;
  let thinkingDepth = 0;
  let sanitized = '';

  for (const match of value.matchAll(THINKING_TAG_PATTERN)) {
    const matchIndex = match.index;
    if (thinkingDepth === 0) {
      sanitized += value.slice(cursor, matchIndex);
    }
    if (match[1]) {
      thinkingDepth = Math.max(0, thinkingDepth - 1);
    } else {
      thinkingDepth += 1;
    }
    cursor = matchIndex + match[0].length;
  }

  if (thinkingDepth === 0) {
    sanitized += value.slice(cursor);
  }
  return sanitized;
}

/**
 * 递归移除工作流展示值中的旧思考标签内容块。
 */
export function sanitizeWorkflowOutputForDisplay(
  value: unknown,
  seen = new WeakMap<object, unknown>(),
): unknown {
  if (typeof value === 'string') {
    return sanitizeWorkflowOutputTextForDisplay(value);
  }
  if (!value || typeof value !== 'object') {
    return value;
  }
  const existing = seen.get(value);
  if (existing !== undefined) {
    return existing;
  }
  if (Array.isArray(value)) {
    const sanitized: unknown[] = [];
    seen.set(value, sanitized);
    value.forEach((item) => {
      sanitized.push(sanitizeWorkflowOutputForDisplay(item, seen));
    });
    return sanitized;
  }
  if (!isPlainObject(value)) {
    return value;
  }
  const sanitized: Record<string, unknown> = {};
  seen.set(value, sanitized);
  Object.entries(value).forEach(([key, item]) => {
    sanitized[key] = sanitizeWorkflowOutputForDisplay(item, seen);
  });
  return sanitized;
}

/**
 * 将工作流顶级最终输出整理为稳定的展示分区。
 */
export function buildWorkflowFinalOutputView(
  output: unknown,
): WorkflowFinalOutputView {
  if (isPlainObject(output) && Object.keys(output).length === 0) {
    return { empty: true, sections: [] };
  }
  if (output === undefined) {
    return { empty: true, sections: [] };
  }

  const entries =
    isPlainObject(output) && !isMediaDescriptor(output)
      ? Object.entries(output)
      : [['output', output] as const];
  return {
    empty: entries.length === 0,
    sections: entries.map(([key, value]) => buildSection(key, value)),
  };
}

/**
 * 创建有界 JSON 预览，避免超大结果一次性挂载到 DOM。
 */
export function formatWorkflowOutputPreview(
  value: unknown,
  expanded = false,
): string {
  const preview = boundedPreview(value, {
    arrayLimit: expanded ? 100 : 12,
    depthLimit: expanded ? 6 : 3,
    keyLimit: expanded ? 100 : 16,
    stringLimit: expanded ? 20_000 : 2000,
  });
  try {
    return JSON.stringify(preview, null, 2);
  } catch {
    return String(value ?? '');
  }
}

/**
 * 将完整输出序列化为复制文本。
 */
export function serializeWorkflowOutput(value: unknown): string {
  const sanitizedValue = sanitizeWorkflowOutputForDisplay(value);
  if (typeof sanitizedValue === 'string') {
    return sanitizedValue;
  }
  try {
    return JSON.stringify(sanitizedValue, null, 2);
  } catch {
    return String(sanitizedValue ?? '');
  }
}

function buildSection(key: string, value: unknown): WorkflowFinalOutputSection {
  const media = resolveMedia(value);
  const base: WorkflowFinalOutputSection = {
    documents: media.documents,
    images: media.images,
    key,
    kind: 'json',
    label: key,
    listItems: [],
    rawValue: value,
    scalarText: '',
    tableColumns: [],
    tableRows: [],
  };
  if (media.images.length > 0 && media.documents.length === 0) {
    return { ...base, kind: 'images' };
  }
  if (media.documents.length > 0 && media.images.length === 0) {
    return { ...base, kind: 'documents' };
  }
  if (media.images.length > 0 && media.documents.length > 0) {
    return { ...base, kind: 'media' };
  }
  if (isScalar(value)) {
    return {
      ...base,
      kind: 'scalar',
      scalarText: formatScalar(value),
    };
  }
  if (Array.isArray(value)) {
    if (value.every((item) => isScalar(item))) {
      return {
        ...base,
        kind: 'list',
        listItems: value.map((item) => formatScalar(item)),
      };
    }
    const columns = homogeneousTableColumns(value);
    if (columns.length > 0) {
      return {
        ...base,
        kind: 'table',
        tableColumns: columns,
        tableRows: value as Record<string, unknown>[],
      };
    }
    return base;
  }
  if (
    isPlainObject(value) &&
    Object.values(value).every((item) => isScalar(item))
  ) {
    return { ...base, kind: 'key-value' };
  }
  return base;
}

function homogeneousTableColumns(value: unknown[]): string[] {
  if (value.length === 0 || !value.every((item) => isPlainObject(item))) {
    return [];
  }
  const firstKeys = Object.keys(value[0] as Record<string, unknown>);
  if (
    firstKeys.length === 0 ||
    firstKeys.length > TABLE_COLUMN_LIMIT ||
    !value.every((row) => {
      const keys = Object.keys(row as Record<string, unknown>);
      return (
        keys.length === firstKeys.length &&
        keys.every((key, index) => key === firstKeys[index]) &&
        Object.values(row as Record<string, unknown>).every((item) =>
          isScalar(item),
        )
      );
    })
  ) {
    return [];
  }
  return firstKeys;
}

function resolveMedia(value: unknown) {
  const candidates = Array.isArray(value) ? value : [value];
  if (
    candidates.length === 0 ||
    !candidates.every((candidate) => isMediaDescriptor(candidate))
  ) {
    return {
      documents: [] as ChatDocumentAttachment[],
      images: [] as ChatImageAttachment[],
    };
  }
  const documents: ChatDocumentAttachment[] = [];
  const images: ChatImageAttachment[] = [];
  for (const candidate of candidates) {
    if (isImageDescriptor(candidate)) {
      const source = mediaSource(candidate);
      images.push({
        mimeType: stringValue(candidate.contentType || candidate.mimeType),
        name: sanitizeWorkflowOutputTextForDisplay(
          mediaName(candidate, source, '图片'),
        ),
        previewUrl: source,
        size: numberValue(candidate.size),
        status: 'ready',
      });
      continue;
    }
    const source = mediaSource(candidate);
    documents.push({
      attachmentRef: stringValue(candidate.attachmentRef) || undefined,
      downloadUrl: source,
      mimeType: stringValue(candidate.contentType || candidate.mimeType),
      name: sanitizeWorkflowOutputTextForDisplay(
        mediaName(candidate, source, '文件'),
      ),
      size: numberValue(candidate.size),
      status: 'ready',
    });
  }
  return { documents, images };
}

function isMediaDescriptor(value: unknown): value is Record<string, unknown> {
  return isImageDescriptor(value) || isDocumentDescriptor(value);
}

function isImageDescriptor(value: unknown): boolean {
  if (!isPlainObject(value)) {
    return false;
  }
  const source = mediaSource(value);
  if (!source) {
    return false;
  }
  const mimeType = stringValue(
    value.contentType || value.mimeType,
  ).toLowerCase();
  if (mimeType.startsWith('image/')) {
    return true;
  }
  const name = mediaName(value, source, '');
  return IMAGE_EXTENSIONS.has(fileExtension(name || source));
}

function isDocumentDescriptor(value: unknown): boolean {
  if (!isPlainObject(value) || isImageDescriptor(value)) {
    return false;
  }
  const source = mediaSource(value);
  const name = stringValue(value.fileName || value.name);
  return Boolean(source && (name || value.attachmentRef));
}

function mediaSource(value: Record<string, unknown>): string {
  return stringValue(
    value.downloadUrl || value.url || value.filePath || value.previewUrl,
  );
}

function mediaName(
  value: Record<string, unknown>,
  source: string,
  fallback: string,
): string {
  const explicit = stringValue(value.fileName || value.name);
  if (explicit) {
    return explicit;
  }
  const fromUrl = source.split('/').pop()?.split('?')[0] || '';
  return fromUrl || fallback;
}

function fileExtension(value: string): string {
  return value.toLowerCase().split('.').pop()?.split('?')[0] || '';
}

function formatScalar(value: unknown): string {
  if (value === null) {
    return '空值';
  }
  if (typeof value === 'string') {
    return sanitizeWorkflowOutputTextForDisplay(value);
  }
  return String(value);
}

function isScalar(value: unknown): boolean {
  return (
    value === null ||
    typeof value === 'boolean' ||
    typeof value === 'number' ||
    typeof value === 'string'
  );
}

function isPlainObject(value: unknown): value is Record<string, unknown> {
  return Boolean(value && typeof value === 'object' && !Array.isArray(value));
}

function stringValue(value: unknown): string {
  return typeof value === 'string' ? value.trim() : '';
}

function numberValue(value: unknown): number | undefined {
  const parsed = Number(value);
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
}

function boundedPreview(
  value: unknown,
  limits: {
    arrayLimit: number;
    depthLimit: number;
    keyLimit: number;
    stringLimit: number;
  },
  depth = 0,
  seen = new WeakSet<object>(),
): unknown {
  if (typeof value === 'string') {
    const sanitizedValue = sanitizeWorkflowOutputTextForDisplay(value);
    return sanitizedValue.length <= limits.stringLimit
      ? sanitizedValue
      : `${sanitizedValue.slice(0, limits.stringLimit)}…`;
  }
  if (isScalar(value) || value === undefined) {
    return value;
  }
  if (depth >= limits.depthLimit) {
    return Array.isArray(value) ? '[数组已折叠]' : '{对象已折叠}';
  }
  if (value && typeof value === 'object') {
    if (seen.has(value)) {
      return '[循环引用]';
    }
    seen.add(value);
  }
  if (Array.isArray(value)) {
    const items = value
      .slice(0, limits.arrayLimit)
      .map((item) => boundedPreview(item, limits, depth + 1, seen));
    if (value.length > limits.arrayLimit) {
      items.push(`… 另有 ${value.length - limits.arrayLimit} 项`);
    }
    return items;
  }
  if (isPlainObject(value)) {
    const keys = Object.keys(value);
    const preview: Record<string, unknown> = {};
    for (const key of keys.slice(0, limits.keyLimit)) {
      preview[key] = boundedPreview(value[key], limits, depth + 1, seen);
    }
    if (keys.length > limits.keyLimit) {
      preview['…'] = `另有 ${keys.length - limits.keyLimit} 个字段`;
    }
    return preview;
  }
  return String(value);
}
