import { Buffer } from "node:buffer"; import { createHash, createHmac, randomUUID } from "node:crypto"; import type { JsonValue } from "@geo/shared-types"; import { marked } from "marked"; import { extractImageSources, fetchImageBlob, normalizeRemoteUrl, sessionCookieValue, sessionFetchJson, } from "./common"; import type { PublishAdapter } from "./base"; type ZhihuMeResponse = { uid?: string; id?: string; name?: string; avatar_url?: string; }; type ZhihuImageTokenResponse = { upload_file?: { state?: number; object_key?: string; }; upload_token?: { access_id?: string; access_key?: string; access_token?: string; }; }; type ZhihuDraftCreateResponse = { id?: string; }; type ZhihuPublishResponse = { code?: number; }; function buildHeaders(headers?: HeadersInit): Headers { const next = new Headers(headers); if (!next.has("x-requested-with")) { next.set("x-requested-with", "fetch"); } return next; } async function zhihuFetch( context: Parameters>[0], input: string, init?: RequestInit, ): Promise { return await sessionFetchJson(context.session, input, { ...init, headers: buildHeaders(init?.headers), }); } function markdownToHtml(markdown: string): string { const source = markdown.trim(); if (!source) { return ""; } return marked.parse(source, { async: false, gfm: true, breaks: false, }) as string; } function baseContent(article: Parameters>[0]["article"]): string { const html = article.html_content?.trim(); if (html) { return html; } return markdownToHtml(article.markdown_content ?? ""); } function wrapStandaloneImages(html: string): string { return html.replace(/]+src="[^"]+"[^>]*)>/gi, "
"); } function convertTablesToZhihuFormat(html: string): string { let next = html; next = next.replace(/<\/thead>\s*/gi, ""); next = next.replace(//gi, ""); next = next.replace(/<\/thead>/gi, ""); next = next.replace( //gi, ""); next = next.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, ""); next = next.replace(/\sstyle="[^"]*"/gi, ""); next = next.replace(/\sdata-(?!draft)[a-z-]+="[^"]*"/gi, ""); next = next.replace(/]*>\s*()\s*<\/figure>/gi, "$1"); next = next.replace(/
/gi, '
');
  next = wrapStandaloneImages(next);
  next = convertTablesToZhihuFormat(next);

  return next.trim();
}

function md5Hex(buffer: ArrayBuffer): string {
  return createHash("md5").update(Buffer.from(buffer)).digest("hex");
}

function hmacSha1Base64(key: string, message: string): string {
  return createHmac("sha1", key).update(message).digest("base64");
}

function randomTraceId(): string {
  return `${Date.now()},${randomUUID()}`;
}

function buildPictureUrl(imageHash: string, mimeType: string): string {
  const extension = mimeType.split("/")[1] || "png";
  return `https://picx.zhimg.com/v2-${imageHash}.${extension}`;
}

async function uploadBinaryImage(
  context: Parameters>[0],
  sourceUrl: string,
): Promise {
  const blob = await fetchImageBlob(sourceUrl);
  if (!blob || !blob.type.startsWith("image/")) {
    return null;
  }

  const buffer = await blob.arrayBuffer();
  const imageHash = md5Hex(buffer);

  const token = await zhihuFetch(context, "https://api.zhihu.com/images", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      image_hash: imageHash,
      source: "article",
    }),
  });

  if (token.upload_file?.state === 2 && token.upload_file.object_key && token.upload_token) {
    const ossDate = new Date().toUTCString();
    const ossUserAgent = "aliyun-sdk-js/6.8.0 Chrome 139.0.0.0 on OS X 10.15.7 64-bit";
    const stringToSign = `PUT\n\n${blob.type}\n${ossDate}\nx-oss-date:${ossDate}\nx-oss-security-token:${token.upload_token.access_token}\nx-oss-user-agent:${ossUserAgent}\n/zhihu-pics/v2-${imageHash}`;
    const signature = hmacSha1Base64(token.upload_token.access_key || "", stringToSign);

    const uploadResponse = await fetch(`https://zhihu-pics-upload.zhimg.com/${token.upload_file.object_key}`, {
      method: "PUT",
      headers: {
        "content-type": blob.type,
        "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
        "x-oss-date": ossDate,
        "x-oss-user-agent": ossUserAgent,
        "x-oss-security-token": token.upload_token.access_token || "",
        authorization: `OSS ${token.upload_token.access_id}:${signature}`,
      },
      body: blob,
    });
    if (!uploadResponse.ok) {
      throw new Error(`zhihu_oss_upload_failed_${uploadResponse.status}`);
    }
  }

  return buildPictureUrl(imageHash, blob.type);
}

