import SwiftUI
import Charts

struct EISView: View {
    @Bindable var state: AppState

    var body: some View {
        VStack(spacing: 0) {
            controlsRow
            Divider()
            GeometryReader { geo in
                if geo.size.width > 700 {
                    wideLayout(geo: geo)
                } else {
                    compactLayout
                }
            }
        }
    }

    // MARK: - Controls

    private var controlsRow: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 10) {
                LabeledField("Start Hz", text: $state.freqStart, width: 90)
                LabeledField("Stop Hz", text: $state.freqStop, width: 90)
                LabeledField("PPD", text: $state.ppd, width: 50)

                LabeledPicker("RTIA", selection: $state.rtia, items: Rtia.allCases) { $0.label }
                    .frame(width: 120)
                LabeledPicker("RCAL", selection: $state.rcal, items: Rcal.allCases) { $0.label }
                    .frame(width: 170)
                LabeledPicker("Electrodes", selection: $state.electrode, items: Electrode.allCases) { $0.label }
                    .frame(width: 180)

                Button("Apply") { state.applyEISSettings() }
                    .buttonStyle(ActionButtonStyle(color: .blue))
                Button("Sweep") { state.startSweep() }
                    .buttonStyle(ActionButtonStyle(color: .green))
            }
            .padding(.horizontal)
            .padding(.vertical, 8)
        }
    }

    // MARK: - Wide layout (iPad)

    private func wideLayout(geo: GeometryProxy) -> some View {
        HSplitLayout(ratio: 0.55) {
            VStack(spacing: 0) {
                HStack(spacing: 8) {
                    bodePlot
                    nyquistPlot
                }
                .padding(4)
            }
        } trailing: {
            eisTable
        }
    }

    // MARK: - Compact layout (iPhone)

    private var compactLayout: some View {
        ScrollView {
            VStack(spacing: 12) {
                bodePlot.frame(height: 300)
                nyquistPlot.frame(height: 300)
                eisTable.frame(height: 300)
            }
            .padding()
        }
    }

    // MARK: - Bode plot

    private var bodePlot: some View {
        VStack(spacing: 0) {
            if state.eisPoints.isEmpty {
                noDataPlaceholder
            } else {
                PlotContainer(title: "") {
                    VStack(spacing: 0) {
                        magnitudePlot
                            .frame(maxHeight: .infinity)
                        Divider()
                        phasePlot
                            .frame(maxHeight: .infinity)
                    }
                }
            }
        }
        .background(Color.black.opacity(0.3))
        .clipShape(RoundedRectangle(cornerRadius: 8))
    }

    private var magnitudePlot: some View {
        Chart {
            if let ref = state.eisRef {
                ForEach(Array(ref.enumerated()), id: \.offset) { _, pt in
                    LineMark(
                        x: .value("Freq", log10(max(Double(pt.freqHz), 1))),
                        y: .value("|Z|", Double(pt.magOhms))
                    )
                    .foregroundStyle(Color.gray.opacity(0.5))
                    .lineStyle(StrokeStyle(lineWidth: 1.5))
                }
            }
            ForEach(Array(state.eisPoints.enumerated()), id: \.offset) { _, pt in
                LineMark(
                    x: .value("Freq", log10(max(Double(pt.freqHz), 1))),
                    y: .value("|Z|", Double(pt.magOhms))
                )
                .foregroundStyle(Color.cyan)
                .lineStyle(StrokeStyle(lineWidth: 2))
            }
            ForEach(Array(state.eisPoints.enumerated()), id: \.offset) { _, pt in
                PointMark(
                    x: .value("Freq", log10(max(Double(pt.freqHz), 1))),
                    y: .value("|Z|", Double(pt.magOhms))
                )
                .foregroundStyle(Color.cyan)
                .symbolSize(16)
            }
        }
        .chartXAxisLabel("|Z| (Ohm)", alignment: .leading)
        .chartXAxis {
            AxisMarks(values: .automatic) { value in
                AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5))
                    .foregroundStyle(Color.gray.opacity(0.3))
                AxisValueLabel {
                    if let v = value.as(Double.self) {
                        Text(freqLabel(v))
                            .font(.caption2)
                            .foregroundStyle(.secondary)
                    }
                }
            }
        }
        .chartYAxisLabel("|Z|", position: .leading)
        .chartYAxis {
            AxisMarks(position: .leading) { _ in
                AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5))
                    .foregroundStyle(Color.gray.opacity(0.3))
                AxisValueLabel()
                    .font(.caption2)
                    .foregroundStyle(Color.cyan)
            }
        }
        .padding(8)
    }

    private var phasePlot: some View {
        Chart {
            if let ref = state.eisRef {
                ForEach(Array(ref.enumerated()), id: \.offset) { _, pt in
                    LineMark(
                        x: .value("Freq", log10(max(Double(pt.freqHz), 1))),
                        y: .value("Phase", Double(pt.phaseDeg))
                    )
                    .foregroundStyle(Color.gray.opacity(0.5))
                    .lineStyle(StrokeStyle(lineWidth: 1.5))
                }
            }
            ForEach(Array(state.eisPoints.enumerated()), id: \.offset) { _, pt in
                LineMark(
                    x: .value("Freq", log10(max(Double(pt.freqHz), 1))),
                    y: .value("Phase", Double(pt.phaseDeg))
                )
                .foregroundStyle(Color.orange)
                .lineStyle(StrokeStyle(lineWidth: 2))
            }
            ForEach(Array(state.eisPoints.enumerated()), id: \.offset) { _, pt in
                PointMark(
                    x: .value("Freq", log10(max(Double(pt.freqHz), 1))),
                    y: .value("Phase", Double(pt.phaseDeg))
                )
                .foregroundStyle(Color.orange)
                .symbolSize(16)
            }
        }
        .chartXAxisLabel("Phase (deg)", alignment: .leading)
        .chartXAxis {
            AxisMarks(values: .automatic) { value in
                AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5))
                    .foregroundStyle(Color.gray.opacity(0.3))
                AxisValueLabel {
                    if let v = value.as(Double.self) {
                        Text(freqLabel(v))
                            .font(.caption2)
                            .foregroundStyle(.secondary)
                    }
                }
            }
        }
        .chartYAxisLabel("Phase", position: .leading)
        .chartYAxis {
            AxisMarks(position: .leading) { _ in
                AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5))
                    .foregroundStyle(Color.gray.opacity(0.3))
                AxisValueLabel()
                    .font(.caption2)
                    .foregroundStyle(Color.orange)
            }
        }
        .padding(8)
    }

    // MARK: - Nyquist plot

    private var nyquistPlot: some View {
        VStack(spacing: 0) {
            if state.eisPoints.isEmpty {
                noDataPlaceholder
            } else {
                PlotContainer(title: "") {
                    nyquistCanvas
                }
            }
        }
        .background(Color.black.opacity(0.3))
        .clipShape(RoundedRectangle(cornerRadius: 8))
    }

    private var nyquistCanvas: some View {
        Canvas { context, size in
            let ml: CGFloat = 50, mr: CGFloat = 12, mt: CGFloat = 12, mb: CGFloat = 22
            let xl = ml, xr = size.width - mr
            let yt = mt, yb = size.height - mb

            let points = state.eisPoints.filter { $0.zReal.isFinite && $0.zImag.isFinite }
            guard !points.isEmpty else { return }

            let reVals = points.map { CGFloat($0.zReal) }
            let niVals = points.map { CGFloat(-$0.zImag) }
            let (reMin, reMax) = (reVals.min()!, reVals.max()!)
            let (niMin, niMax) = (niVals.min()!, niVals.max()!)
            let reSpan = max(reMax - reMin, 1)
            let niSpan = max(niMax - niMin, 1)
            let span = max(reSpan, niSpan) * 1.3
            let reC = (reMin + reMax) / 2
            let niC = (niMin + niMax) / 2
            let xvLo = reC - span / 2, xvHi = reC + span / 2
            let yvLo = niC - span / 2, yvHi = niC + span / 2

            func lx(_ v: CGFloat) -> CGFloat { xl + (v - xvLo) / (xvHi - xvLo) * (xr - xl) }
            func ly(_ v: CGFloat) -> CGFloat { yt + (yvHi - v) / (yvHi - yvLo) * (yb - yt) }

            // grid
            let gridColor = Color(white: 0.25).opacity(0.6)
            drawGrid(context: context, xl: xl, xr: xr, yt: yt, yb: yb,
                     xvLo: xvLo, xvHi: xvHi, yvLo: yvLo, yvHi: yvHi,
                     gridColor: gridColor, size: size)

            // zero line
            let zy = ly(0)
            if zy > yt && zy < yb {
                var zeroPath = Path()
                zeroPath.move(to: CGPoint(x: xl, y: zy))
                zeroPath.addLine(to: CGPoint(x: xr, y: zy))
                context.stroke(zeroPath, with: .color(.gray.opacity(0.6)), lineWidth: 1)
            }

            // axis labels
            context.draw(
                Text("-Z''").font(.caption2).foregroundStyle(Color(red: 0.4, green: 1, blue: 0.4)),
                at: CGPoint(x: 20, y: yt - 2))
            context.draw(
                Text("Z'").font(.caption2).foregroundStyle(Color(red: 0.4, green: 1, blue: 0.4)),
                at: CGPoint(x: (xl + xr) / 2, y: yb + 12))

            // reference
            if let ref = state.eisRef {
                let refPts = ref.map { CGPoint(x: lx(CGFloat($0.zReal)), y: ly(CGFloat(-$0.zImag))) }
                drawPolyline(context: context, points: refPts, color: Color.gray.opacity(0.5), width: 1.5)
            }

            // data
            let dataPts = points.map { CGPoint(x: lx(CGFloat($0.zReal)), y: ly(CGFloat(-$0.zImag))) }
            let nyqColor = Color(red: 0.4, green: 1, blue: 0.4)
            drawPolyline(context: context, points: dataPts, color: nyqColor, width: 2)
            for pt in dataPts {
                context.fill(Path(ellipseIn: CGRect(x: pt.x - 3, y: pt.y - 3, width: 6, height: 6)),
                             with: .color(nyqColor))
            }

            // circle fit
            if let fit = kasaCircleFit(points: points.map { (Double($0.zReal), Double(-$0.zImag)) }) {
                let disc = fit.r * fit.r - fit.cy * fit.cy
                if disc > 0 {
                    let sd = sqrt(disc)
                    let rs = fit.cx - sd
                    let rp = 2 * sd

                    if rp > 0 {
                        let thetaR = atan2(-fit.cy, sd)
                        var thetaL = atan2(-fit.cy, -sd)
                        if thetaL < thetaR { thetaL += 2 * .pi }

                        let nArc = 120
                        var arcPath = Path()
                        for i in 0...nArc {
                            let t = thetaR + (thetaL - thetaR) * Double(i) / Double(nArc)
                            let ax = fit.cx + fit.r * cos(t)
                            let ay = fit.cy + fit.r * sin(t)
                            let pt = CGPoint(x: lx(CGFloat(ax)), y: ly(CGFloat(ay)))
                            if i == 0 { arcPath.move(to: pt) } else { arcPath.addLine(to: pt) }
                        }
                        context.stroke(arcPath, with: .color(Color(red: 0.7, green: 0.3, blue: 0.9).opacity(0.5)),
                                       lineWidth: 1.5)

                        // Rs and Rp markers
                        let fitPtColor = Color(red: 0.9, green: 0.4, blue: 0.9)
                        let rsScr = CGPoint(x: lx(CGFloat(rs)), y: ly(0))
                        let rpScr = CGPoint(x: lx(CGFloat(rs + rp)), y: ly(0))
                        context.fill(Path(ellipseIn: CGRect(x: rsScr.x - 5, y: rsScr.y - 5, width: 10, height: 10)),
                                     with: .color(fitPtColor))
                        context.fill(Path(ellipseIn: CGRect(x: rpScr.x - 5, y: rpScr.y - 5, width: 10, height: 10)),
                                     with: .color(fitPtColor))
                        context.draw(
                            Text(String(format: "Rs=%.0f", rs)).font(.caption2).foregroundStyle(fitPtColor),
                            at: CGPoint(x: rsScr.x, y: rsScr.y + 14))
                        context.draw(
                            Text(String(format: "Rp=%.0f", rp)).font(.caption2).foregroundStyle(fitPtColor),
                            at: CGPoint(x: rpScr.x, y: rpScr.y + 14))
                    }
                }
            }
        }
    }

    // MARK: - Data table

    private var eisTable: some View {
        MeasurementTable(
            columns: [
                MeasurementColumn(header: "Freq (Hz)", width: 80, alignment: .trailing),
                MeasurementColumn(header: "|Z| (Ohm)", width: 90, alignment: .trailing),
                MeasurementColumn(header: "Phase (\u{00B0})", width: 70, alignment: .trailing),
                MeasurementColumn(header: "Re (Ohm)", width: 90, alignment: .trailing),
                MeasurementColumn(header: "Im (Ohm)", width: 90, alignment: .trailing),
            ],
            rows: state.eisPoints,
            cellText: { pt, col in
                switch col {
                case 0: String(format: "%.1f", pt.freqHz)
                case 1: String(format: "%.2f", pt.magOhms)
                case 2: String(format: "%.2f", pt.phaseDeg)
                case 3: String(format: "%.2f", pt.zReal)
                case 4: String(format: "%.2f", pt.zImag)
                default: ""
                }
            }
        )
    }

    // MARK: - Helpers

    private var noDataPlaceholder: some View {
        Text("No data")
            .foregroundStyle(Color(white: 0.4))
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    private func freqLabel(_ logVal: Double) -> String {
        let hz = pow(10, logVal)
        if hz >= 1000 { return String(format: "%.0fk", hz / 1000) }
        return String(format: "%.0f", hz)
    }
}

