import { promises as fs } from 'node:fs'
import path from 'node:path'
import { ChapterWriter } from '../storage/adapters/ChapterWriter.js'
import { ThreadLedgerWriter } from '../storage/adapters/ThreadLedgerWriter.js'
import { EntityWriter } from '../storage/adapters/EntityWriter.js'
import { TimelineWriter } from '../storage/adapters/TimelineWriter.js'
import { SecretWriter } from '../storage/adapters/SecretWriter.js'
import { SummaryWriter } from '../storage/adapters/SummaryWriter.js'
import { createGit } from './git.js'
import { refreshCacheAfterSourceChange } from '../cache/index.js'
import { normalizeWorkspaceRel } from '../util/workspace-path.js'

/**
 * 定稿：原子 commit（D3）。写工作树 → git add → commit → 最后清工作区。
 * 断电安全：commit 前任何中断都回滚未提交写入（仅 定稿/大纲，不碰工作区），
 * 工作区草稿原样保留，不存在半章入档。
 *
 * @param {{repoPath: string, cache?: object}} ctx
 * @param {object} payload 定稿包（章档案/正文/摘要/条目/设定变更/commit 行/待清工作区文件）
 * @param {{git?: object, faultAfterWrite?: boolean}} [opts] 注入 git（测试）/故障注入（断电模拟）
 * @returns {Promise<{ok: boolean, commitHash?: string, error?: string}>}
 */
