import SwiftUI
import SwiftData

/// 时间线条目反查到的源记录,驱动只读详情 sheet。
/// 注:报告详情这里是 W2 轻量只读版;W4 的 C2 `ReportDetailView`(三 Tab + 对比上次)另建,
/// 届时把时间线报告行改路由到 C2 即可,本类型不与之冲突。
enum TimelineDetail {
    case indicator(Indicator)
    case bloodPressure(sys: Indicator, dia: Indicator?)
    case report(Report)
    case diary(DiaryEntry)
    case symptom(Symptom)

    /// 把时间线条目反查回源记录(id 形如 `<kind>-<persistentModelID>` / `bp-<sysID>-<diaID>`)。
    /// 主页「最近记录」与档案库 C1 共用同一套反查,避免逻辑重复。无法定位源记录时返回 nil。
    static func resolve(for entry: TimelineEntry,
                        indicators: [Indicator],
                        reports: [Report],
                        diaries: [DiaryEntry],
                        symptoms: [Symptom]) -> TimelineDetail? {
        switch entry.kind {
        case .report:
            return reports.first { "report-\($0.persistentModelID)" == entry.id }
                .map(TimelineDetail.report)
        case .diary, .medication:
            // 用药记录本质是带「用药」tag 的 DiaryEntry,详情同日记。
            return diaries.first { "diary-\($0.persistentModelID)" == entry.id }
                .map(TimelineDetail.diary)
        case .symptom:
            return symptoms.first { "symptom-\($0.persistentModelID)" == entry.id }
                .map(TimelineDetail.symptom)
        case .indicator:
            if let i = indicators.first(where: { "indicator-\($0.persistentModelID)" == entry.id }) {
                return .indicator(i)
            }
            // 合并血压条目:bp-<sysID>-<diaID>
            if entry.id.hasPrefix("bp-"),
               let sys = indicators.first(where: { entry.id.hasPrefix("bp-\($0.persistentModelID)-") }) {
                // 用 id 里编码的 diaID 精确反查,不再用 ±5s 时间窗近似匹配
                //(后者在同日多次量血压时会把详情配到错误的舒张读数)。
                let dia = indicators.first { entry.id.hasSuffix("-\($0.persistentModelID)") }
                return .bloodPressure(sys: sys, dia: dia)
            }
            return nil
        }
    }
}

/// 时间线条目的只读详情:展示该记录的完整字段。各类型一屏看完,不可编辑。
struct TimelineEntryDetailView: View {
    @Environment(\.dismiss) private var dismiss
    @Environment(\.modelContext) private var ctx
    let detail: TimelineDetail

    @State private var showDeleteConfirm = false
    @State private var evidenceTarget: Indicator?
    @State private var reminderPrefill: ReminderPrefill?

    /// 「用药记录」点药 → 预填吃药提醒表单用的载体。
    private struct ReminderPrefill: Identifiable {
        let id = UUID()
        let title: String
        let note: String
    }

    /// 报告详情「查看原图」起始页载体。
    @State private var reportPhotoStart: ReportPhotoPage?
    private struct ReportPhotoPage: Identifiable {
        let id = UUID()
        let index: Int
    }

    /// 当前详情若是报告则取出,供「查看原图」用。
    private var reportEntry: Report? {
        if case .report(let r) = detail { return r }
        return nil
    }

    var body: some View {
        VStack(spacing: 0) {
            header
            ScrollView {
                VStack(alignment: .leading, spacing: 16) {
                    bodyContent
                    deleteButton
                }
                .padding(.horizontal, 20)
                .padding(.vertical, 16)
                .frame(maxWidth: .infinity, alignment: .leading)
            }
        }
        .background(Tj.Palette.sand.ignoresSafeArea())
        .presentationDetents([.medium, .large])
        .presentationDragIndicator(.visible)
        .presentationBackground(Tj.Palette.sand)
        .presentationCornerRadius(Tj.Radius.xl)
        .alert(String(appLoc: "永久删除这条记录?"), isPresented: $showDeleteConfirm) {
            Button(String(appLoc: "删除"), role: .destructive) { performDelete() }
            Button(String(appLoc: "取消"), role: .cancel) { }
        } message: {
            Text("删除后无法恢复。")
        }
        .sheet(item: $evidenceTarget) { indicator in
            if let report = indicator.report {
                EvidenceImagePreview(report: report, indicator: indicator)
            }
        }
        .sheet(item: $reminderPrefill) { prefill in
            // 复用自由提醒表单(每天/每周/每月/每年 + 时间点;一日多次就再建一条)。
            CustomReminderEditSheet(prefillTitle: prefill.title, prefillNote: prefill.note)
        }
        .sheet(item: $reportPhotoStart) { start in
            if let r = reportEntry {
                ReportImagesViewer(assets: r.assets, startIndex: start.index)
            }
        }
    }

