import { lang } from '@/main'
import { ArticleApiError } from './articleReading'

/**
 * 后端英文 detail → 面向用户的本地化文案（spec §9.2.5）。
 *
 * 形状照话题讨论的 `friendlyErrorMessage`（`useDialogueEngine.ts:586-601`），
 * 但有一处**故意不同**：话题讨论未命中时 `return map[raw] || raw`，把英文原文漏了出去。
 * 这里未命中一律回通用兜底 —— 后端的 detail 全是英文短句，学生看不懂，而且
 * `request()` 早期版本会把整个 `{"detail":"..."}` 塞进 message，漏出去就是原始 JSON。
 *
 * 映射表在函数体内构造：`lang` 是响应式对象，模块顶层展开会把语言冻在首次 import 那一刻。
 */
export function friendlyArticleError(cause: unknown): string {
  const detail = extractDetail(cause)
  if (!detail) return lang.ssArticleErrGeneric as string

  const map: Record<string, string> = {
    'Config not found': lang.ssArticleErrConfigNotFound as string,
    'Config is not an article config': lang.ssArticleErrNotArticleConfig as string,
    'Article content is empty': lang.ssArticleErrContentEmpty as string,
    'Session not found': lang.ssArticleErrSessionNotFound as string,
    'Paragraph not found': lang.ssArticleErrParagraphNotFound as string,
    'Session is abandoned': lang.ssArticleSessionExpired as string,
    'Session has ended': lang.ssArticleErrSessionEnded as string,
    // 总评开始后重录被拒（spec §4.1 规则 4，2026-07-27）。视图本该已经把录音锁上了，
    // 撞到这条只剩陈旧标签页那一种 —— 但陈旧标签页也不该看到英文原文。
    'Report already generated': lang.ssArticleErrReportGenerated as string,
    'Segmentation failed': lang.ssArticleSegmentFailed as string,
    'Segmentation returned empty output': lang.ssArticleSegmentFailed as string,
    'content is required': lang.ssArticleErrBadRequest as string,
    'configId is required': lang.ssArticleErrBadRequest as string,
    'userId is required': lang.ssArticleErrBadRequest as string,
    'users is required': lang.ssArticleErrBadRequest as string,
    'users capped at 100': lang.ssArticleErrBadRequest as string,
  }

  // `Demo audio synthesis failed: [2, 5]` 带着段号，所以走正则而不是查表。
  // 段号要留给老师 —— 「第几段生成失败」是他唯一能采取行动的信息（去看那一段是不是
  // 异常长、或纯符号）。文案还要说清「配置未保存」：保存是全成功才写入，失败是
  // 一个干净的 no-op，老师需要知道自己的修改还没进去、重按一次就是完整重试。
  const demoFailed = /^Demo audio synthesis failed: \[([\d,\s]+)\]$/.exec(detail)
  if (demoFailed) {
    const paragraphs = demoFailed[1].split(',').map(n => n.trim()).filter(Boolean).join('、')
    return String(lang.ssArticleErrDemoAudio).replace('{n}', paragraphs)
  }

  // `Students are practising: N` **故意不在这张表里**（spec §4.3 B，2026-07-24 改成确认框）：
  // 它不是错误而是问句，`handleApply` 在 catch 里先用 articleActiveCountFromError() 截住它去弹
  // 确认框，老师选「仍要保存」就带 force 重发。真漏到这里只能是别处误用，走通用兜底即可。
  return map[detail] ?? (lang.ssArticleErrGeneric as string)
}

/**
 * 409 `Students are practising: N` 里的那个 N；不是这条错误 → null。
 *
 * 后端把人数编进 detail 就是为了让这一步不必再请求一次 —— 多问那一次恰恰又会拿到
 * **另一个时刻**的数字，而弹框里那个数字必须是老师按下那一刻的（spec §4.3 B）。
 */
export function articleActiveCountFromError(cause: unknown): number | null {
  const detail = typeof cause === 'string' ? cause : extractDetail(cause)
  if (!detail) return null
  const matched = /^Students are practising: (\d+)$/.exec(detail)
  return matched ? Number(matched[1]) : null
}

/** 后端 409 = 这一轮已被作废（spec §4.2）。视图据此改成「请重新开始」而不是给重试按钮。 */
export function isArticleSessionGone(cause: unknown): boolean {
  return cause instanceof ArticleApiError && cause.status === 409
}

/**
 * 从各种 throw 形状里刨出后端那句裸英文。三种来源都要吃得下：
 *  - `articleReading.ts` 的 `request()`：已经拆好的裸 detail
 *  - WS 错误帧（`articleReadingStream.ts:42`）：同样是裸 detail
 *  - `@/services/speaking` 的 `parse()`：`[404] {"detail":"..."}`（共用端点，改不动它）
 */
function extractDetail(cause: unknown): string | null {
  const raw = cause instanceof Error ? cause.message : typeof cause === 'string' ? cause : ''
  if (!raw) return null
  const jsonStart = raw.indexOf('{')
  if (jsonStart < 0) return raw
  try {
    const parsed: unknown = JSON.parse(raw.slice(jsonStart))
    if (parsed && typeof parsed === 'object' && typeof (parsed as { detail?: unknown }).detail === 'string') {
      return (parsed as { detail: string }).detail
    }
  }
  catch {
    // 不是 JSON —— 原样当英文短句处理，命不中就走兜底
  }
  return raw
}
