import SwiftUI
import NucleicCore
import NucleicProtocol

/// The Covalence Mesh Viewer's **graph** of the mesh: this Mac at the center, everything it can
/// reach right now on an inner ring, everything known-but-unreachable on the outer one, and one
/// line per link — our own dials (tinted by the transport they went over), the connections phones
/// open into us, and the peer-to-peer links our peers report from their own rosters.
///
/// The same `MeshGraph` snapshot the host list renders, drawn spatially: the list answers "what
/// does each host look like", the graph answers "what is connected to what, and how".
struct MeshGraphView: View {
    let graph: MeshGraph
    @Binding var selection: String?

    /// Node circle diameter, and the inset the layout's unit square is mapped into so a node's
    /// glyph *and* its caption stay inside the canvas at the rim.
    private let nodeDiameter: CGFloat = 40
    private let horizontalInset: CGFloat = 54
    private let verticalInset: CGFloat = 34

    private var positions: [String: MeshGraphPoint] { graph.layout() }

    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            GeometryReader { geo in
                let points = screenPoints(in: geo.size)
                ZStack {
                    Canvas { context, _ in
                        for edge in graph.edges {
                            draw(edge, in: context, points: points)
                        }
                    }
                    ForEach(graph.nodes) { node in
                        if let point = points[node.id] {
                            MeshGraphNodeBadge(
                                node: node, diameter: nodeDiameter,
                                selected: selection == node.id)
                                .position(point)
                                .onTapGesture {
                                    selection = selection == node.id ? nil : node.id
                                }
                        }
                    }
                }
            }
            .frame(height: 360)
            .background(Color.secondary.opacity(0.06), in: RoundedRectangle(cornerRadius: 10))
            MeshGraphLegend(graph: graph)
            if let selected = selection.flatMap(graph.node) {
                MeshGraphDetailCard(node: selected, graph: graph)
            }
        }
    }

    /// Map the layout's unit space onto the canvas, keeping the rings circular (one radius from
    /// the tighter axis) so a wide window doesn't stretch the mesh into an ellipse.
    private func screenPoints(in size: CGSize) -> [String: CGPoint] {
        let center = CGPoint(x: size.width / 2, y: size.height / 2)
        let radius = max(
            20,
            min(size.width / 2 - horizontalInset, size.height / 2 - verticalInset))
        return positions.mapValues { point in
            CGPoint(x: center.x + radius * point.x, y: center.y + radius * point.y)
        }
    }

    /// One link. A live dial is solid and tinted by its transport; a link we only have hearsay
    /// for is dashed and neutral; a dial that's currently down is faint, so an unreachable peer
    /// reads as "we know the way there, it isn't working" rather than as no link at all.
    private func draw(_ edge: MeshGraph.Edge, in context: GraphicsContext, points: [String: CGPoint]) {
        guard let from = points[edge.from], let to = points[edge.to] else { return }
        var path = Path()
        path.move(to: from)
        path.addLine(to: to)
        let color = MeshGraphStyle.color(for: edge)
        let style = StrokeStyle(
            lineWidth: edge.online ? (edge.kind == .reported ? 1 : 1.8) : 1,
            lineCap: .round,
            dash: edge.online && edge.kind != .reported ? [] : [3, 4])
        context.stroke(path, with: .color(color.opacity(edge.online ? 0.85 : 0.35)), style: style)
        // Only our own live dials get a label: their transport is first-hand truth, and it's the
        // single most useful thing on the canvas ("that peer is on the relay, not the LAN").
        guard edge.kind == .dialed, edge.online, let transport = edge.transport else { return }
        let mid = CGPoint(x: (from.x + to.x) / 2, y: (from.y + to.y) / 2)
        let text = context.resolve(
            Text(transport.label).font(.system(size: 9, weight: .medium)).foregroundStyle(color))
        let size = text.measure(in: CGSize(width: 200, height: 40))
        let box = CGRect(
            x: mid.x - size.width / 2 - 4, y: mid.y - size.height / 2 - 2,
            width: size.width + 8, height: size.height + 4)
        context.fill(
            Path(roundedRect: box, cornerRadius: 4),
            with: .color(MeshGraphStyle.labelBackground))
        context.draw(text, at: mid, anchor: .center)
    }
}

// MARK: - Node

/// One host on the canvas: kind glyph in a ring, the load arc when the host casts a presence
/// card, and its name underneath. Offline hosts stay drawn but dimmed.
private struct MeshGraphNodeBadge: View {
    let node: MeshGraph.Node
    let diameter: CGFloat
    let selected: Bool