    // MARK: - 删除(永久:SwiftData 硬删 + Vault 原图 unlink,见 CLAUDE.md §6)

    private var deleteButton: some View {
        Button(role: .destructive) { showDeleteConfirm = true } label: {
            Label(String(appLoc: "永久删除"), systemImage: "trash")
                .font(.tjScaled( 12, weight: .medium))
                .foregroundStyle(Tj.Palette.brick.opacity(0.8))
                .padding(.horizontal, 14)
                .padding(.vertical, 8)
                .background(
                    RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
                        .strokeBorder(Tj.Palette.brick.opacity(0.3), lineWidth: 1)
                )
                // 纯描边按钮:补 contentShape 让整框可点(否则中间透明区点不到)。
                .contentShape(Rectangle())
        }
        .buttonStyle(.plain)
        .padding(.top, 8)
    }

    private func performDelete() {
        switch detail {
        case .indicator(let i):
            deleteIndicator(i)
        case .bloodPressure(let sys, let dia):
            deleteIndicator(sys)
            if let dia { deleteIndicator(dia) }
        case .report(let r):
            // cascade 只删 Asset/Indicator 记录,Vault 里的 JPEG 要手动 unlink。
            var paths = Set(r.assets.map(\.relativePath))
            paths.formUnion(r.indicators.compactMap { $0.asset?.relativePath })
            for p in paths { try? FileVault.shared.remove(relativePath: p) }
            ctx.delete(r)
        case .diary(let d):
            // 拍药盒日记可能挂原图;cascade 删 Asset 记录,Vault 里的 JPEG 要手动 unlink。
            for p in Set(d.assets.map(\.relativePath)) {
                try? FileVault.shared.remove(relativePath: p)
            }
            ctx.delete(d)
        case .symptom(let s):
            ctx.delete(s)
        }
        try? ctx.save()
        dismiss()
    }

    /// 删一条指标:先 unlink 其原图文件 + Asset 记录(关系默认 nullify,不会自动级联),再删指标本身。
    private func deleteIndicator(_ i: Indicator) {
        if let asset = i.asset {
            try? FileVault.shared.remove(relativePath: asset.relativePath)
            ctx.delete(asset)
        }
        ctx.delete(i)
    }

    // MARK: - Header

    private var header: some View {
        HStack(spacing: 12) {
            Button { dismiss() } label: {
                Image(systemName: "xmark")
                    .font(.tjScaled( 16, weight: .semibold))
                    .foregroundStyle(Tj.Palette.text)
                    .frame(width: 32, height: 32)
                    .background(Circle().fill(Tj.Palette.sand2))
            }
            Text(titleText)
                .font(.tjH2())
                .foregroundStyle(Tj.Palette.text)
            Spacer()
            TjLockChip()
        }
        .padding(.horizontal, 20)
        .padding(.vertical, 14)
        .background(Tj.Palette.sand)
        .overlay(alignment: .bottom) {
            Rectangle().fill(Tj.Palette.lineSoft).frame(height: 1)
        }
    }

    private var titleText: String {
        switch detail {
        case .indicator:     return String(appLoc: "指标详情")
        case .bloodPressure: return String(appLoc: "血压详情")
        case .report:        return String(appLoc: "报告详情")
        case .diary(let d):  return d.isMedicationLog ? String(appLoc: "用药详情") : String(appLoc: "日记详情")
        case .symptom:       return String(appLoc: "症状详情")
        }
    }