// MARK: - Kasa circle fit (ported from plot.rs)

struct CircleFitResult {
    let cx: Double
    let cy: Double
    let r: Double
}

func kasaCircleFit(points: [(Double, Double)]) -> CircleFitResult? {
    let all = points.filter { $0.0.isFinite && $0.1.isFinite }
    guard all.count >= 4 else { return nil }

    let minPts = max(4, all.count / 3)
    var best: CircleFitResult?
    var bestScore = Double.greatestFiniteMagnitude

    for start in 0..<all.count {
        var pts = Array(all[start...])
        while pts.count >= minPts {
            guard let raw = kasaFitRaw(pts) else { break }
            let (cx, cy, r) = raw
            let disc = r * r - cy * cy
            if disc > 0 {
                let sd = sqrt(disc)
                let rp = 2 * sd
                if rp > 0 {
                    let avgErr = pts.map { p in
                        abs(sqrt((p.0 - cx) * (p.0 - cx) + (p.1 - cy) * (p.1 - cy)) - r)
                    }.reduce(0, +) / (Double(pts.count) * r)
                    let coverage = Double(pts.count) / Double(all.count)
                    let score = avgErr / coverage
                    if score < bestScore {
                        bestScore = score
                        best = CircleFitResult(cx: cx, cy: cy, r: r)
                    }
                    break
                }
            }
            // remove worst outlier
            guard let worstIdx = pts.enumerated().max(by: { a, b in
                let dA = abs(sqrt((a.element.0 - cx) * (a.element.0 - cx) +
                                  (a.element.1 - cy) * (a.element.1 - cy)) - r)
                let dB = abs(sqrt((b.element.0 - cx) * (b.element.0 - cx) +
                                  (b.element.1 - cy) * (b.element.1 - cy)) - r)
                return dA < dB
            })?.offset else { break }
            pts.remove(at: worstIdx)
        }
    }
    return best
}

