import {
  buildApiUrl,
  buildAuthHeaders,
  isRecord,
  readJsonResponse,
  serverRequest,
  isServerRequestError,
  throwResponseError,
} from "./serverConnection";
import { isOptionalApiRouteMissing } from "./apiErrorUtils";
import type { WebGenerationPreviewTask } from "../types";

export interface ImageGenInput {
  projectId?: string;
  conversationId?: number;
  model: string;
  prompt: string;
  ratio?: string;
  quality?: string;
  gridMode?: string;
  referenceUrls?: string[];
}

export interface ImageProviderDebug {
  requestedModel?: string;
  effectiveModel?: string;
  primaryProvider?: string;
  fallbackProviders?: string[];
  route?: string[];
  candidates?: Array<{
    provider?: string;
    transport?: string;
    model?: string;
    requestedModel?: string;
    billingProvider?: string;
    fallbackOf?: string;
  }>;
}

export interface ImageTaskCreateResponse {
  taskId: string;
  providerDebug?: ImageProviderDebug;
}

type ImageRouteDebugEntry = Record<string, unknown> & {
  at: string;
  label: string;
};

export interface VideoGenInput {
  projectId?: string;
  conversationId?: number;
  model: string;
  prompt: string;
  ratio?: string;
  duration?: number;
  quality?: string;
  resolution?: string;
  frameMode?: string;
  referenceUrls?: string[];
  imageUrl?: string;
  audioUrl?: string;
  muted?: boolean;
  hasReferenceVideo?: boolean;
  style?: "speech" | "sing" | "performance" | string;
}

export interface VideoEditInput {
  projectId?: string;
  conversationId?: number;
  videoUrl: string;
  referenceUrls: string[];
  prompt?: string;
  model?: string;
  ratio?: string;
  resolution?: string;
}

export interface VideoSuperResolveInput {
  projectId?: string;
  conversationId?: number;
  videoUrl: string;
  bitRate?: number;
  provider?: string;
  style?: number;
  videoFps?: number;
  minLen?: 540 | 720;
  useSR?: boolean;
  animateEmotion?: boolean;
}

export interface EraseSubtitlesInput {
  videoUrl: string;
  bx?: number;
  by?: number;
  bw?: number;
  bh?: number;
}

export interface ImageEditInput {
  imageUrl: string;
  function: string;
  prompt?: string;
  n?: number;
}

export interface ImageSuperResolveInput {
  projectId?: string;
  conversationId?: number;
  imageUrl: string;
  scale?: "2x" | "4x" | number;
}

export interface UploadAssetInput {
  dataUrl: string;
  name?: string;
  mimeType?: string;
  scope?: "profile-avatar" | "profile-background" | string;
}

export interface UploadAssetByUrlInput {
  sourceUrl: string;
  name?: string;
  mimeType?: string;
  scope?: string;
}

export type ChatMessageContent =
  | string
  | Array<{ type: "text"; text: string } | { type: "image_url"; image_url: { url: string } }>;

export interface ChatInput {
  model: string;
  messages: Array<{ role: string; content: ChatMessageContent }>;
  stream?: boolean;
  temperature?: number;
}

export interface ChatUsage {
  promptTokens?: number;
  completionTokens?: number;
  totalTokens?: number;
}

export interface AiTaskStatus {
  taskId: string;
  projectId?: string;
  conversationId?: number | null;
  clientQueueId?: string | null;
  type: "image" | "video";
  status: "pending" | "running" | "completed" | "failed" | "cancelled";
  progress: number;
  progressSource?: "real" | "estimated" | string | null;
  stage?: string | null;
  startedAt?: string | null;
  expectedDurationMs?: number | null;
  resultUrl: string | null;
  error: string | null;
  params?: Record<string, unknown>;
  createdAt: string;
  updatedAt: string;
  completedAt?: string | null;
}

function normalizeTaskStatus(status: AiTaskStatus["status"]): WebGenerationPreviewTask["status"] {
  if (status === "running" || status === "completed" || status === "failed") return status;
  if (status === "cancelled") return "failed";
  return "queued";
}