    @ViewBuilder
    private var bodyContent: some View {
        switch detail {
        case .indicator(let i):              indicatorBody(i)
        case .bloodPressure(let s, let d):   bpBody(sys: s, dia: d)
        case .report(let r):                 reportBody(r)
        case .diary(let d):                  diaryBody(d)
        case .symptom(let s):                symptomBody(s)
        }
    }

    // MARK: - 指标

    private func indicatorBody(_ i: Indicator) -> some View {
        VStack(alignment: .leading, spacing: 16) {
            card {
                HStack(alignment: .firstTextBaseline) {
                    Text(i.name).font(.tjH2()).foregroundStyle(Tj.Palette.text)
                    Spacer()
                    statusChip(i.status)
                }
                HStack(alignment: .firstTextBaseline, spacing: 4) {
                    Text(i.value)
                        .font(.tjScaled( 30, weight: .bold, design: .rounded))
                        .foregroundStyle(i.status == .normal ? Tj.Palette.text : Tj.Palette.brick)
                    if !i.unit.isEmpty {
                        Text(i.unit).font(.tjScaled( 14)).foregroundStyle(Tj.Palette.text3)
                    }
                }
                divider
                if !i.range.isEmpty { field(String(appLoc: "参考范围"), i.range) }
                field(String(appLoc: "记录时间"), Self.dateTimeText(i.capturedAt))
                field(String(appLoc: "来源"), i.report?.title ?? i.source.label)
                if let report = i.report {
                    evidenceButton(for: i, assets: report.assets)
                }
                if let note = i.note, !note.isEmpty { field(String(appLoc: "备注"), note) }
            }
            RecordAnotherButton(name: i.name, prefill: .init(indicator: i))
        }
    }

    // MARK: - 血压(合并条目)

    private func bpBody(sys: Indicator, dia: Indicator?) -> some View {
        let combined: IndicatorStatus = sys.status != .normal
            ? sys.status
            : (dia?.status ?? .normal)
        return VStack(alignment: .leading, spacing: 16) {
            card {
                HStack(alignment: .firstTextBaseline) {
                    Text(String(appLoc: "血压")).font(.tjH2()).foregroundStyle(Tj.Palette.text)
                    Spacer()
                    statusChip(combined)
                }
                HStack(alignment: .firstTextBaseline, spacing: 4) {
                    Text("\(sys.value)/\(dia?.value ?? "—")")
                        .font(.tjScaled( 30, weight: .bold, design: .rounded))
                        .foregroundStyle(combined == .normal ? Tj.Palette.text : Tj.Palette.brick)
                    Text("mmHg").font(.tjScaled( 14)).foregroundStyle(Tj.Palette.text3)
                }
                divider
                if !sys.range.isEmpty { field(String(appLoc: "参考范围"), sys.range) }
                field(String(appLoc: "记录时间"), Self.dateTimeText(sys.capturedAt))
            }
            // 血压走双字段:seriesKey 用 bp.systolic 反查到 MonitorMetric.bloodPressure。
            RecordAnotherButton(name: String(appLoc: "血压"),
                                prefill: .init(seriesKey: sys.seriesKey ?? "bp.systolic",
                                               name: String(appLoc: "血压"),
                                               unit: "mmHg", range: sys.range))
        }
    }

    // MARK: - 报告

