import SwiftUI
import Charts

struct SeriesChartCard: View {
    let bucket: SeriesBucket

    // bucket 不可变,这些派生量在 init 里一次性算好存为 let,避免 body 内被
    // header / chart / daysSpanLabel 多次访问时反复 flatMap / min / max 重算。
    private let allPoints: [(line: SeriesBucket.SeriesLine, point: SeriesBucket.Point)]
    private let dateDomain: ClosedRange<Date>?
    private let valueDomain: ClosedRange<Double>?

    init(bucket: SeriesBucket) {
        self.bucket = bucket
        let pts = bucket.lines.flatMap { line in line.points.map { (line, $0) } }
        self.allPoints = pts
        self.dateDomain = Self.makeDateDomain(pts)
        self.valueDomain = Self.makeValueDomain(pts, lines: bucket.lines)
    }

    private static func makeDateDomain(
        _ allPoints: [(line: SeriesBucket.SeriesLine, point: SeriesBucket.Point)]
    ) -> ClosedRange<Date>? {
        let dates = allPoints.map(\.point.date)
        guard let lo = dates.min(), let hi = dates.max() else { return nil }
        if lo == hi {
            // 只有一个点的极端情况:扩 1 天显示
            let cal = Calendar.current
            let earlier = cal.date(byAdding: .hour, value: -12, to: lo) ?? lo
            let later = cal.date(byAdding: .hour, value: 12, to: hi) ?? hi
            return earlier...later
        }
        return lo...hi
    }

    private static func makeValueDomain(
        _ allPoints: [(line: SeriesBucket.SeriesLine, point: SeriesBucket.Point)],
        lines: [SeriesBucket.SeriesLine]
    ) -> ClosedRange<Double>? {
        var lo = Double.greatestFiniteMagnitude
        var hi = -Double.greatestFiniteMagnitude
        for (_, p) in allPoints {
            lo = min(lo, p.value)
            hi = max(hi, p.value)
        }
        for line in lines {
            if let r = line.referenceRange {
                lo = min(lo, r.lowerBound)
                hi = max(hi, r.upperBound)
            }
        }
        // 无数据时 lo>hi → nil;所有点同值(lo==hi)时按值本身对称留白,
        // 否则会落到 0...1 把数据点挤出可视域。
        guard lo <= hi else { return nil }
        let span = hi - lo
        let pad = span > 0 ? max(1, span * 0.12) : max(1, abs(lo) * 0.1)
        return (lo - pad)...(hi + pad)
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            header
            chart
                .frame(height: 120)
            if bucket.lines.count > 1 {
                legendLine
            }
        }
        .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 var header: some View {
        HStack(alignment: .lastTextBaseline, spacing: 10) {
            Text(bucket.title)
                .font(.tjScaled( 15, weight: .semibold))
                .foregroundStyle(Tj.Palette.text)
            Text("\(allPoints.count) 条 · 近 \(daysSpanLabel)")
                .font(.tjScaled( 11))
                .foregroundStyle(Tj.Palette.text3)
            Spacer()
            latestValueBadge
        }
    }

    private var latestValueBadge: some View {
        let parts = bucket.lines.compactMap { line -> String? in
            guard let p = line.latestPoint else { return nil }
            return formatValue(p.value)
        }
        let joined = parts.joined(separator: " / ")
        let anyAbnormal = bucket.lines.contains { line in
            (line.latestPoint?.status ?? .normal) != .normal
        }
        return HStack(spacing: 4) {
            Text(joined)
                .font(.tjScaled( 14, weight: .semibold, design: .monospaced))
                .foregroundStyle(anyAbnormal ? Tj.Palette.brick : Tj.Palette.text)
            Text(bucket.unit)
                .font(.tjScaled( 10, design: .monospaced))
                .foregroundStyle(Tj.Palette.text3)
        }
    }