function taskTitle(task: AiTaskStatus): string {
  const prompt = typeof task.params?.prompt === "string" ? task.params.prompt.trim() : "";
  if (prompt) return prompt.length > 20 ? `${prompt.slice(0, 20)}...` : prompt;
  return task.type === "video" ? "\u89c6\u9891\u751f\u6210\u4efb\u52a1" : "\u56fe\u50cf\u751f\u6210\u4efb\u52a1";
}

function toPreviewTask(task: AiTaskStatus): WebGenerationPreviewTask {
  return {
    id: task.taskId,
    title: taskTitle(task),
    type: task.type,
    status: normalizeTaskStatus(task.status),
    progress: Math.max(0, Math.min(100, Math.trunc(task.progress || 0))),
    prompt: typeof task.params?.prompt === "string" ? task.params.prompt : taskTitle(task),
    createdAt: task.createdAt,
    projectId: task.projectId || undefined,
    outputUrl: task.resultUrl || undefined,
    source: "server",
    errorMessage: task.error || undefined,
  };
}

function parseContentDispositionFilename(value: string | null): string | undefined {
  if (!value) return undefined;
  const utf8Match = value.match(/filename\*=UTF-8''([^;]+)/i);
  if (utf8Match?.[1]) {
    try {
      return decodeURIComponent(utf8Match[1].trim());
    } catch {
      return utf8Match[1].trim();
    }
  }
  const plainMatch = value.match(/filename="?([^";]+)"?/i);
  return plainMatch?.[1]?.trim() || undefined;
}

function extractTaskList(payload: unknown): AiTaskStatus[] {
  if (Array.isArray(payload)) return payload as AiTaskStatus[];
  if (!isRecord(payload)) return [];
  const rows = payload.tasks ?? payload.items;
  return Array.isArray(rows) ? (rows as AiTaskStatus[]) : [];
}

function getStoredSessionRole(): string {
  try {
    if (typeof window === "undefined") return "";
    const raw = window.localStorage.getItem("omniai-web-session");
    if (!raw) return "";
    const session = JSON.parse(raw);
    return String(session?.user?.role || "").trim().toLowerCase();
  } catch {
    return "";
  }
}

function emitImageRouteDebug(label: string, payload: Record<string, unknown>): void {
  // Only emit console logs for admin users — hides enterprise routing details
  if (getStoredSessionRole() === "admin") {
    const entry: ImageRouteDebugEntry = {
      at: new Date().toISOString(),
      label,
      ...payload,
    };
    try {
      console.log(`${label} ${JSON.stringify(entry)}`);
    } catch {
      console.log(label, entry);
    }
  }

  if (typeof window === "undefined") return;
  const debugWindow = window as Window & { __OMNIAI_IMAGE_ROUTE_DEBUG__?: ImageRouteDebugEntry[] };
  const previousEntries = Array.isArray(debugWindow.__OMNIAI_IMAGE_ROUTE_DEBUG__)
    ? debugWindow.__OMNIAI_IMAGE_ROUTE_DEBUG__
    : [];
  const entry: ImageRouteDebugEntry = { at: new Date().toISOString(), label, ...payload };
  debugWindow.__OMNIAI_IMAGE_ROUTE_DEBUG__ = [...previousEntries.slice(-19), entry];
}

let taskHistoryRouteMissing = false;

const TASK_SUBMIT_TIMEOUT_MS = 90_000;
const TASK_STATUS_TIMEOUT_MS = 20_000;
const NON_RETRYING_REQUEST = { maxRetries: 0 };
const PENDING_CANCEL_TASKS_KEY = "omniai:pending-task-cancellations";

function readPendingCancelTaskIds(): string[] {
  if (typeof window === "undefined") return [];
  try {
    const raw = window.localStorage.getItem(PENDING_CANCEL_TASKS_KEY);
    const parsed = raw ? JSON.parse(raw) : [];
    return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === "string" && id.trim().length > 0) : [];
  } catch {
    return [];
  }
}

function writePendingCancelTaskIds(taskIds: string[]): void {
  if (typeof window === "undefined") return;
  try {
    const uniqueIds = Array.from(new Set(taskIds.filter(Boolean)));
    if (uniqueIds.length) {
      window.localStorage.setItem(PENDING_CANCEL_TASKS_KEY, JSON.stringify(uniqueIds));
    } else {
      window.localStorage.removeItem(PENDING_CANCEL_TASKS_KEY);
    }
  } catch {
    // Pending cancellation recovery is best-effort.
  }
}