    private func reportBody(_ r: Report) -> some View {
        let sorted = r.indicators.sorted {
            ($0.status == .normal ? 1 : 0) < ($1.status == .normal ? 1 : 0)
        }
        return VStack(alignment: .leading, spacing: 16) {
            card {
                Text(r.title).font(.tjH2()).foregroundStyle(Tj.Palette.text)
                HStack(spacing: 8) {
                    TjBadge(text: r.type.label, style: .neutral)
                    Text(Self.dateText(r.reportDate))
                        .font(.tjScaled( 12)).foregroundStyle(Tj.Palette.text3)
                }
                if let inst = r.institution, !inst.isEmpty {
                    field(String(appLoc: "机构"), inst)
                }
            }

            if !r.assets.isEmpty {
                reportPhotosCard(r.assets)
            }

            ReportSummaryCard(report: r)

            if !r.indicators.isEmpty {
                card {
                    Text(String(appLoc: "指标"))
                        .font(.tjScaled( 12, weight: .semibold)).foregroundStyle(Tj.Palette.text2)
                    ForEach(sorted) { ind in
                        VStack(alignment: .leading, spacing: 6) {
                            HStack {
                                Text(ind.name).font(.tjScaled( 14)).foregroundStyle(Tj.Palette.text)
                                Spacer(minLength: 8)
                                Text(ind.unit.isEmpty ? ind.value : "\(ind.value) \(ind.unit)")
                                    .font(.tjScaled( 13, design: .monospaced))
                                    .foregroundStyle(ind.status == .normal ? Tj.Palette.text2 : Tj.Palette.brick)
                                statusChip(ind.status)
                            }
                            evidenceButton(for: ind, assets: r.assets)
                        }
                    }
                }
            }

            if let note = r.note, !note.isEmpty {
                card { field(String(appLoc: "备注"), note) }
            }
        }
    }