private func kasaFitRaw(_ pts: [(Double, Double)]) -> (Double, Double, Double)? {
    guard pts.count >= 3 else { return nil }
    let n = Double(pts.count)
    var sx = 0.0, sy = 0.0
    var sx2 = 0.0, sy2 = 0.0, sxy = 0.0
    var sx3 = 0.0, sy3 = 0.0, sx2y = 0.0, sxy2 = 0.0

    for (x, y) in pts {
        sx += x; sy += y
        let x2 = x * x, y2 = y * y, xy = x * y
        sx2 += x2; sy2 += y2; sxy += xy
        sx3 += x2 * x; sy3 += y2 * y; sx2y += x2 * y; sxy2 += x * y2
    }

    let (a00, a01, a02) = (sx2, sxy, sx)
    let (a10, a11, a12) = (sxy, sy2, sy)
    let (a20, a21, a22) = (sx, sy, n)
    let (r0, r1, r2) = (sx3 + sxy2, sx2y + sy3, sx2 + sy2)

    let det = a00 * (a11 * a22 - a12 * a21)
            - a01 * (a10 * a22 - a12 * a20)
            + a02 * (a10 * a21 - a11 * a20)
    guard abs(det) > 1e-20 else { return nil }

    let a = (r0 * (a11 * a22 - a12 * a21)
           - a01 * (r1 * a22 - a12 * r2)
           + a02 * (r1 * a21 - a11 * r2)) / det
    let b = (a00 * (r1 * a22 - a12 * r2)
           - r0 * (a10 * a22 - a12 * a20)
           + a02 * (a10 * r2 - r1 * a20)) / det
    let c = (a00 * (a11 * r2 - r1 * a21)
           - a01 * (a10 * r2 - r1 * a20)
           + r0 * (a10 * a21 - a11 * a20)) / det

    let cx = a / 2
    let cy = b / 2
    let rSq = c + cx * cx + cy * cy
    guard rSq > 0 else { return nil }
    return (cx, cy, sqrt(rSq))
}