    private var tint: Color {
        guard node.online else { return .secondary }
        if node.isSelf { return .accentColor }
        return node.kind.canOwnSessions ? .green : .blue
    }

    var body: some View {
        VStack(spacing: 4) {
            ZStack {
                Circle()
                    .fill(tint.opacity(node.online ? 0.16 : 0.08))
                Circle()
                    .strokeBorder(
                        selected ? Color.accentColor : tint.opacity(node.online ? 0.7 : 0.4),
                        lineWidth: selected ? 2.5 : (node.isSelf ? 2 : 1))
                if let ratio = node.loadRatio {
                    Circle()
                        .trim(from: 0, to: max(0.02, min(ratio, 1)))
                        .stroke(
                            MeshGraphStyle.loadColor(ratio),
                            style: StrokeStyle(lineWidth: 3, lineCap: .round))
                        .rotationEffect(.degrees(-90))
                        .padding(-3.5)
                }
                Image(systemName: MeshGraphStyle.symbol(for: node))
                    .font(.system(size: 15))
                    .foregroundStyle(node.online ? Color.primary : Color.secondary)
                if node.lastError != nil, !node.online {
                    Image(systemName: "exclamationmark.triangle.fill")
                        .font(.system(size: 9))
                        .foregroundStyle(.orange)
                        .offset(x: diameter / 2 - 4, y: -diameter / 2 + 4)
                }
            }
            .frame(width: diameter, height: diameter)
            Text(node.isSelf ? "\(node.label) (this Mac)" : node.label)
                .font(.caption2)
                .fontWeight(node.isSelf ? .semibold : .regular)
                .foregroundStyle(node.online ? Color.primary : Color.secondary)
                .lineLimit(1)
                .truncationMode(.middle)
                .frame(width: 96)
        }
        .contentShape(Rectangle())
        .help(MeshGraphStyle.tooltip(for: node))
    }
}

// MARK: - Legend

/// What the lines mean. Only the transports actually in use are listed, so the legend stays a
/// reading of *this* mesh rather than a catalogue of every path Covalence supports.
private struct MeshGraphLegend: View {
    let graph: MeshGraph

    private var transports: [SyncTransportHint] {
        var seen: [SyncTransportHint] = []
        for edge in graph.edges where edge.online {
            if let transport = edge.transport, !seen.contains(transport) { seen.append(transport) }
        }
        return seen
    }

    private var hasInbound: Bool { graph.edges.contains { $0.kind == .inbound } }
    private var hasReported: Bool { graph.edges.contains { $0.kind == .reported } }
    private var hasDown: Bool { graph.edges.contains { !$0.online } }

    var body: some View {
        HStack(spacing: 12) {
            ForEach(transports, id: \.rawValue) { transport in
                item(transport.label, color: MeshGraphStyle.transportColor(transport), dashed: false)
            }
            if hasInbound {
                item("Inbound", color: MeshGraphStyle.inbound, dashed: false)
            }
            if hasReported {
                item("Reported by a peer", color: .secondary, dashed: true)
            }
            if hasDown {
                item("Not connected", color: .secondary, dashed: true)
            }
            Spacer()
        }
        .font(.caption2)
        .foregroundStyle(.secondary)
    }

    private func item(_ label: String, color: Color, dashed: Bool) -> some View {
        HStack(spacing: 4) {
            Line()
                .stroke(
                    color.opacity(dashed ? 0.5 : 0.85),
                    style: StrokeStyle(lineWidth: 2, lineCap: .round, dash: dashed ? [3, 3] : []))
                .frame(width: 16, height: 2)
            Text(label)
        }
    }

    private struct Line: Shape {
        func path(in rect: CGRect) -> Path {
            var path = Path()
            path.move(to: CGPoint(x: rect.minX, y: rect.midY))
            path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
            return path
        }
    }
}

// MARK: - Selection detail

/// The selected host, spelled out: everything the list row shows for it, plus the links it's an
/// endpoint of — the question the graph is for ("how is this host actually reached?").
private struct MeshGraphDetailCard: View {
    @Environment(AppStore.self) private var store
    let node: MeshGraph.Node
    let graph: MeshGraph

    private var links: [MeshGraph.Edge] { graph.edges(touching: node.id) }