    /// 报告原图卡:可点缩略图 → 全屏翻页查看。归档只存图时,这是看原图的唯一入口,必须独立于指标存在。
    private func reportPhotosCard(_ assets: [Asset]) -> some View {
        card {
            HStack {
                Text(String(appLoc: "原图\(assets.count)张"))
                    .font(.tjScaled( 12, weight: .semibold)).foregroundStyle(Tj.Palette.text2)
                Spacer()
                Text(String(appLoc: "点图放大")).font(.tjScaled( 11)).foregroundStyle(Tj.Palette.text3)
            }
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(spacing: 10) {
                    ForEach(Array(assets.enumerated()), id: \.offset) { idx, asset in
                        Button {
                            reportPhotoStart = ReportPhotoPage(index: idx)
                        } label: {
                            reportThumb(asset)
                        }
                        .buttonStyle(.plain)
                    }
                }
            }
        }
    }

    private func reportThumb(_ asset: Asset) -> some View {
        VaultImage(relativePath: asset.relativePath, maxPixel: 400) { img in
            Image(uiImage: img).resizable().scaledToFill()
        } placeholder: { isLoading in
            if isLoading {
                Tj.Palette.paper
            } else {
                TjPlaceholder(label: String(appLoc: "原图无法读取"))
            }
        }
        .frame(width: 96, height: 120)
        .clipped()
        .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
        .overlay(
            RoundedRectangle(cornerRadius: 8, style: .continuous)
                .strokeBorder(Tj.Palette.line, lineWidth: 1)
        )
    }

    // MARK: - 日记

    @ViewBuilder
    private func diaryBody(_ d: DiaryEntry) -> some View {
        if d.isMedicationLog {
            medicationBody(d)
        } else {
            VStack(alignment: .leading, spacing: 16) {
                card {
                    Text(Self.dateTimeText(d.createdAt))
                        .font(.tjScaled( 12)).foregroundStyle(Tj.Palette.text3)
                    Text(d.content)
                        .font(.tjScaled( 15))
                        .foregroundStyle(Tj.Palette.text)
                        .textSelection(.enabled)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .fixedSize(horizontal: false, vertical: true)
                    if !d.tags.isEmpty {
                        field(String(appLoc: "标签"), d.tags.map { "#\($0)" }.joined(separator: "  "))
                    }
                }
            }
        }
    }

    // MARK: - 用药使用记录(展示药名/剂量/时间 + 设置提醒)

    /// 用药使用记录(带「用药」tag 的日记):展示「药名 [规格] · 剂量」+ 时间,下方「设置提醒」。
    /// 只到点提示,不做剂量/频次建议(CLAUDE.md §1、§10)。
    private func medicationBody(_ d: DiaryEntry) -> some View {
        let lines = Self.medicationLines(d.content)
        return VStack(alignment: .leading, spacing: 16) {
            card {
                Text(Self.dateTimeText(d.createdAt))
                    .font(.tjScaled( 12)).foregroundStyle(Tj.Palette.text3)
                if lines.isEmpty {
                    Text(d.content)
                        .font(.tjScaled( 15)).foregroundStyle(Tj.Palette.text)
                        .textSelection(.enabled)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .fixedSize(horizontal: false, vertical: true)
                } else {
                    ForEach(Array(lines.enumerated()), id: \.offset) { idx, line in
                        if idx > 0 { divider }
                        Text(line)
                            .font(.tjScaled( 15)).foregroundStyle(Tj.Palette.text)
                            .textSelection(.enabled)
                            .frame(maxWidth: .infinity, alignment: .leading)
                            .fixedSize(horizontal: false, vertical: true)
                    }
                }
            }

            medicationActionRow(d)

            Text("「设置提醒」只到点提示,不提供任何用药或剂量建议。")
                .font(.tjScaled( 11)).foregroundStyle(Tj.Palette.text3)
                .frame(maxWidth: .infinity, alignment: .leading)
                .fixedSize(horizontal: false, vertical: true)
        }
    }

    /// 单动作:设置提醒(复用自由提醒表单,预填药名 + 用法)。只到点提示,不做剂量建议。
    private func medicationActionRow(_ d: DiaryEntry) -> some View {
        HStack(spacing: 10) {
            medAction(title: String(appLoc: "设置提醒"), icon: "bell.badge") {
                let lines = Self.medicationLines(d.content)
                if lines.count <= 1 {
                    let f = Self.medicationReminderFields(forLine: lines.first ?? d.content)
                    reminderPrefill = ReminderPrefill(title: f.title, note: f.note)
                } else {
                    // 多种药:一个提醒涵盖,药名清单进备注,用户据此自定时间/频率。
                    reminderPrefill = ReminderPrefill(title: String(appLoc: "服药提醒"),
                                                      note: lines.joined(separator: "\n"))
                }
            }
        }
    }

    private func medAction(title: String, icon: String, action: @escaping () -> Void) -> some View {
        Button(action: action) {
            VStack(spacing: 6) {
                Image(systemName: icon).font(.tjScaled( 18, weight: .medium))
                Text(title).font(.tjScaled( 12, weight: .semibold))
            }
            .foregroundStyle(Tj.Palette.ink)
            .frame(maxWidth: .infinity)
            .padding(.vertical, 12)
            .background(
                RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
                    .fill(Tj.Palette.amber.opacity(0.14))
            )
            .contentShape(RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous))
        }
        .buttonStyle(.plain)
    }

    // MARK: - 症状

    private func symptomBody(_ s: Symptom) -> some View {
        card {
            HStack(alignment: .firstTextBaseline) {
                Text(s.name).font(.tjH2()).foregroundStyle(Tj.Palette.text)
                Spacer()
                if s.isOngoing {
                    Text(String(appLoc: "进行中"))
                        .font(.tjScaled( 12, weight: .semibold))
                        .foregroundStyle(Tj.Palette.brick)
                        .padding(.horizontal, 8).padding(.vertical, 4)
                        .background(Capsule().fill(Tj.Palette.brick.opacity(0.14)))
                }
            }
            divider
            field(String(appLoc: "程度"), "\(s.severity) / 5")
            field(String(appLoc: "开始"), Self.dateTimeText(s.startedAt))
            field(String(appLoc: "结束"), s.endedAt.map(Self.dateTimeText) ?? String(appLoc: "进行中"))
            field(String(appLoc: "持续"), formatDuration(s.duration))
            if let note = s.note, !note.isEmpty { field(String(appLoc: "备注"), note) }
            if !s.tags.isEmpty {
                field(String(appLoc: "标签"), s.tags.map { "#\($0)" }.joined(separator: "  "))
            }
        }
    }

    // MARK: - 复用件

    @ViewBuilder
    private func card<Content: View>(@ViewBuilder content: () -> Content) -> some View {
        VStack(alignment: .leading, spacing: 10) { content() }
            .padding(14)
            .frame(maxWidth: .infinity, alignment: .leading)
            .background(
                RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
                    .fill(Tj.Palette.paper)
            )
            .overlay(
                RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
                    .strokeBorder(Tj.Palette.lineSoft, lineWidth: 1)
            )
    }

    private func field(_ label: String, _ value: String) -> some View {
        HStack(alignment: .top, spacing: 12) {
            Text(label).font(.tjScaled( 13)).foregroundStyle(Tj.Palette.text3)
            Spacer(minLength: 12)
            Text(value)
                .font(.tjScaled( 14, weight: .medium))
                .foregroundStyle(Tj.Palette.text)
                .multilineTextAlignment(.trailing)
                .fixedSize(horizontal: false, vertical: true)
        }
    }

    @ViewBuilder
    private func evidenceButton(for indicator: Indicator, assets: [Asset]) -> some View {
        if indicator.hasEvidenceBox,
           let page = indicator.sourcePageIndex,
           assets.indices.contains(page) {
            Button {
                evidenceTarget = indicator
            } label: {
                Label(String(appLoc: "查看原图位置"), systemImage: "viewfinder")
                    .font(.tjScaled( 12, weight: .semibold))
                    .foregroundStyle(Tj.Palette.ink)
                    .padding(.horizontal, 10)
                    .padding(.vertical, 6)
                    .background(Capsule().fill(Tj.Palette.leaf.opacity(0.14)))
                    .contentShape(Rectangle())
            }
            .buttonStyle(.plain)
        }
    }

    private var divider: some View {
        Rectangle().fill(Tj.Palette.lineSoft).frame(height: 1)
    }

    private func statusChip(_ s: IndicatorStatus) -> some View {
        let text: String
        let color: Color
        let arrow: String
        switch s {
        case .high:   text = String(appLoc: "偏高"); color = Tj.Palette.brick; arrow = "↑"
        case .low:    text = String(appLoc: "偏低"); color = Tj.Palette.brick; arrow = "↓"
        case .normal: text = String(appLoc: "正常"); color = Tj.Palette.leaf;  arrow = ""
        }
        return HStack(spacing: 3) {
            if !arrow.isEmpty { Text(arrow).font(.tjScaled( 11, weight: .bold)) }
            Text(text).font(.tjScaled( 12, weight: .semibold))
        }
        .foregroundStyle(color)
        .padding(.horizontal, 8)
        .padding(.vertical, 4)
        .background(Capsule().fill(color.opacity(0.14)))
    }

    private nonisolated static func dateTimeText(_ d: Date) -> String {
        d.formatted(.dateTime.year().month().day().hour().minute())
    }

    private nonisolated static func dateText(_ d: Date) -> String {
        d.formatted(.dateTime.year().month().day())
    }

    // MARK: - 用药行解析(纯函数,便于单测)

    /// 把用药日记 content 按换行拆成单行药品,去掉空白行与首尾空格。
    nonisolated static func medicationLines(_ content: String) -> [String] {
        content.split(whereSeparator: \.isNewline)
            .map { $0.trimmingCharacters(in: .whitespaces) }
            .filter { !$0.isEmpty }
    }

    /// 从一行药品文本(如「缬沙坦胶囊 80mg · 一日一次」)派生吃药提醒预填:
    /// 标题 =「吃药:<药名+规格>」,备注 = 用法(" · " 之后部分,供用户据此选时间/频率)。
    nonisolated static func medicationReminderFields(forLine line: String) -> (title: String, note: String) {
        let parts = line.components(separatedBy: " · ")
        let head = (parts.first ?? line).trimmingCharacters(in: .whitespaces)
        let usage = parts.count > 1
            ? parts.dropFirst().joined(separator: " · ").trimmingCharacters(in: .whitespaces)
            : ""
        let name = head.isEmpty ? line.trimmingCharacters(in: .whitespaces) : head
        return (title: String(appLoc: "吃药:") + name, note: usage)
    }
}

