import { SPEAKING_ARTICLE_API_BASE_URL } from './speakingApiConfig'
import type {
  ArticleClassSessionSummary,
  ArticleClassSummaryResponse,
  ArticleClassUser,
  ArticleReadingConfig,
  ArticleReadingReport,
} from '@/types/articleReading'

/**
 * 带 HTTP status 的失败。`message` 是后端那句**裸英文 detail**（如 `Session not found`），
 * 不是响应体 —— 它是给 `friendlyArticleError()` 当映射 key 用的机器可读值。
 * 任何直接把它渲染给人看的地方都是 bug（spec §9.2.5）。
 */
export class ArticleApiError extends Error {
  readonly status: number

  constructor(message: string, status: number) {
    super(message)
    this.name = 'ArticleApiError'
    this.status = status
  }
}

async function request<T>(path: string, init?: RequestInit): Promise<T> {
  const response = await fetch(`${SPEAKING_ARTICLE_API_BASE_URL}${path}`, init)
  if (!response.ok) {
    throw new ArticleApiError(await readDetail(response), response.status)
  }
  return response.json() as Promise<T>
}

/** FastAPI 一律回 `{"detail": "..."}`；拆不出来就退回 status，**绝不把整个响应体当文案**。 */
async function readDetail(response: Response): Promise<string> {
  const body = await response.text().catch(() => '')
  try {
    const parsed: unknown = JSON.parse(body)
    if (parsed && typeof parsed === 'object' && typeof (parsed as { detail?: unknown }).detail === 'string') {
      return (parsed as { detail: string }).detail
    }
  }
  catch {
    // 不是 JSON（网关 502 的 HTML 页面之类）—— 别把它当文案
  }
  return `Article API failed: ${response.status}`
}

const jsonHeaders = { 'Content-Type': 'application/json' }

/** `userId` 为 null = 编辑态老师预览（spec §4.5）：后端落 NULL，不进任何统计。 */
export function createArticleSession(configId: string, userId: string | null) {
  return request<ArticleReadingReport>('/session', {
    method: 'POST',
    headers: jsonHeaders,
    body: JSON.stringify({ configId, userId }),
  })
}

export function getArticleReport(sessionId: string, signal?: AbortSignal) {
  return request<ArticleReadingReport>(`/session/${encodeURIComponent(sessionId)}/report`, { signal })
}

/** 只读 resume 探针：该生该 config 最新 session 的 report；无则 session=null。绝不创建 session。 */
export function getLatestArticleSession(configId: string, userId: string) {
  const query = new URLSearchParams({ configId, userId }).toString()
  return request<{ session: ArticleReadingReport | null }>(`/sessions/latest?${query}`)
}

/** 幂等；已完成的 session 走后端第二分支，completedAt 可能为 null */
export function completeArticleSession(sessionId: string) {
  return request<{ sessionId: string; status: 'completed'; completedAt: string | null }>(
    `/session/${encodeURIComponent(sessionId)}/complete`,
    { method: 'POST' },
  )
}

export function getArticleDemoAudio(sessionId: string, paragraphIdx: number) {
  return request<{ url: string }>(
    `/session/${encodeURIComponent(sessionId)}/paragraph/${paragraphIdx}/demo-audio`,
  )
}

/** 老师端：请求体是 users:[{userId,name}]，不是 dialogue 的 userIds */
export function listArticleSessionsByConfig(configId: string, users: ArticleClassUser[]) {
  return request<{ summaries: ArticleClassSessionSummary[] }>('/sessions/by-config', {
    method: 'POST',
    headers: jsonHeaders,
    body: JSON.stringify({ configId, users }),
  })
}

/** 无 locale 参数：班级总结固定中文（spec §8.1） */
export function generateArticleClassSummary(configId: string, users: ArticleClassUser[]) {
  return request<ArticleClassSummaryResponse>('/sessions/by-config/summary', {
    method: 'POST',
    headers: jsonHeaders,
    body: JSON.stringify({ configId, users }),
  })
}

/**
 * 配置端自动分段（spec §3.2 / §6）：整篇送后端 → LLM → `\n\n` 分段纯文本。
 *
 * 后端 segmenter 超时 30s；模块内的 `request()` 本身不带超时，
 * 若后端挂住 fetch 会永远 pending，故这里自带 40s AbortController 兜底。
 */
export function segmentArticle(content: string) {
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), 40_000)
  return request<{ content: string }>('/segment', {
    method: 'POST',
    headers: jsonHeaders,
    body: JSON.stringify({ content }),
    signal: controller.signal,
  }).finally(() => clearTimeout(timer))
}

/** 「重新开始」：把当前 session 标作废（spec §4.2）。幂等；不建新 session —— 下次 createArticleSession 会因查不到未作废的行而新建。 */
export function abandonArticleSession(sessionId: string) {
  return request<{ sessionId: string; status: string }>(
    `/session/${encodeURIComponent(sessionId)}/abandon`,
    { method: 'POST' },
  )
}

/**
 * 文章专属的建立接口。响应形状与共用的 `POST /config` 一致。
 *
 * 为什么新建也要走文章自己的端点：后端在这里会把整篇的示范音频**同步**合成好，
 * 全部成功才写入配置。学生点「示范」时因此永远是 S3 直接命中，不用等合成。
 * 合成失败 = 完全没写入，老师重按一次就是完整的重试（已合成的段落留在 S3，
 * 重试只补剩下的），所以不需要任何「半份配置」的收拾逻辑。
 *
 * 共用的 `POST /config` 拿到什么存什么，给不了「TTS 成功才写入」这个语义，
 * 而把它塞进去就会让通用端点长出只对一种题型生效的行为。
 */
export function createArticleConfig(config: ArticleReadingConfig, ownerUid: string | null = null) {
  return request<{ id: string; config: ArticleReadingConfig; ownerUid: string | null }>(
    '/config',
    { method: 'POST', headers: jsonHeaders, body: JSON.stringify({ config, ownerUid }) },
  )
}

/**
 * 文章专属的保存接口（spec §4.3 B）。响应形状与共用的 `PUT /config/{id}` 一致。
 * 同 `createArticleConfig`：写入前先把整篇示范音频备齐，全部成功才写。
 *
 * 有人正在练时后端回 409，detail 带着人数 —— 那是**问句不是墙**：`handleApply` 据此弹确认框，
 * 老师选「仍要保存」就带 `force=true` 原样重发一次，后端照写。所以面板不需要预先问人数、
 * 也不需要置灰（`GET /active-count` 已随之删除）。
 */
export function updateArticleConfig(
  configId: string,
  config: ArticleReadingConfig,
  force = false,
) {
  const query = force ? '?force=true' : ''
  return request<{ id: string; config: ArticleReadingConfig; ownerUid: string | null }>(
    `/config/${encodeURIComponent(configId)}${query}`,
    { method: 'PUT', headers: jsonHeaders, body: JSON.stringify({ config }) },
  )
}
