// 审稿 SSE 帧归一 + reducer（对齐 C3 / C4 扩 / ARCH §7.3）。
// 帧：`section{name,status}` / `conflict{type,where,refs,suggestion}` / `done{length}` / `error{...}`。
// 纯逻辑，便于 node 环境单测（不依赖 DOM）。

// 五类冲突（C6 ConflictType / ARCH §6.1）。
export type ConflictType =
  | "性格漂移"
  | "能力不符"
  | "设定违例"
  | "地理矛盾"
  | "时间线倒错";

export interface ReviewConflict {
  type: string;
  where: string;
  refs: string[];
  suggestion: string;
  // 一键采纳补丁对（可缺）：可局部修复时审稿提议，前端「采纳改法」据此 find-replace 进终稿。
  original: string | null;
  replacement: string | null;
}

export type SectionStatus = "started" | "done" | "incomplete";

// 伏笔建议（C4 扩 T3.3）：planted=新埋待确认 / resolved=疑似回收（只读建议）。
export type ForeshadowKind = "planted" | "resolved";

export interface ForeshadowSuggestion {
  kind: ForeshadowKind;
  code?: string | null;
  title: string;
  where?: string | null;
  note?: string | null;
}

// 节奏报告（C4 扩 T3.3）：注水段 + 章末钩子 + 爽点节拍图。
export interface PaceIssue {
  where: string;
  reason: string;
}

export interface PaceReport {
  water: PaceIssue[];
  hook: boolean;
  beat_map: number[];
}

// 文风漂移（第四审，C4 扩 T4.2）：整体相似度 score + 段级漂移。
// `text` = 该漂移段从草稿逐字摘录的原文（内容锚，供前端内容匹配定位回炉目标，
// 不靠位置 idx；旧数据/降级可为空串）。
export interface StyleDriftSegment {
  idx: number;
  text: string;
  score: number;
  label: string | null;
}

export interface StyleDriftReport {
  score: number;
  segments: StyleDriftSegment[];
}

// 人物塑造（灵感③ advisory 单维）：只读建议，不阻断验收（无裁决 UI）。
// 以人物卡 motive/appearance 为客观锚点；每条带逐字引用 quote + 置信度 confidence。
export interface CharacterizationIssue {
  character: string;
  aspect: string;
  where: string;
  quote: string;
  diagnosis: string;
  suggestion: string;
  confidence: number;
}

export interface CharacterizationReport {
  issues: CharacterizationIssue[];
}

export interface SectionEvent {
  event: "section";
  data: { name: string; status: SectionStatus };
}
export interface ConflictEvent {
  event: "conflict";
  data: ReviewConflict;
}
export interface ForeshadowEvent {
  event: "foreshadow";
  data: {
    kind: ForeshadowKind;
    code?: string | null;
    title: string;
    where?: string | null;
    note?: string | null;
  };
}
export interface PaceEvent {
  event: "pace";
  data: {
    water: PaceIssue[];
    hook: boolean;
    beat_map: number[];
  };
}
export interface StyleEvent {
  event: "style";
  data: {
    score: number;
    segments: {
      idx: number;
      text?: string | null;
      score: number;
      label?: string | null;
    }[];
  };
}
export interface CharacterizationEvent {
  event: "characterization";
  data: { issues: CharacterizationIssue[] };
}
export interface DoneEvent {
  event: "done";
  data: { length: number };
}
export interface ErrorEvent {
  event: "error";
  data: { code: string; message: string; request_id?: string | null };
}
export type ReviewSseEvent =
  | SectionEvent
  | ConflictEvent
  | ForeshadowEvent
  | PaceEvent
  | StyleEvent
  | CharacterizationEvent
  | DoneEvent
  | ErrorEvent;

const KNOWN_EVENTS = new Set([
  "section",
  "conflict",
  "foreshadow",
  "pace",
  "style",
  "characterization",
  "done",
  "error",
]);