/// 报告原图浏览(纯翻页看图,无指标高亮)。归档只存图的报告也能随时调取查看。
private struct ReportImagesViewer: View {
    @Environment(\.dismiss) private var dismiss
    let assets: [Asset]
    @State private var selection: Int

    init(assets: [Asset], startIndex: Int) {
        self.assets = assets
        _selection = State(initialValue: min(max(startIndex, 0), max(assets.count - 1, 0)))
    }

    var body: some View {
        VStack(spacing: 0) {
            HStack(spacing: 12) {
                Button { dismiss() } label: {
                    Image(systemName: "xmark")
                        .font(.tjScaled( 16, weight: .semibold))
                        .foregroundStyle(Tj.Palette.text)
                        .frame(width: 32, height: 32)
                        .background(Circle().fill(Tj.Palette.sand2))
                }
                Text("原图 · 第 \(selection + 1)/\(assets.count) 页")
                    .font(.tjScaled( 14, weight: .semibold))
                    .foregroundStyle(Tj.Palette.text)
                Spacer()
            }
            .padding(.horizontal, 20)
            .padding(.vertical, 14)
            .background(Tj.Palette.sand)
            .overlay(alignment: .bottom) {
                Rectangle().fill(Tj.Palette.lineSoft).frame(height: 1)
            }

            TabView(selection: $selection) {
                ForEach(Array(assets.enumerated()), id: \.offset) { index, asset in
                    EvidenceImagePage(asset: asset, highlight: nil)
                        .tag(index)
                        .padding(16)
                }
            }
            .tabViewStyle(.page(indexDisplayMode: assets.count > 1 ? .automatic : .never))
        }
        .background(Tj.Palette.sand.ignoresSafeArea())
        .presentationDetents([.large])
        .presentationDragIndicator(.visible)
        .presentationBackground(Tj.Palette.sand)
    }
}

