import { api } from "../services/api/client";
import type { AgentChatReply, AgentChatResponse, AgentChatResultResponse, ChatMessage, ConversationRecord, SessionTab, WorkspaceRecord } from "../types/domain";
import { firstNonEmptyString, nextProtocolId, nonEmptyString, relativeUpdatedLabel, shortToken } from "../utils";
import { WORKBENCH_PROJECT_ID, workspaceProjectId } from "./constants";

export function messagesFromWorkspace(workspace: WorkspaceRecord | null): ChatMessage[] {
  const selected = workspace?.selectedConversation?.messages;
  if (Array.isArray(selected)) return selected;
  const workspaceMessages = workspace?.workspace?.messages;
  return Array.isArray(workspaceMessages) ? workspaceMessages : [];
}

export function makeMessage(role: "user" | "agent", text: string, status: ChatMessage["status"], meta: { traceId: string; conversationId: string; sessionId: string | null; threadId: string | null; title?: string }): ChatMessage {
  return { id: nextProtocolId("msg"), role, title: meta.title ?? (role === "user" ? "用户" : "Code Agent"), text, status, traceId: meta.traceId, conversationId: meta.conversationId, sessionId: meta.sessionId, threadId: meta.threadId, createdAt: new Date().toISOString() };
}

export function mergeConversationMessages(existing: ChatMessage[], incoming: ChatMessage[] = []): ChatMessage[] {
  const merged: ChatMessage[] = [];
  for (const message of [...existing, ...incoming]) {
    const index = merged.findIndex((item) => sameConversationMessage(item, message));
    const current = index >= 0 ? merged[index] : null;
    if (current) merged[index] = mergeConversationMessage(current, message);
    else merged.push(message);
  }
  return merged;
}

function mergeConversationMessage(existing: ChatMessage, next: ChatMessage): ChatMessage {
  return {
    ...existing,
    ...next,
    title: firstNonEmptyString(next.title, existing.title) ?? next.title,
    text: firstNonEmptyString(next.text, existing.text) ?? next.text,
    runnerTrace: next.runnerTrace ?? existing.runnerTrace,
    traceEvents: next.traceEvents ?? existing.traceEvents,
    error: next.error ?? existing.error,
    updatedAt: next.updatedAt ?? existing.updatedAt
  };
}

function sameConversationMessage(left: ChatMessage, right: ChatMessage): boolean {
  if (left.id && right.id && left.id === right.id) return true;
  if (!left.traceId || !right.traceId || left.traceId !== right.traceId || left.role !== right.role) return false;
  return !left.conversationId || !right.conversationId || left.conversationId === right.conversationId;
}

export function messageFromAgentResponse(messageId: string, pending: ChatMessage, response: AgentChatResponse | AgentChatResultResponse): ChatMessage {
  const errorObject = typeof response.error === "object" && response.error ? response.error : null;
  const failed = Boolean(response.error) || ["failed", "blocked", "timeout", "cancelled"].includes(String(response.status ?? ""));
  const replyText = replyContentText(response.reply);
  const text = firstNonEmptyString(
    response.assistantText,
    replyText,
    response.reply,
    response.text,
    terminalAssistantText(response),
    response.summary,
    errorObject?.message,
    typeof response.error === "string" ? response.error : null
  );
  const normalizedStatus = failed ? "failed" : response.status === "running" ? "running" : "completed";
  return {
    ...pending,
    id: messageId,
    title: failed ? "Code Agent 返回阻塞" : normalizedStatus === "running" ? "Code Agent 处理中" : "Code Agent 回复",
    text: text ?? (failed ? "后端没有返回可显示正文。" : "Code Agent 已完成，但本次结果缺少可显示的 final response；请回放 trace 或重试。"),
    status: normalizedStatus,
    updatedAt: new Date().toISOString(),
    traceId: response.traceId ?? pending.traceId,
    conversationId: response.conversationId ?? pending.conversationId,
    sessionId: response.sessionId ?? pending.sessionId,
    threadId: response.threadId ?? pending.threadId,
    runnerTrace: response.runnerTrace ?? null,
    traceEvents: response.traceEvents,
    error: errorObject ? { ...errorObject, message: errorObject.message ?? "Code Agent failed" } : null
  };
}

function replyContentText(reply: AgentChatReply | string | undefined): string | null {
  if (!reply) return null;
  if (typeof reply === "string") return reply;
  return firstNonEmptyString(reply.content, reply.messageId);
}

function terminalAssistantText(response: AgentChatResponse | AgentChatResultResponse): string | null {
  const trace = response.runnerTrace && typeof response.runnerTrace === "object" ? response.runnerTrace : null;
  const traceStreams = unknownArray(trace?.assistantStreams);
  const responseStreams = unknownArray((response as { assistantStreams?: unknown }).assistantStreams);
  const streams = traceStreams.length > 0 ? traceStreams : responseStreams;
  for (const stream of [...streams].reverse()) {
    if (!stream || typeof stream !== "object") continue;
    const item = stream as Record<string, unknown>;
    const text = firstNonEmptyString(item.text, item.lastChunk);
    if (text) return text;
  }
  const responseEvents = unknownArray((response as { events?: unknown }).events);
  const traceEvents = unknownArray(response.traceEvents);
  const runnerEvents = unknownArray(trace?.events);
  const events = responseEvents.length > 0 ? responseEvents : traceEvents.length > 0 ? traceEvents : runnerEvents;
  for (const event of [...events].reverse()) {
    if (!event || typeof event !== "object") continue;
    const item = event as Record<string, unknown>;
    if (!isTerminalAssistantEvent(item)) continue;
    const text = firstNonEmptyString(item.message, item.outputSummary, item.text, item.chunk);
    if (text) return text;
  }
  return null;
}