    private var chart: some View {
        Chart {
            // 参考范围带
            ForEach(bucket.lines) { line in
                if let r = line.referenceRange,
                   let dom = dateDomain {
                    RectangleMark(
                        xStart: .value("start", dom.lowerBound),
                        xEnd: .value("end", dom.upperBound),
                        yStart: .value("lo", r.lowerBound),
                        yEnd: .value("hi", r.upperBound)
                    )
                    .foregroundStyle(line.color.opacity(0.08))
                }
            }

            // 单条线时,线下垫一层渐变面积,增加体量、柔化观感
            //(多条线如血压不加,避免两片面积互相盖住)。
            if bucket.lines.count == 1, let line = bucket.lines.first {
                ForEach(line.points) { p in
                    // 显式把基线钉在值域下界:单值 AreaMark 的隐式基线(0/域外)会把渐变
                    // 拉到可视区外、几乎不淡出,看着像一块实色底色一路铺到图表底。
                    AreaMark(
                        x: .value("时间", p.date),
                        yStart: .value("基线", (valueDomain ?? 0...1).lowerBound),
                        yEnd: .value(line.label ?? bucket.title, p.value)
                    )
                    .foregroundStyle(LinearGradient(
                        colors: [line.color.opacity(0.16), line.color.opacity(0)],
                        startPoint: .top, endPoint: .bottom))
                    .interpolationMethod(.monotone)
                }
            }

            // 折线 + 点
            ForEach(bucket.lines) { line in
                ForEach(line.points) { p in
                    LineMark(
                        x: .value("时间", p.date),
                        y: .value(line.label ?? bucket.title, p.value)
                    )
                    .foregroundStyle(line.color)
                    // monotone:平滑但不在数据尖峰处过冲鼓包,比 catmullRom 更贴合真实读数。
                    .interpolationMethod(.monotone)
                    // 圆角端点 + 连接,去掉折线的生硬尖角。
                    .lineStyle(StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
                }
                .symbol {
                    Circle()
                        .fill(line.color)
                        .frame(width: 6, height: 6)
                }
            }
        }
        .chartXAxis {
            AxisMarks(values: .automatic(desiredCount: 4)) { _ in
                AxisGridLine().foregroundStyle(Tj.Palette.lineSoft)
                AxisValueLabel(format: .dateTime.month(.abbreviated).day(),
                               centered: false)
                    .foregroundStyle(Tj.Palette.text3)
            }
        }
        .chartYAxis {
            AxisMarks(position: .leading, values: .automatic(desiredCount: 3)) { _ in
                AxisGridLine().foregroundStyle(Tj.Palette.lineSoft)
                AxisValueLabel()
                    .foregroundStyle(Tj.Palette.text3)
                    .font(.tjScaled( 10, design: .monospaced))
            }
        }
        .chartYScale(domain: valueDomain ?? 0...1)
    }

    private var legendLine: some View {
        HStack(spacing: 12) {
            ForEach(bucket.lines) { line in
                HStack(spacing: 4) {
                    Circle()
                        .fill(line.color)
                        .frame(width: 8, height: 8)
                    Text(line.label ?? line.seriesKey)
                        .font(.tjScaled( 11))
                        .foregroundStyle(Tj.Palette.text2)
                }
            }
        }
    }

    private var daysSpanLabel: String {
        guard let dom = dateDomain else { return "—" }
        let days = Calendar.current.dateComponents([.day],
                                                    from: dom.lowerBound,
                                                    to: dom.upperBound).day ?? 0
        if days <= 0 { return String(appLoc: "今天") }
        if days < 30 { return String(appLoc: "\(days) 天") }
        if days < 365 { return String(appLoc: "\(days / 30) 个月") }
        return String(appLoc: "\(days / 365) 年")
    }

    private func formatValue(_ v: Double) -> String {
        v.truncatingRemainder(dividingBy: 1) == 0
            ? String(format: "%.0f", v)
            : String(format: "%.1f", v)
    }
}