// MARK: - Canvas drawing helpers

private func drawPolyline(context: GraphicsContext, points: [CGPoint], color: Color, width: CGFloat) {
    guard points.count >= 2 else { return }
    var path = Path()
    path.move(to: points[0])
    for pt in points.dropFirst() {
        guard pt.x.isFinite && pt.y.isFinite else { continue }
        path.addLine(to: pt)
    }
    context.stroke(path, with: .color(color), lineWidth: width)
}

private func drawGrid(context: GraphicsContext, xl: CGFloat, xr: CGFloat, yt: CGFloat, yb: CGFloat,
                      xvLo: CGFloat, xvHi: CGFloat, yvLo: CGFloat, yvHi: CGFloat,
                      gridColor: Color, size: CGSize) {
    let xStep = niceStep(Double(xvHi - xvLo), targetTicks: 4)
    if xStep > 0 {
        var g = (Double(xvLo) / xStep).rounded(.up) * xStep
        while g <= Double(xvHi) {
            let x = xl + CGFloat((g - Double(xvLo)) / Double(xvHi - xvLo)) * (xr - xl)
            var p = Path(); p.move(to: CGPoint(x: x, y: yt)); p.addLine(to: CGPoint(x: x, y: yb))
            context.stroke(p, with: .color(gridColor), lineWidth: 0.5)
            context.draw(
                Text(String(format: "%.0f", g)).font(.system(size: 9)).foregroundStyle(.secondary),
                at: CGPoint(x: x, y: yb + 10))
            g += xStep
        }
    }
    let yStep = niceStep(Double(yvHi - yvLo), targetTicks: 4)
    if yStep > 0 {
        var g = (Double(yvLo) / yStep).rounded(.up) * yStep
        while g <= Double(yvHi) {
            let y = yt + CGFloat((Double(yvHi) - g) / Double(yvHi - yvLo)) * (yb - yt)
            var p = Path(); p.move(to: CGPoint(x: xl, y: y)); p.addLine(to: CGPoint(x: xr, y: y))
            context.stroke(p, with: .color(gridColor), lineWidth: 0.5)
            context.draw(
                Text(String(format: "%.0f", g)).font(.system(size: 9)).foregroundStyle(.secondary),
                at: CGPoint(x: xl - 20, y: y))
            g += yStep
        }
    }
}