/// 原图证据预览(翻页 + 高亮框)。指标详情与同类聚合详情共用,故为模块内可见。
struct EvidenceImagePreview: View {
    @Environment(\.dismiss) private var dismiss
    let report: Report
    let indicator: Indicator

    @State private var selection: Int

    init(report: Report, indicator: Indicator) {
        self.report = report
        self.indicator = indicator
        let page = indicator.sourcePageIndex ?? 0
        _selection = State(initialValue: min(max(page, 0), max(report.assets.count - 1, 0)))
    }

    var body: some View {
        VStack(spacing: 0) {
            HStack(spacing: 12) {
                Button { dismiss() } label: {
                    Image(systemName: "xmark")
                        .font(.tjScaled( 16, weight: .semibold))
                        .foregroundStyle(Tj.Palette.text)
                        .frame(width: 32, height: 32)
                        .background(Circle().fill(Tj.Palette.sand2))
                }
                VStack(alignment: .leading, spacing: 2) {
                    Text(indicator.name)
                        .font(.tjScaled( 16, weight: .semibold))
                        .foregroundStyle(Tj.Palette.text)
                    Text("第 \(selection + 1) 页 · 原图证据")
                        .font(.tjScaled( 12))
                        .foregroundStyle(Tj.Palette.text3)
                }
                Spacer()
            }
            .padding(.horizontal, 20)
            .padding(.vertical, 14)
            .background(Tj.Palette.sand)
            .overlay(alignment: .bottom) {
                Rectangle().fill(Tj.Palette.lineSoft).frame(height: 1)
            }

            TabView(selection: $selection) {
                ForEach(Array(report.assets.enumerated()), id: \.offset) { index, asset in
                    EvidenceImagePage(
                        asset: asset,
                        highlight: index == indicator.sourcePageIndex ? indicator.evidenceRect : nil
                    )
                    .tag(index)
                    .padding(16)
                }
            }
            .tabViewStyle(.page(indexDisplayMode: report.assets.count > 1 ? .automatic : .never))
        }
        .background(Tj.Palette.sand.ignoresSafeArea())
        .presentationDetents([.large])
        .presentationDragIndicator(.visible)
        .presentationBackground(Tj.Palette.sand)
    }
}

private struct EvidenceImagePage: View {
    let asset: Asset
    let highlight: CGRect?