    var body: some View {
        VStack(alignment: .leading, spacing: 5) {
            HStack(spacing: 8) {
                Image(systemName: MeshGraphStyle.symbol(for: node))
                Text(node.isSelf ? "\(node.label) (this Mac)" : node.label)
                    .fontWeight(.semibold)
                if node.isIndirect {
                    Text("known through a peer")
                        .font(.caption2)
                        .padding(.horizontal, 5).padding(.vertical, 1)
                        .background(Color.secondary.opacity(0.15), in: Capsule())
                        .help("This Mac isn't paired with this host — a connected peer listed it.")
                }
                Spacer()
                Text(node.online ? "online" : "offline")
                    .font(.caption)
                    .foregroundStyle(node.online ? .green : .secondary)
            }
            .font(.callout)
            Text(summaryLine)
                .font(.caption)
                .foregroundStyle(.secondary)
                .textSelection(.enabled)
            if let error = node.lastError {
                Label(error, systemImage: "exclamationmark.triangle")
                    .font(.caption)
                    .foregroundStyle(.orange)
                    .textSelection(.enabled)
            }
            ForEach(links) { link in
                HStack(spacing: 6) {
                    Image(systemName: link.online ? "link" : "link.badge.plus")
                        .foregroundStyle(MeshGraphStyle.color(for: link))
                    Text(linkDescription(link))
                    Spacer()
                }
                .font(.caption)
                .foregroundStyle(.secondary)
            }
            if links.isEmpty {
                Text("No links — this host has no live connection and no reported one.")
                    .font(.caption).foregroundStyle(.secondary)
            }
        }
        .padding(10)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color.secondary.opacity(0.08), in: RoundedRectangle(cornerRadius: 8))
    }

    /// Kind, capacity and dispatch posture on one line — the presence-card facts the graph's
    /// glyph can only hint at.
    private var summaryLine: String {
        var parts: [String] = [node.kind.displayName]
        if node.hasPresenceCard {
            parts.append("\(node.activeSessionCount)/\(node.maxConcurrent.map(String.init) ?? "∞") sessions")
            parts.append("\(node.projectCount) project(s)")
            parts.append(node.acceptsMeshDispatch ? "accepts dispatch" : "declines dispatch")
            if node.canCreateProjects { parts.append("can clone at dispatch") }
            if node.canReceiveSessionTransfer { parts.append("can receive transfers") }
        } else if node.kind.canOwnSessions {
            parts.append("no presence card — not a dispatch candidate")
        }
        return parts.joined(separator: " · ")
    }

    private func linkDescription(_ link: MeshGraph.Edge) -> String {
        let otherID = link.from == node.id ? link.to : link.from
        let other = graph.node(otherID)?.label ?? store.meshHostLabel(otherID)
        switch link.kind {
        case .dialed:
            guard link.online else {
                return "→ \(other) · not connected" + (link.detail.map { " · \($0)" } ?? "")
            }
            return "→ \(other) · \(link.transport?.label ?? "connected")"
        case .inbound:
            return "← \(other) · inbound connection"
        case .reported:
            return "↔ \(other) · \(link.detail ?? "reported by a peer")"
        }
    }
}

// MARK: - Shared styling

/// One place for the graph's visual vocabulary, so the canvas, the legend and the detail card
/// can't drift apart on what a color or a glyph means.
enum MeshGraphStyle {
    static let inbound = Color.blue
    static let labelBackground = Color.gray.opacity(0.22)

    /// Transports are colored by how *direct* they are: a same-network or hole-punched path is
    /// green, the tailnet teal, the relay orange — the same "further from ideal" ordering the
    /// connection-truth strings use.
    static func transportColor(_ transport: SyncTransportHint) -> Color {
        switch transport {
        case .lan, .direct: .green
        case .tailnet: .teal
        case .relay: .orange
        }
    }

    static func color(for edge: MeshGraph.Edge) -> Color {
        guard edge.online else { return .secondary }
        return switch edge.kind {
        case .dialed: edge.transport.map(transportColor) ?? .green
        case .inbound: inbound
        case .reported: .secondary
        }
    }

    /// Green under half capacity, orange approaching it, red at or over — matching the host
    /// list's load gauge, which is the same number.
    static func loadColor(_ ratio: Double) -> Color {
        ratio < 0.5 ? .green : (ratio < 1 ? .orange : .red)
    }

    static func symbol(for node: MeshGraph.Node) -> String {
        switch node.kind {
        case .iphone: "iphone"
        case .cloud: "cloud"
        default: "desktopcomputer"
        }
    }

    static func tooltip(for node: MeshGraph.Node) -> String {
        var parts: [String] = [node.isSelf ? "\(node.label) (this Mac)" : node.label]
        parts.append(node.online ? "online" : "offline")
        if let transport = node.transport { parts.append("via \(transport.label)") }
        else if node.inbound, !node.isSelf { parts.append("inbound") }
        if node.hasPresenceCard {
            parts.append(
                "\(node.activeSessionCount)/\(node.maxConcurrent.map(String.init) ?? "∞") sessions")
        }
        return parts.joined(separator: " · ")
    }
}