function markTaskCancelPending(taskId: string): void {
  writePendingCancelTaskIds([...readPendingCancelTaskIds(), taskId]);
}

function clearPendingTaskCancel(taskId: string): void {
  writePendingCancelTaskIds(readPendingCancelTaskIds().filter((id) => id !== taskId));
}

function shouldRetryTaskCancel(error: unknown): boolean {
  if (!isServerRequestError(error)) return true;
  const status = error.status;
  return status === 429 || status === undefined || status >= 500;
}

export const aiGenerationClient = {
  async createImageTask(input: ImageGenInput): Promise<ImageTaskCreateResponse> {
    const requestUrl = buildApiUrl("ai/image");
    emitImageRouteDebug("[ai/image-request]", {
      url: requestUrl,
      model: input.model,
      ratio: input.ratio,
      quality: input.quality,
      gridMode: input.gridMode,
      referenceCount: input.referenceUrls?.length || 0,
      projectId: input.projectId,
      conversationId: input.conversationId,
    });
    const payload = await serverRequest<ImageTaskCreateResponse>("ai/image", {
      method: "POST",
      body: input,
      timeoutMs: TASK_SUBMIT_TIMEOUT_MS,
      maxRetries: NON_RETRYING_REQUEST.maxRetries,
      fallbackMessage: "Image generation request failed",
    });
    if (payload.providerDebug) {
      emitImageRouteDebug("[ai/image-provider-debug]", payload.providerDebug as Record<string, unknown>);
    }
    return payload;
  },

  async createVideoTask(input: VideoGenInput): Promise<{ taskId: string }> {
    return serverRequest<{ taskId: string }>("ai/video", {
      method: "POST",
      body: input,
      timeoutMs: TASK_SUBMIT_TIMEOUT_MS,
      maxRetries: NON_RETRYING_REQUEST.maxRetries,
      fallbackMessage: "Video generation request failed",
    });
  },

  async createVideoSuperResolveTask(input: VideoSuperResolveInput): Promise<{ taskId: string }> {
    return serverRequest<{ taskId: string }>("ai/video/super-resolve", {
      method: "POST",
      body: input,
      timeoutMs: TASK_SUBMIT_TIMEOUT_MS,
      maxRetries: NON_RETRYING_REQUEST.maxRetries,
      fallbackMessage: "Video super-resolution request failed",
    });
  },

  async createEraseSubtitlesTask(input: EraseSubtitlesInput): Promise<{ taskId: string }> {
    return serverRequest<{ taskId: string }>("ai/video/erase-subtitles", {
      method: "POST",
      body: input,
      timeoutMs: TASK_SUBMIT_TIMEOUT_MS,
      maxRetries: NON_RETRYING_REQUEST.maxRetries,
      fallbackMessage: "Subtitle removal request failed",
    });
  },

  async createVideoEditTask(input: VideoEditInput): Promise<{ taskId: string }> {
    return serverRequest<{ taskId: string }>("ai/video/edit", {
      method: "POST",
      body: { ...input, model: input.model || "happyhorse-1.0-video-edit" },
      timeoutMs: TASK_SUBMIT_TIMEOUT_MS,
      maxRetries: NON_RETRYING_REQUEST.maxRetries,
      fallbackMessage: "Video edit request failed",
    });
  },

  async createImageSuperResolveTask(input: ImageSuperResolveInput): Promise<{ taskId: string }> {
    return serverRequest<{ taskId: string }>("ai/image/super-resolve", {
      method: "POST",
      body: input,
      timeoutMs: TASK_SUBMIT_TIMEOUT_MS,
      maxRetries: NON_RETRYING_REQUEST.maxRetries,
      fallbackMessage: "Image super-resolution request failed",
    });
  },

  async createImageEditTask(input: ImageEditInput): Promise<{ taskId: string }> {
    return serverRequest<{ taskId: string }>("ai/image/edit", {
      method: "POST",
      body: input,
      timeoutMs: TASK_SUBMIT_TIMEOUT_MS,
      maxRetries: NON_RETRYING_REQUEST.maxRetries,
      fallbackMessage: "Image edit request failed",
    });
  },

  async cancelTask(taskId: string): Promise<void> {
    markTaskCancelPending(taskId);
    try {
      await serverRequest<void>(`ai/tasks/${taskId}/cancel`, {
        method: "PATCH",
        maxRetries: NON_RETRYING_REQUEST.maxRetries,
        fallbackMessage: "Task cancel failed",
      });
      clearPendingTaskCancel(taskId);
    } catch (error) {
      if (isOptionalApiRouteMissing(error) || !shouldRetryTaskCancel(error)) {
        clearPendingTaskCancel(taskId);
        return;
      }
      throw error;
    }
  },

  cancelTaskOnUnload(taskId: string): void {
    markTaskCancelPending(taskId);
    const url = buildApiUrl(`ai/tasks/${encodeURIComponent(taskId)}/cancel`);
    const headers = buildAuthHeaders();
    const body = JSON.stringify({ reason: "page_unload" });

    try {
      void fetch(url, {
        method: "PATCH",
        headers,
        body,
        credentials: "include",
        keepalive: true,
      });
    } catch {
      // Page unload cancellation is best-effort.
    }
  },

  flushPendingTaskCancellations(): void {
    readPendingCancelTaskIds().forEach((taskId) => {
      this.cancelTask(taskId).catch(() => {});
    });
  },

  async getTaskStatus(taskId: string): Promise<AiTaskStatus> {
    return serverRequest<AiTaskStatus>(`ai/tasks/${taskId}`, {
      timeoutMs: TASK_STATUS_TIMEOUT_MS,
      fallbackMessage: "Task status request failed",
    });
  },

  async downloadTaskResult(taskId: string): Promise<{ blob: Blob; filename?: string; contentType?: string }> {
    const res = await fetch(buildApiUrl(`ai/tasks/${encodeURIComponent(taskId)}/download`), {
      method: "GET",
      headers: buildAuthHeaders(),
    });
    if (!res.ok) {
      await throwResponseError(res, "Task result download failed");
    }
    const blob = await res.blob();
    return {
      blob,
      filename: parseContentDispositionFilename(res.headers.get("content-disposition")),
      contentType: res.headers.get("content-type") || blob.type || undefined,
    };
  },

  async listTasks(params?: { limit?: number; status?: string; type?: string; projectId?: string }): Promise<WebGenerationPreviewTask[]> {
    if (taskHistoryRouteMissing) return [];
    const search = new URLSearchParams();
    if (params?.limit) search.set("limit", String(params.limit));
    if (params?.status) search.set("status", params.status);
    if (params?.type) search.set("type", params.type);
    if (params?.projectId) search.set("projectId", params.projectId);
    try {
      const payload = await serverRequest<unknown>(`ai/tasks${search.toString() ? `?${search}` : ""}`, {
        fallbackMessage: "Task history request failed",
      });
      return extractTaskList(payload).map(toPreviewTask);
    } catch (error) {
      if (isOptionalApiRouteMissing(error)) {
        taskHistoryRouteMissing = true;
        return [];
      }
      throw error;
    }
  },

  async bindTaskToConversation(taskId: string, conversationId: number): Promise<void> {
    try {
      await serverRequest<void>(`ai/tasks/${taskId}/conversation`, {
        method: "PATCH",
        body: { conversationId },
        maxRetries: NON_RETRYING_REQUEST.maxRetries,
        fallbackMessage: "Task conversation binding failed",
      });
    } catch (error) {
      if (isOptionalApiRouteMissing(error)) return;
      throw error;
    }
  },

  async uploadAsset(input: UploadAssetInput): Promise<{ url: string; signedUrl?: string; ossKey?: string }> {
    return serverRequest<{ url: string; signedUrl?: string; ossKey?: string }>("oss/upload", {
      method: "POST",
      body: input,
      maxRetries: NON_RETRYING_REQUEST.maxRetries,
      fallbackMessage: "Asset upload failed",
    });
  },

  async uploadAssetBinary(blob: Blob, options?: { name?: string; mimeType?: string; scope?: string }): Promise<{ url: string; signedUrl?: string; ossKey?: string }> {
    const form = new FormData();
    form.append("file", blob, options?.name || "upload.png");
    if (options?.scope) form.append("scope", options.scope);
    if (options?.mimeType) form.append("mimeType", options.mimeType);
    // Exclude Content-Type so browser auto-sets multipart/form-data with boundary
    const { "Content-Type": _ct, ...authHeaders } = buildAuthHeaders();
    const res = await fetch(buildApiUrl("oss/upload-binary"), {
      method: "POST",
      headers: authHeaders,
      body: form,
    });
    if (!res.ok) {
      await throwResponseError(res, "Binary asset upload failed");
    }
    return readJsonResponse<{ url: string; ossKey?: string }>(res, "Binary asset upload response failed");
  },

  async uploadAssetByUrl(input: UploadAssetByUrlInput): Promise<{ url: string; signedUrl?: string; ossKey?: string }> {
    return serverRequest<{ url: string; signedUrl?: string; ossKey?: string }>("oss/upload-by-url", {
      method: "POST",
      body: input,
      maxRetries: NON_RETRYING_REQUEST.maxRetries,
      fallbackMessage: "Asset upload by URL failed",
    });
  },

  subscribeTaskStatus(
    taskId: string,
    onUpdate: (
      task: Pick<
        AiTaskStatus,
        | "taskId"
        | "status"
        | "progress"
        | "progressSource"
        | "stage"
        | "startedAt"
        | "expectedDurationMs"
        | "resultUrl"
        | "error"
      >,
    ) => void,
  ): () => void {
    const url = buildApiUrl(`ai/tasks/${taskId}/stream`);
    const controller = new AbortController();

    (async () => {
      try {
        const res = await fetch(url, {
          headers: { ...buildAuthHeaders(), Accept: "text/event-stream" },
          signal: controller.signal,
        });
        if (!res.ok || !res.body) return;

        const reader = res.body.getReader();
        const decoder = new TextDecoder();
        let buffer = "";

        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split("\n");
          buffer = lines.pop() || "";

          for (const line of lines) {
            if (!line.startsWith("data: ")) continue;
            try {
              const data = JSON.parse(line.slice(6));
              onUpdate(data);
            } catch { /* ignore */ }
          }
        }
      } catch { /* aborted or network error */ }
    })();

    return () => controller.abort();
  },

  async streamChat(
    input: ChatInput,
    onChunk: (text: string) => void,
    signal?: AbortSignal,
    onUsage?: (usage: ChatUsage) => void,
  ): Promise<void> {
    const res = await fetch(buildApiUrl("ai/chat"), {
      method: "POST",
      headers: buildAuthHeaders(),
      body: JSON.stringify({ ...input, stream: true }),
      signal,
    });
    if (!res.ok) {
      await throwResponseError(res, "Chat request failed");
    }

    const reader = res.body?.getReader();
    if (!reader) throw new Error("\u65e0\u6cd5\u8bfb\u53d6\u54cd\u5e94\u6d41");

    const decoder = new TextDecoder();
    let buffer = "";

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split("\n");
      buffer = lines.pop() || "";

      for (const line of lines) {
        if (!line.startsWith("data: ")) continue;
        const payload = line.slice(6).trim();
        if (!payload) continue;
        try {
          const chunk = JSON.parse(payload) as {
            delta?: string;
            done?: boolean;
            error?: string;
            usage?: ChatUsage & {
              prompt_tokens?: number;
              completion_tokens?: number;
              total_tokens?: number;
            };
          };
          if (chunk.error) throw new Error(chunk.error);
          if (chunk.usage) {
            onUsage?.({
              promptTokens: chunk.usage.promptTokens ?? chunk.usage.prompt_tokens,
              completionTokens: chunk.usage.completionTokens ?? chunk.usage.completion_tokens,
              totalTokens: chunk.usage.totalTokens ?? chunk.usage.total_tokens,
            });
          }
          if (chunk.delta) onChunk(chunk.delta);
          if (chunk.done) return;
        } catch (e) {
          if (e instanceof SyntaxError) continue;
          throw e;
        }
      }
    }
  },

  async chatCompletion(input: ChatInput, signal?: AbortSignal): Promise<string> {
    const res = await fetch(buildApiUrl("ai/chat"), {
      method: "POST",
      headers: buildAuthHeaders(),
      body: JSON.stringify({ ...input, stream: false }),
      signal,
    });
    if (!res.ok) {
      await throwResponseError(res, "Chat completion failed");
    }
    const json = await readJsonResponse<{ content?: string }>(res, "Chat completion response failed");
    return (json as { content?: string }).content || "";
  },
};
