import SwiftUI
import WireProtocol

/// T-iOS-11 · Connection-state banner over the terminal (plan §1: the terminal
/// must NEVER "look connected but dead" — every non-live state is explicit).
///
/// Render input is the `Model` enum only; the mapping from engine events to a
/// `Model` lives in `TerminalViewModel.bannerModel` (tested there).
struct ReconnectBanner: View {
    /// What the banner says. Terminal phases (`failed`/`exited`) win over
    /// transient connection states (mapping in `TerminalViewModel.bannerModel`).
    enum Model: Equatable {
        case connecting
        case reconnecting(attempt: Int, retryIn: Duration)
        /// Non-retryable failure: actionable copy, NO spinner, NO retry loop.
        case failed(message: String)
        /// Session over — terminal is read-only from here on.
        case exited(code: Int, reason: String?)
    }

    let model: Model
    /// T-iOS-29 · "开新会话" on the EXITED banner only (session over → the
    /// natural next step; manager.ts:145-153 — the old session is done for
    /// good). nil = affordance hidden. Not offered on `.failed`: that copy
    /// asks the user to change a knob first, not to spawn again.
    var onNewSession: (@MainActor () -> Void)? = nil

    private enum Copy {
        static let newSession = "开新会话"
    }

    /// 精致原生：材质胶囊 + 发丝描边，状态只由前导指示色（+ 图标形状）表达，
    /// 而非整块告警底色 —— 重连读作「安静的进行中」，不惊扰（GROUP BRIEF）。
    var body: some View {
        HStack(spacing: DS.Space.sm8) {
            leadingIndicator
            Text(text)
                .font(DS.Typography.callout)
                .foregroundStyle(DS.Palette.textPrimary)
                .multilineTextAlignment(.leading)
            if let onNewSession, Self.isNewSessionActionAvailable(for: model) {
                Button(Copy.newSession) { onNewSession() }
                    .font(DS.Typography.callout.weight(.semibold))
                    .foregroundStyle(DS.Palette.accent)
                    .buttonStyle(.plain)
                    .accessibilityIdentifier("terminal.exitNewSessionButton")
            }
        }
        .padding(.horizontal, DS.Space.lg16)
        .padding(.vertical, DS.Space.sm8)
        .background(.regularMaterial, in: Capsule())
        .overlay(
            Capsule().strokeBorder(DS.Palette.hairline, lineWidth: DS.Stroke.hairline)
        )
        .accessibilityElement(children: .combine)
    }

    /// Spinner while a connection is in flight, otherwise the state's icon —
    /// both painted in the semantic indicator color (shape + color, never color
    /// alone).
    @ViewBuilder private var leadingIndicator: some View {
        if isSpinning {
            ProgressView()
                .controlSize(.small)
                .tint(indicatorColor)
        } else {
            Image(systemName: iconName)
                .foregroundStyle(indicatorColor)
        }
    }

    /// The new-session affordance appears on the `.exited` banner only —
    /// pure predicate so the T-iOS-29 tests pin the rule.
    static func isNewSessionActionAvailable(for model: Model) -> Bool {
        if case .exited = model { return true }
        return false
    }

    private var isSpinning: Bool {
        switch model {
        case .connecting, .reconnecting: return true
        case .failed, .exited: return false
        }
    }

    private var iconName: String {
        switch model {
        case .failed: return "exclamationmark.octagon.fill"
        case .exited: return "flag.checkered"
        case .connecting, .reconnecting: return "wifi"
        }
    }

    /// Semantic indicator color (DS only): connecting is quiet gray, reconnecting
    /// is the calm accent (not an orange alarm), failed is the stuck red, exited
    /// is dimmed secondary. Paired with a distinct icon so it is never color-only.
    private var indicatorColor: Color {
        switch model {
        case .connecting: return DS.Palette.textSecondary
        case .reconnecting: return DS.Palette.accent
        case .failed: return DS.Palette.statusStuck
        case .exited: return DS.Palette.statusExited
        }
    }

    private var text: String {
        switch model {
        case .connecting:
            return "连接中…"
        case .reconnecting(let attempt, let retryIn):
            return "连接已断开 · 第 \(attempt) 次重连将在 \(retryIn.components.seconds)s 后…"
        case .failed(let message):
            return message
        case .exited(let code, let reason):
            return Self.exitText(code: code, reason: reason)
        }
    }

    /// Exit copy: spawn failure (-1, M4) reads as an error; a normal exit reads
    /// as a conclusion. `reason` is server-supplied display text (untrusted —
    /// rendered as plain text only).
    private static func exitText(code: Int, reason: String?) -> String {
        if code == WireConstants.spawnFailedExitCode {
            return "启动 shell 失败：\(reason ?? "未知原因")"
        }
        let suffix = reason.map { "（\($0)）" } ?? ""
        return "会话已退出 exit \(code)\(suffix) · 终端已只读"
    }
}