export async function finalizeChapter(ctx, payload, opts = {}) {
  const { repoPath } = ctx
  const git = opts.git || createGit(repoPath)
  const {
    chapterNum,
    frontMatter,
    body = '',
    summary = null,
    threadCreates = [],
    threadUpdates = [],
    characterUpdates = [],
    rosterUpserts = [],
    timelineRows = [],
    secretWrites = [],
    commitLines = {},
    workspaceFiles = [],
  } = payload

  // 1. 校验（不过则什么都没写，天然原样）
  if (!Number.isInteger(chapterNum)) return { ok: false, error: '章号必须是整数' }
  if (!frontMatter || !frontMatter.标题) return { ok: false, error: '缺少章档案或标题' }

  const stageFiles = []
  const rollbackFiles = []
  const chapterBackups = []
  let cacheRefresh = null
  try {
    // 2. 写工作树（全部落 定稿/大纲，非 工作区）
    const cw = await new ChapterWriter(repoPath).writeChapter(chapterNum, frontMatter, body, {
      preserveBackups: true,
    })
    if (!cw.ok) throw new Error(cw.error)
    stageFiles.push(cw.filePath, ...(cw.removedPaths || []))
    rollbackFiles.push(cw.filePath)
    chapterBackups.push(...(cw.backups || []))

    if (summary != null) {
      const sw = await new SummaryWriter(repoPath).writeChapterSummary(chapterNum, summary)
      if (!sw.ok) throw new Error(sw.error)
      stageFiles.push(sw.filePath)
      rollbackFiles.push(sw.filePath)
    }

    const tlw = new ThreadLedgerWriter(repoPath)
    // 新开条目先建档（开启类声明的入档执行体），再处理更新——同一章可"埋下并写首条履历"
    for (const t of threadCreates) {
      const r = await tlw.createThread(t)
      if (!r.ok) throw new Error(r.error)
      stageFiles.push(r.filePath)
      rollbackFiles.push(r.filePath)
    }
    for (const t of threadUpdates) {
      if (t.updates) {
        const r = await tlw.updateThread(t.id, t.updates)
        if (!r.ok) throw new Error(r.error)
      }
      if (t.history) {
        const r = await tlw.appendHistory(t.id, t.history)
        if (!r.ok) throw new Error(r.error)
      }
      const f = await tlw._findThreadFile(t.id)
      if (f) {
        stageFiles.push(f)
        rollbackFiles.push(f)
      }
    }

    const ew = new EntityWriter(repoPath)
    for (const c of characterUpdates) {
      const r = await ew.updateCharacter(c.name, c.updates)
      if (!r.ok) throw new Error(r.error)
      // 路径取写入器返回值（A10：文件名净化单源，防手拼路径与实际写点分裂）
      stageFiles.push(r.filePath)
      rollbackFiles.push(r.filePath)
    }
    for (const row of rosterUpserts) {
      const r = await ew.upsertRosterRow(row)
      if (!r.ok) throw new Error(r.error)
      const filePath = path.join(repoPath, '定稿', '设定', '名册.md')
      stageFiles.push(filePath)
      rollbackFiles.push(filePath)
    }

    const tw = new TimelineWriter(repoPath)
    for (const tr of timelineRows) {
      const r = await tw.appendRow(tr.volumeNum, tr.row)
      if (!r.ok) throw new Error(r.error)
      stageFiles.push(r.filePath)
      rollbackFiles.push(r.filePath)
    }

    const secw = new SecretWriter(repoPath)
    for (const s of secretWrites) {
      const r = await secw.write(s.id, s.frontMatter, s.content)
      if (!r.ok) throw new Error(r.error)
      stageFiles.push(r.filePath)
      rollbackFiles.push(r.filePath)
    }

    // 故障注入点（断电模拟，仅测试用）
    if (opts.faultAfterWrite) throw new Error('注入故障：写工作树后、commit 前中断')

    // 3. git add + commit（原子点）
    const relFiles = [...new Set(stageFiles)].map((f) => path.relative(repoPath, f))
    await git.add(relFiles)
    const commitHash = await git.commit(buildCommitMessage(chapterNum, frontMatter.标题, commitLines))

    for (const b of chapterBackups) {
      try {
        await fs.rm(b.backup, { force: true })
      } catch {
        // 备份残留不影响已完成 commit；文件名不以 .md 结尾，不进缓存扫描
      }
    }

    // P0-1：定稿后同步刷新缓存，避免 next 读旧章号重抄本章。
    // 重建失败不阻断定稿（已 commit 入档），但不能继续保留旧缓存，否则 next 会读旧章号。
    cacheRefresh = await refreshCacheAfterSourceChange(ctx)

    // 4. 清工作区（必须在 commit 成功之后；失败只记 warning——
    //    commit 已经发生，任何清理问题都不得把结果反转成"失败/已回滚"（R4））
    const warnings = await cleanWorkspaceFiles(repoPath, workspaceFiles)

    return { ok: true, commitHash, cacheRefresh, warnings, error: '' }
  } catch (err) {
    // commit 前中断：回滚本次 stage/rollback 集合（非整棵 定稿/大纲 子树,避免误伤同子树其他章手改）。
    // 逐文件 restore:新章文件未跟踪会让整条 restore 报错被吞,逐个跑才能精确复原已跟踪文件。
    const relStageFiles = [...new Set(stageFiles)].map((f) => path.relative(repoPath, f))
    const relRollbackFiles = [...new Set(rollbackFiles)].map((f) => path.relative(repoPath, f))
    try {
      for (const rel of relStageFiles) {
        await git.restore([rel])
      }
      await git.clean(relRollbackFiles)
      for (const b of chapterBackups.toReversed()) {
        await fs.rm(b.original, { force: true })
        await fs.rename(b.backup, b.original)
      }
      for (const b of chapterBackups) {
        const rel = path.relative(repoPath, b.original)
        if (!(await git.hasDiff([rel]))) await git.restore([rel])
      }
    } catch {
      // 回滚尽力而为；M3 git 健康检查兜底
    }
    return {
      ok: false,
      error: `定稿中断，已回滚未提交写入、工作区原样保留：${err.message}`,
    }
  }
}

function buildCommitMessage(chapterNum, title, lines) {
  let msg = `ch(${chapterNum}): ${title}`
  const extras = []
  if (lines.条目) extras.push(`条目: ${lines.条目}`)
  if (lines.设定) extras.push(`设定: ${lines.设定}`)
  if (extras.length) msg += '\n\n' + extras.join('\n')
  return msg
}

/**
 * commit 后的工作区清理：归一路径（剥前缀+拒 `..`，C2/G-4）、目录也能清（recursive），
 * 任何失败只收 warning 永不抛——本函数在原子点之后运行，不许影响定稿结果。
 */
async function cleanWorkspaceFiles(repoPath, workspaceFiles) {
  const warnings = []
  for (const wf of workspaceFiles) {
    const name = normalizeWorkspaceRel(wf)
    if (!name) {
      warnings.push(`工作区清理跳过可疑路径「${wf}」（不在工作区内）。`)
      continue
    }
    try {
      await fs.rm(path.join(repoPath, '工作区', name), { recursive: true, force: true })
    } catch (err) {
      warnings.push(`工作区/${name} 清理失败（${err.message}）——本章已入档，稍后手动删除即可。`)
    }
  }
  return warnings
}