// 把一个完整 SSE 块（多行）解析成事件；无法解析则返回 null（跳过）。
export function parseReviewBlock(block: string): ReviewSseEvent | null {
  let event = "";
  const dataLines: string[] = [];
  for (const rawLine of block.split("\n")) {
    const line = rawLine.replace(/\r$/, "");
    if (line.startsWith(":")) continue; // 注释/心跳
    const sep = line.indexOf(":");
    if (sep === -1) continue;
    const field = line.slice(0, sep);
    const value = line.slice(sep + 1).replace(/^ /, "");
    if (field === "event") event = value;
    else if (field === "data") dataLines.push(value);
  }
  if (!KNOWN_EVENTS.has(event) || dataLines.length === 0) return null;
  let data: unknown;
  try {
    data = JSON.parse(dataLines.join("\n"));
  } catch {
    return null;
  }
  return narrowReviewEvent(event, data);
}

function isRecord(v: unknown): v is Record<string, unknown> {
  return typeof v === "object" && v !== null && !Array.isArray(v);
}

function asStringArray(v: unknown): string[] {
  return Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : [];
}

function asNumberArray(v: unknown): number[] {
  return Array.isArray(v)
    ? v.filter((x): x is number => typeof x === "number" && Number.isFinite(x))
    : [];
}

function nullableString(v: unknown): string | null {
  return typeof v === "string" ? v : null;
}

function asPaceIssues(v: unknown): PaceIssue[] {
  if (!Array.isArray(v)) return [];
  return v.flatMap((x) =>
    isRecord(x) && typeof x.where === "string" && typeof x.reason === "string"
      ? [{ where: x.where, reason: x.reason }]
      : [],
  );
}

function asNumber(v: unknown, fallback = 0): number {
  return typeof v === "number" && Number.isFinite(v) ? v : fallback;
}

function asStringField(v: unknown): string {
  return typeof v === "string" ? v : "";
}

// 收窄人物塑造 issues（每字段安全默认，advisory 韧性；缺字段不丢整条）。
export function asCharacterizationIssues(v: unknown): CharacterizationIssue[] {
  if (!Array.isArray(v)) return [];
  return v.flatMap((x) =>
    isRecord(x)
      ? [
          {
            character: asStringField(x.character),
            aspect: asStringField(x.aspect),
            where: asStringField(x.where),
            quote: asStringField(x.quote),
            diagnosis: asStringField(x.diagnosis),
            suggestion: asStringField(x.suggestion),
            confidence: asNumber(x.confidence),
          },
        ]
      : [],
  );
}

function asStyleSegments(v: unknown): StyleEvent["data"]["segments"] {
  if (!Array.isArray(v)) return [];
  return v.flatMap((x) =>
    isRecord(x) && typeof x.idx === "number" && typeof x.score === "number"
      ? [
          {
            idx: x.idx,
            text: nullableString(x.text),
            score: x.score,
            label: nullableString(x.label),
          },
        ]
      : [],
  );
}

// 逐事件类型守卫：按 event 名收窄 data 形状；形状不符返回 null（安全跳过）。
function narrowReviewEvent(event: string, data: unknown): ReviewSseEvent | null {
  if (!isRecord(data)) return null;
  switch (event) {
    case "section":
      return typeof data.name === "string" && isSectionStatus(data.status)
        ? { event: "section", data: { name: data.name, status: data.status } }
        : null;
    case "conflict":
      return typeof data.type === "string" &&
        typeof data.where === "string" &&
        typeof data.suggestion === "string"
        ? {
            event: "conflict",
            data: {
              type: data.type,
              where: data.where,
              refs: asStringArray(data.refs),
              suggestion: data.suggestion,
              original: nullableString(data.original),
              replacement: nullableString(data.replacement),
            },
          }
        : null;
    case "foreshadow":
      return isForeshadowKind(data.kind) && typeof data.title === "string"
        ? {
            event: "foreshadow",
            data: {
              kind: data.kind,
              code: nullableString(data.code),
              title: data.title,
              where: nullableString(data.where),
              note: nullableString(data.note),
            },
          }
        : null;
    case "pace":
      return {
        event: "pace",
        data: {
          water: asPaceIssues(data.water),
          hook: data.hook === true,
          beat_map: asNumberArray(data.beat_map),
        },
      };
    case "style":
      return typeof data.score === "number"
        ? {
            event: "style",
            data: {
              score: data.score,
              segments: asStyleSegments(data.segments),
            },
          }
        : null;
    case "characterization":
      return {
        event: "characterization",
        data: { issues: asCharacterizationIssues(data.issues) },
      };
    case "done":
      return typeof data.length === "number"
        ? { event: "done", data: { length: data.length } }
        : null;
    case "error":
      return typeof data.code === "string" && typeof data.message === "string"
        ? {
            event: "error",
            data: {
              code: data.code,
              message: data.message,
              request_id: nullableString(data.request_id),
            },
          }
        : null;
    default:
      return null;
  }
}

