import { Buffer } from 'node:buffer' import { createHash, createHmac, randomUUID } from 'node:crypto' import type { JsonValue } from '@geo/shared-types' import { marked } from 'marked' import type { PublishAdapter } from './base' import { extractImageSources, fetchImageBlob, normalizeRemoteUrl, sessionCookieValue, sessionFetchJson, } from './common' 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)
    }
  },
}