private func niceStep(_ range: Double, targetTicks: Int) -> Double {
    guard abs(range) > 1e-10 else { return 1 }
    let rough = range / Double(targetTicks)
    let mag = pow(10, floor(log10(abs(rough))))
    let norm = abs(rough) / mag
    let s: Double
    if norm < 1.5 { s = 1 }
    else if norm < 3.5 { s = 2 }
    else if norm < 7.5 { s = 5 }
    else { s = 10 }
    return s * mag
}

// MARK: - Shared UI components

struct LabeledField: View {
    let label: String
    @Binding var text: String
    let width: CGFloat

    init(_ label: String, text: Binding<String>, width: CGFloat) {
        self.label = label
        self._text = text
        self.width = width
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 2) {
            Text(label)
                .font(.caption)
                .foregroundStyle(.secondary)
            TextField(label, text: $text)
                .textFieldStyle(.roundedBorder)
                .frame(width: width)
                #if os(iOS)
                .keyboardType(.decimalPad)
                #endif
        }
    }
}

struct LabeledPicker<Item: Hashable & Identifiable>: View {
    let label: String
    @Binding var selection: Item
    let items: [Item]
    let itemLabel: (Item) -> String

    init(_ label: String, selection: Binding<Item>, items: [Item], itemLabel: @escaping (Item) -> String) {
        self.label = label
        self._selection = selection
        self.items = items
        self.itemLabel = itemLabel
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 2) {
            Text(label)
                .font(.caption)
                .foregroundStyle(.secondary)
            Picker(label, selection: $selection) {
                ForEach(items) { item in
                    Text(itemLabel(item)).tag(item)
                }
            }
            .labelsHidden()
            #if os(iOS)
            .pickerStyle(.menu)
            #endif
        }
    }
}

struct ActionButtonStyle: ButtonStyle {
    let color: Color

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .font(.subheadline.weight(.medium))
            .padding(.horizontal, 16)
            .padding(.vertical, 8)
            .background(
                RoundedRectangle(cornerRadius: 8)
                    .fill(color.opacity(configuration.isPressed ? 0.7 : 1.0))
            )
            .foregroundStyle(.white)
    }
}

struct HSplitLayout<Leading: View, Trailing: View>: View {
    let ratio: CGFloat
    @ViewBuilder let leading: () -> Leading
    @ViewBuilder let trailing: () -> Trailing

    var body: some View {
        GeometryReader { geo in
            HStack(spacing: 0) {
                leading()
                    .frame(width: geo.size.width * ratio)
                Divider()
                trailing()
                    .frame(width: geo.size.width * (1 - ratio))
            }
        }
    }
}