function isSectionStatus(v: unknown): v is SectionStatus {
  return v === "started" || v === "done" || v === "incomplete";
}

function isForeshadowKind(v: unknown): v is ForeshadowKind {
  return v === "planted" || v === "resolved";
}

// 增量缓冲：吃进一段文本，吐出已完成的事件块（空行分隔），保留未完成尾部。
export class ReviewFrameBuffer {
  private buf = "";

  push(chunk: string): ReviewSseEvent[] {
    this.buf += chunk;
    const events: ReviewSseEvent[] = [];
    let idx: number;
    while ((idx = this.findBoundary(this.buf)) !== -1) {
      const block = this.buf.slice(0, idx);
      this.buf = this.buf.slice(this.boundaryEnd(this.buf, idx));
      const evt = parseReviewBlock(block);
      if (evt) events.push(evt);
    }
    return events;
  }

  private findBoundary(s: string): number {
    const a = s.indexOf("\n\n");
    const b = s.indexOf("\r\n\r\n");
    if (a === -1) return b;
    if (b === -1) return a;
    return Math.min(a, b);
  }

  private boundaryEnd(s: string, idx: number): number {
    return s.startsWith("\r\n\r\n", idx) ? idx + 4 : idx + 2;
  }
}

export type ReviewPhase = "idle" | "reviewing" | "done" | "error" | "aborted";

export interface ReviewSection {
  name: string;
  status: SectionStatus;
}

export interface ReviewStreamState {
  phase: ReviewPhase;
  sections: ReviewSection[];
  conflicts: ReviewConflict[];
  foreshadow: ForeshadowSuggestion[];
  pace: PaceReport | null;
  style: StyleDriftReport | null;
  characterization: CharacterizationReport | null;
  error: { code: string; message: string; request_id?: string | null } | null;
}

export const initialReviewState: ReviewStreamState = {
  phase: "idle",
  sections: [],
  conflicts: [],
  foreshadow: [],
  pace: null,
  style: null,
  characterization: null,
  error: null,
};

// 纯 reducer：把单个事件折叠进状态。section 按 name upsert（最后状态生效）。
export function reduceReview(
  state: ReviewStreamState,
  event: ReviewSseEvent,
): ReviewStreamState {
  switch (event.event) {
    case "section":
      return {
        ...state,
        phase: "reviewing",
        sections: upsertSection(state.sections, event.data),
      };
    case "conflict":
      return {
        ...state,
        phase: "reviewing",
        conflicts: [...state.conflicts, event.data],
      };
    case "foreshadow":
      return {
        ...state,
        phase: "reviewing",
        foreshadow: [...state.foreshadow, { ...event.data }],
      };
    case "pace":
      return {
        ...state,
        phase: "reviewing",
        pace: { ...event.data },
      };
    case "style":
      return {
        ...state,
        phase: "reviewing",
        style: {
          score: event.data.score,
          segments: event.data.segments.map((s) => ({
            idx: s.idx,
            text: s.text ?? "",
            score: s.score,
            label: s.label ?? null,
          })),
        },
      };
    case "characterization":
      return {
        ...state,
        phase: "reviewing",
        characterization: { issues: event.data.issues.map((i) => ({ ...i })) },
      };
    case "done":
      return { ...state, phase: "done" };
    case "error":
      return { ...state, phase: "error", error: event.data };
    default:
      return state;
  }
}

function upsertSection(
  sections: ReviewSection[],
  next: ReviewSection,
): ReviewSection[] {
  const idx = sections.findIndex((s) => s.name === next.name);
  if (idx === -1) return [...sections, next];
  return sections.map((s, i) => (i === idx ? next : s));
}