    var body: some View {
        GeometryReader { geo in
            VaultImage(relativePath: asset.relativePath, maxPixel: 2000) { image in
                ZStack {
                    Image(uiImage: image)
                        .resizable()
                        .scaledToFit()
                        .frame(width: geo.size.width, height: geo.size.height)
                    if let highlight {
                        // 降采样保持原始宽高比,imageSize 仅用于算 letterbox 比例,定位不受影响。
                        EvidenceHighlightOverlay(imageSize: image.size, normalizedRect: highlight)
                    }
                }
                .frame(width: geo.size.width, height: geo.size.height)
                .background(Tj.Palette.paper)
                .clipShape(RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous))
                .overlay(
                    RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
                        .strokeBorder(Tj.Palette.lineSoft, lineWidth: 1)
                )
            } placeholder: { isLoading in
                if isLoading {
                    ProgressView()
                        .frame(width: geo.size.width, height: geo.size.height)
                } else {
                    TjPlaceholder(label: String(appLoc: "原图无法读取"))
                        .frame(width: geo.size.width, height: geo.size.height)
                }
            }
        }
    }
}

private struct EvidenceHighlightOverlay: View {
    let imageSize: CGSize
    let normalizedRect: CGRect

    var body: some View {
        GeometryReader { geo in
            let fitted = fittedRect(imageSize: imageSize, containerSize: geo.size)
            let rect = CGRect(
                x: fitted.minX + normalizedRect.minX * fitted.width,
                y: fitted.minY + normalizedRect.minY * fitted.height,
                width: normalizedRect.width * fitted.width,
                height: normalizedRect.height * fitted.height
            )
            RoundedRectangle(cornerRadius: 4, style: .continuous)
                .fill(Tj.Palette.brick.opacity(0.16))
                .overlay(
                    RoundedRectangle(cornerRadius: 4, style: .continuous)
                        .stroke(Tj.Palette.brick, lineWidth: 2)
                )
                .frame(width: rect.width, height: rect.height)
                .position(x: rect.midX, y: rect.midY)
                .shadow(color: Tj.Palette.brick.opacity(0.24), radius: 8, y: 2)
        }
        .allowsHitTesting(false)
    }

    private func fittedRect(imageSize: CGSize, containerSize: CGSize) -> CGRect {
        guard imageSize.width > 0,
              imageSize.height > 0,
              containerSize.width > 0,
              containerSize.height > 0 else {
            return .zero
        }
        let scale = min(containerSize.width / imageSize.width, containerSize.height / imageSize.height)
        let size = CGSize(width: imageSize.width * scale, height: imageSize.height * scale)
        return CGRect(
            x: (containerSize.width - size.width) / 2,
            y: (containerSize.height - size.height) / 2,
            width: size.width,
            height: size.height
        )
    }
}

// MARK: - 报告摘要卡(无摘要时后台预生成兜底)

/// 有摘要直接显示;无摘要且有指标时触发后台预生成(归档时若被抢占,这里兜底),
/// 生成期间显示流光线,完成后 SwiftData 观察自动刷新出文本。
private struct ReportSummaryCard: View {
    @Environment(\.modelContext) private var ctx
    let report: Report
    @State private var generating = false

    var body: some View {
        Group {
            if let sum = report.summary, !sum.isEmpty {
                container {
                    Text(String(appLoc: "摘要"))
                        .font(.tjScaled( 12, weight: .semibold)).foregroundStyle(Tj.Palette.text2)
                    Text(sum).font(.tjScaled( 14)).foregroundStyle(Tj.Palette.text)
                        .fixedSize(horizontal: false, vertical: true)
                }
            } else if generating {
                container {
                    Text("本地 AI 正在解读这份报告…")
                        .font(.tjScaled( 12)).foregroundStyle(Tj.Palette.text3)
                    AIFlowBar()
                }
            }
        }
        .task {
            guard (report.summary ?? "").isEmpty, !report.indicators.isEmpty else { return }
            generating = true
            await ReportInsightService.shared.pregenerateIfNeeded(report: report, in: ctx)
            generating = false
        }
    }

    private func container<C: View>(@ViewBuilder _ body: () -> C) -> some View {
        VStack(alignment: .leading, spacing: 10) { body() }
            .padding(14)
            .frame(maxWidth: .infinity, alignment: .leading)
            .background(
                RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
                    .fill(Tj.Palette.paper)
            )
            .overlay(
                RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous)
                    .strokeBorder(Tj.Palette.lineSoft, lineWidth: 1)
            )
    }
}