async function processContentImages(
  context: Parameters>[0],
  html: string,
): Promise {
  const sources = extractImageSources(html);
  if (!sources.length) {
    return html;
  }

  let next = html;
  const uploaded = new Map();

  for (const source of sources) {
    if (/zhimg\.com/i.test(source)) {
      continue;
    }
    const target = await uploadBinaryImage(context, source);
    if (target) {
      uploaded.set(source, target);
    }
  }

  for (const [from, to] of uploaded.entries()) {
    next = next.split(from).join(to);
  }

  return next;
}

async function detectZhihu(
  context: Parameters>[0],
): Promise {
  const me = await zhihuFetch(context, "https://www.zhihu.com/api/v4/me", {
    method: "GET",
  }).catch(() => null);

  const uid = me?.uid || me?.id ? String(me?.uid || me?.id) : "";
  if (!uid || !me?.name) {
    return null;
  }

  return {
    ...me,
    uid,
    avatar_url: normalizeRemoteUrl(me.avatar_url) ?? undefined,
  };
}

async function createDraft(
  context: Parameters>[0],
  titleImage: string | null,
): Promise {
  const title = context.article.title?.trim() || "未命名文章";
  const transformedContent = transformContent(baseContent(context.article));
  if (!transformedContent) {
    throw new Error("article_content_empty");
  }

  context.reportProgress("zhihu.upload_content_images");
  const content = await processContentImages(context, transformedContent);

  context.reportProgress("zhihu.create_draft");
  const created = await zhihuFetch(context, "https://zhuanlan.zhihu.com/api/articles/drafts", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      delta_time: 0,
      can_reward: false,
      title,
      content,
      ...(titleImage ? { titleImage } : {}),
    }),
  });

  if (!created.id) {
    throw new Error("zhihu_create_draft_failed");
  }
  return created.id;
}

async function publishDraft(
  context: Parameters>[0],
  draftId: string,
): Promise {
  const xsrfToken = await sessionCookieValue(context.session, "zhihu.com", "_xsrf");

  context.reportProgress("zhihu.publish_draft");
  const result = await zhihuFetch(context, "https://www.zhihu.com/api/v4/content/publish", {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-xsrftoken": xsrfToken,
    },
    body: JSON.stringify({
      action: "article",
      data: {
        publish: {
          traceId: randomTraceId(),
        },
        draft: {
          disabled: 1,
          id: draftId,
          isPublished: false,
        },
        commentsPermission: {
          comment_permission: "anyone",
        },
        creationStatement: {
          disclaimer_type: "",
          disclaimer_status: "",
        },
        contentsTables: {
          table_of_contents_enabled: false,
        },
        commercialReportInfo: {
          isReport: 0,
        },
        appreciate: {
          can_reward: false,
          tagline: "",
        },
        hybridInfo: {},
      },
    }),
  });

  return result.code === 0;
}

function buildResultPayload(
  articleId: string,
  mediaName: string,
  externalManageUrl: string,
  externalArticleUrl: string | null,
): Record {
  return {
    platform: "zhihu",
    media_name: mediaName,
    external_article_id: articleId,
    external_manage_url: externalManageUrl,
    external_article_url: externalArticleUrl,
    publish_type: "publish",
  };
}

function buildFailureResult(error: unknown): ReturnType extends Promise ? T : never {
  const message = error instanceof Error ? error.message : String(error);

  if (message === "article_content_empty") {
    return {
      status: "failed",
      summary: "文章内容为空,无法推送到知乎。",
      error: {
        code: "article_content_empty",
        message,
      },
    };
  }

  if (message === "zhihu_not_logged_in") {
    return {
      status: "failed",
      summary: "知乎登录态失效,无法执行发布。",
      error: {
        code: "zhihu_not_logged_in",
        message,
      },
    };
  }

  return {
    status: "failed",
    summary: "知乎发布失败。",
    error: {
      code: "zhihu_publish_failed",
      message,
    },
  };
}

export const zhihuAdapter: PublishAdapter = {
  platform: "zhihu",
  executionMode: "session",
  async publish(context) {
    context.reportProgress("zhihu.detect_login");
    const me = await detectZhihu(context);
    if (!me?.uid || !me.name) {
      return buildFailureResult(new Error("zhihu_not_logged_in"));
    }

    try {
      context.reportProgress("zhihu.upload_cover");
      const coverUrl = context.article.cover_asset_url?.trim() || null;
      const titleImage = coverUrl ? await uploadBinaryImage(context, coverUrl) : null;
      const draftId = await createDraft(context, titleImage);

      const published = await publishDraft(context, draftId);
      if (!published) {
        return buildFailureResult(new Error("zhihu_publish_failed"));
      }

      const publishedUrl = `https://zhuanlan.zhihu.com/p/${draftId}`;
      return {
        status: "succeeded",
        payload: buildResultPayload(draftId, me.name, `${publishedUrl}/edit`, publishedUrl),
        summary: "知乎发布成功。",
      };
    } catch (error) {
      return buildFailureResult(error);
    }
  },
};