function unknownArray(value: unknown): unknown[] {
  return Array.isArray(value) ? value : [];
}

function isTerminalAssistantEvent(event: Record<string, unknown>): boolean {
  const label = String(event.label ?? "");
  const type = String(event.type ?? "");
  if (label === "assistant:completed") return true;
  if (label === "agentrun:assistant:message") return event.replyAuthority === true || event.final === true;
  return type === "assistant_message" && (event.status === "completed" || event.final === true || event.terminal === true);
}

export async function ensureWorkspace(current: WorkspaceRecord | null, projectId = WORKBENCH_PROJECT_ID): Promise<WorkspaceRecord | null> {
  if (current?.workspaceId && workspaceProjectId(current, projectId) === projectId) return current;
  const response = await api.workspace(projectId);
  return response.ok ? response.data?.workspace ?? null : null;
}

export async function persistConversation(input: { workspace: WorkspaceRecord | null; projectId?: string; conversationId: string; sessionId: string | null; threadId: string | null; messages: ChatMessage[] }): Promise<WorkspaceRecord | null> {
  const projectId = workspaceProjectId(input.workspace, input.projectId ?? WORKBENCH_PROJECT_ID);
  const messages = mergeConversationMessages(input.messages);
  const latest = messages.at(-1);
  await api.saveConversation(input.conversationId, {
    projectId,
    conversationId: input.conversationId,
    sessionId: input.sessionId,
    threadId: input.threadId,
    sessionStatus: latest?.status ?? "source",
    lastTraceId: latest?.traceId,
    messages,
    snapshot: { source: "cloud-web-react-account-sync", updatedAt: new Date().toISOString(), sessionStatus: latest?.status ?? "source" }
  });
  if (input.workspace?.workspaceId) {
    const response = await api.updateWorkspace(input.workspace.workspaceId, {
      projectId,
      expectedRevision: input.workspace.revision,
      selectedConversationId: input.conversationId,
      selectedAgentSessionId: input.sessionId,
      threadId: input.threadId,
      activeTraceId: messages.findLast((message) => message.status === "running")?.traceId ?? null,
      messages,
      workspace: { source: "cloud-web-react-workspace-sync", updatedAt: new Date().toISOString() },
      updatedByClient: "cloud-web-react"
    });
    return response.ok ? response.data?.workspace ?? input.workspace : input.workspace;
  }
  return input.workspace;
}

export function conversationsToTabs(conversations: ConversationRecord[], activeConversationId: string | null): SessionTab[] {
  return conversations.map((conversation) => conversationToTab(conversation, activeConversationId)).sort((left, right) => timestampMs(right.updatedAt) - timestampMs(left.updatedAt));
}

function conversationToTab(conversation: ConversationRecord, activeConversationId: string | null): SessionTab {
  const sessionId = firstNonEmptyString(conversation.sessionId, conversation.session?.sessionId);
  const conversationId = conversation.conversationId;
  const updatedAt = firstNonEmptyString(conversation.updatedAt, conversation.snapshot?.updatedAt, conversation.startedAt);
  const status = firstNonEmptyString(conversation.snapshot?.sessionStatus, conversation.status, conversation.messages?.at(-1)?.status) ?? "source";
  const trace = firstNonEmptyString(conversation.lastTraceId, conversation.messages?.at(-1)?.traceId);
  const preview = firstNonEmptyString(conversation.firstUserMessagePreview, conversation.snapshot?.firstUserMessagePreview, conversation.messages?.find((message) => message.role === "user")?.text);
  return { key: sessionId ?? conversationId, label: truncateLabel(preview) ?? `Session ${shortToken(sessionId ?? conversationId, 8)}`, subtitle: `${relativeUpdatedLabel(timestampMs(updatedAt))} · ${status === "running" ? "处理中" : trace ? `trace ${shortToken(trace, 8)}` : "未开始"}`, projectId: conversation.projectId, conversationId, sessionId, threadId: firstNonEmptyString(conversation.threadId, conversation.session?.threadId), lastTraceId: trace, status, messageCount: Number.isFinite(Number(conversation.messageCount)) ? Number(conversation.messageCount) : conversation.messages?.length ?? 0, active: conversationId === activeConversationId, updatedAt, userPreview: preview };
}

function truncateLabel(value: string | null): string | null {
  if (!value) return null;
  const text = value.replace(/\s+/gu, " ").trim();
  return text.length <= 36 ? text : `${text.slice(0, 36).trimEnd()}...`;
}

function timestampMs(value: unknown): number {
  const text = nonEmptyString(value);
  if (!text) return 0;
  const parsed = Date.parse(text);
  return Number.isFinite(parsed) ? parsed : 0;
}
