import ClientTLS
import Foundation
import Observation
import SwiftUI
import UIKit
import WireProtocol

/// B3 · Zero-`.p12` device-enrollment screen — the phone half of zero-touch.
///
/// Flow: one operator login → a short-lived `device:enroll` bearer → a
/// NON-EXPORTABLE Secure-Enclave key + PKCS#10 CSR → `POST /device/enroll` →
/// the returned leaf is stored against the SE key and presents AUTOMATICALLY on
/// the existing mTLS path (no manual certificate import, ever). After this one
/// login the cert renews itself silently (`CertificateRotationScheduler`).
///
/// Pure presentation over `EnrollmentViewModel`; validation, the flow, and error
/// mapping live in the VM. Presented from the host-menu ("自动获取证书").
struct EnrollmentScreen: View {
    @State private var model: EnrollmentViewModel

    init(model: EnrollmentViewModel) {
        _model = State(initialValue: model)
    }

    var body: some View {
        Form {
            installedSection
            enrollSection
        }
        .navigationTitle(EnrollmentCopy.title)
        .task { model.refresh() }
    }

    // MARK: - Sections

    @ViewBuilder private var installedSection: some View {
        Section(EnrollmentCopy.installedHeader) {
            if let summary = model.summary {
                LabeledContent(EnrollmentCopy.subject, value: summary.subjectCommonName ?? "—")
                LabeledContent(EnrollmentCopy.issuer, value: summary.issuerCommonName ?? "—")
                LabeledContent(EnrollmentCopy.expiry, value: model.expiryText(summary))
                if summary.isExpired() {
                    Label(EnrollmentCopy.expiredWarning, systemImage: "exclamationmark.triangle")
                        .foregroundStyle(.orange)
                }
            } else {
                Text(EnrollmentCopy.noneInstalled).foregroundStyle(.secondary)
            }
        }
    }

    @ViewBuilder private var enrollSection: some View {
        Section {
            TextField(EnrollmentCopy.controlPlaneURL, text: $model.controlPlaneURLText)
                .textContentType(.URL)
                .keyboardType(.URL)
                .textInputAutocapitalization(.never)
                .autocorrectionDisabled()
            TextField(EnrollmentCopy.subdomain, text: $model.subdomain)
                .textInputAutocapitalization(.never)
                .autocorrectionDisabled()
            TextField(EnrollmentCopy.deviceName, text: $model.deviceName)
            SecureField(EnrollmentCopy.password, text: $model.password)
                .textContentType(.password)
                .autocorrectionDisabled()
            Button {
                Task { await model.enroll() }
            } label: {
                if model.isEnrolling {
                    ProgressView()
                } else {
                    Label(
                        model.summary == nil
                            ? EnrollmentCopy.enrollAction
                            : EnrollmentCopy.rotateAction,
                        systemImage: "checkmark.shield"
                    )
                }
            }
            .disabled(!model.canEnroll)
            .accessibilityIdentifier("enroll.submit")

            if let error = model.errorMessage {
                Text(error).foregroundStyle(.red).font(.footnote)
            }
            if model.didSucceed {
                Label(EnrollmentCopy.success, systemImage: "checkmark.circle.fill")
                    .foregroundStyle(.green).font(.footnote)
            }
        } header: {
            Text(model.summary == nil ? EnrollmentCopy.enrollHeader : EnrollmentCopy.rotateHeader)
        } footer: {
            Text(EnrollmentCopy.footer)
        }
    }
}

/// B3 · Enrollment state machine. Validates inputs at the boundary, drives
/// `DeviceEnrollmentFlow`, and maps every failure to actionable copy — never
/// swallowed, never leaking the password or bearer.
@MainActor
@Observable
final class EnrollmentViewModel {
    /// The enroll operation: login → SE keygen+CSR → enroll → store. Injected so
    /// the VM is unit-testable without a keychain / network (production wires it
    /// to `DeviceEnrollmentFlow`).
    typealias EnrollOperation = @Sendable (
        _ password: String, _ subdomain: String, _ deviceName: String, _ controlPlaneURL: URL
    ) async throws -> ClientCertificateSummary?

    var controlPlaneURLText: String
    var subdomain: String
    var deviceName: String
    var password = ""

    private(set) var summary: ClientCertificateSummary?
    private(set) var isEnrolling = false
    private(set) var didSucceed = false
    private(set) var errorMessage: String?

    @ObservationIgnored private let enrollOperation: EnrollOperation
    @ObservationIgnored private let loadSummary: @Sendable () -> ClientCertificateSummary?
    @ObservationIgnored private let persistSettings: @Sendable (_ url: String, _ subdomain: String) -> Void

    init(
        controlPlaneURLText: String = EnrollmentSettings.controlPlaneURL ?? EnrollmentCopy.defaultControlPlaneURL,
        subdomain: String = EnrollmentSettings.subdomain ?? "",
        deviceName: String = UIDevice.current.name,
        enrollOperation: @escaping EnrollOperation,
        loadSummary: @escaping @Sendable () -> ClientCertificateSummary?,
        persistSettings: @escaping @Sendable (_ url: String, _ subdomain: String) -> Void = { url, sub in
            EnrollmentSettings.controlPlaneURL = url
            EnrollmentSettings.subdomain = sub
        }
    ) {
        self.controlPlaneURLText = controlPlaneURLText
        self.subdomain = subdomain
        self.deviceName = deviceName
        self.enrollOperation = enrollOperation
        self.loadSummary = loadSummary
        self.persistSettings = persistSettings
    }

    /// Fields all present → the button is enabled (deeper URL validation on tap).
    var canEnroll: Bool {
        !isEnrolling
            && !controlPlaneURLText.trimmed.isEmpty
            && !subdomain.trimmed.isEmpty
            && !deviceName.trimmed.isEmpty
            && !password.isEmpty
    }

    func refresh() {
        summary = loadSummary()
    }

    func expiryText(_ summary: ClientCertificateSummary) -> String {
        guard let notAfter = summary.notAfter else { return "—" }
        return notAfter.formatted(date: .abbreviated, time: .omitted)
    }

    /// Run enrollment. Validates the control-plane URL (must be https with a
    /// host) before any network, then drives the flow. The password is cleared
    /// after every attempt (success OR failure) so it never lingers in memory.
    func enroll() async {
        errorMessage = nil
        didSucceed = false
        let trimmedURL = controlPlaneURLText.trimmed
        let trimmedSubdomain = subdomain.trimmed
        let trimmedName = deviceName.trimmed
        guard let url = URL(string: trimmedURL),
              url.scheme?.lowercased() == "https",
              let host = url.host, !host.isEmpty else {
            errorMessage = EnrollmentCopy.errInvalidURL
            return
        }
        guard !trimmedSubdomain.isEmpty, !trimmedName.isEmpty, !password.isEmpty else {
            errorMessage = EnrollmentCopy.errMissingFields
            return
        }
        isEnrolling = true
        defer { isEnrolling = false }
        let secret = password
        do {
            summary = try await enrollOperation(secret, trimmedSubdomain, trimmedName, url)
            persistSettings(trimmedURL, trimmedSubdomain)
            password = "" // never linger after success
            didSucceed = true
            refresh()
        } catch {
            password = "" // never linger after failure either
            errorMessage = Self.copy(for: error)
        }
    }

    /// Map a login/enroll/keychain error to enrollment-UX copy (never swallowed,
    /// never containing the password/token).
    private static func copy(for error: any Error) -> String {
        switch error {
        case ControlPlaneLoginError.emptyPassword:
            return EnrollmentCopy.errEmptyPassword
        case ControlPlaneLoginError.http(let status, _) where status == 401:
            return EnrollmentCopy.errBadCredential
        case ControlPlaneLoginError.http:
            return EnrollmentCopy.errLoginFailed
        case ControlPlaneLoginError.malformedResponse:
            return EnrollmentCopy.errServer
        case DeviceEnrollmentError.http(let status, _) where status == 403:
            return EnrollmentCopy.errSubdomainNotOwned
        case DeviceEnrollmentError.http(let status, _) where status == 429:
            return EnrollmentCopy.errRateLimited
        case DeviceEnrollmentError.http(let status, _) where status == 400:
            return EnrollmentCopy.errRejected
        case DeviceEnrollmentError.http:
            return EnrollmentCopy.errEnrollFailed
        case DeviceEnrollmentError.malformedResponse:
            return EnrollmentCopy.errServer
        case SecureEnclaveKeyError.secureEnclaveUnavailable:
            return EnrollmentCopy.errNoSecureEnclave
        case is SecureEnclaveKeyError:
            return EnrollmentCopy.errKeygen
        case ClientIdentityStoreError.keychain, ClientIdentityStoreError.corruptStoredBlob:
            return EnrollmentCopy.errKeychain
        case DeviceEnrollmentWiringError.missingControlPlaneURL,
             DeviceEnrollmentWiringError.invalidControlPlaneURL:
            return EnrollmentCopy.errInvalidURL
        default:
            return EnrollmentCopy.errUnknown
        }
    }
}

private extension String {
    var trimmed: String { trimmingCharacters(in: .whitespacesAndNewlines) }
}

/// User-facing copy (Chinese, matching the rest of the app).
enum EnrollmentCopy {
    static let defaultControlPlaneURL = "https://cp.terminal.yaojia.wang"

    static let title = "自动获取证书"
    static let installedHeader = "已安装证书"
    static let subject = "设备 (CN)"
    static let issuer = "签发 CA"
    static let expiry = "有效期至"
    static let expiredWarning = "证书已过期，请重新注册。"
    static let noneInstalled = "尚未安装设备证书。"

    static let enrollHeader = "注册本设备"
    static let rotateHeader = "重新注册"
    static let controlPlaneURL = "控制面地址"
    static let subdomain = "子域名 (你拥有的隧道名)"
    static let deviceName = "设备名称"
    static let password = "操作口令"
    static let enrollAction = "注册本设备"
    static let rotateAction = "重新注册"
    static let success = "已注册，证书已保存到本设备安全区。"
    static let footer =
        "首次登录一次即可：本设备在安全区 (Secure Enclave) 生成不可导出的私钥，向控制面申请证书并自动保存；之后连接隧道主机时自动出示，证书到期前会自动续期，无需再手动导入 .p12。"

    static let errInvalidURL = "控制面地址无效，请输入 https:// 开头的完整地址。"
    static let errMissingFields = "请填写子域名、设备名称和操作口令。"
    static let errEmptyPassword = "请输入操作口令。"
    static let errBadCredential = "操作口令错误，请重试。"
    static let errLoginFailed = "登录失败，请稍后重试。"
    static let errSubdomainNotOwned = "该账号未拥有此子域名，无法签发证书。"
    static let errRateLimited = "请求过于频繁，请稍后再试。"
    static let errRejected = "请求被拒绝：子域名或证书请求无效。"
    static let errEnrollFailed = "注册失败，请稍后重试。"
    static let errNoSecureEnclave = "本设备不支持安全区 (Secure Enclave)（模拟器？），无法生成硬件密钥。"
    static let errKeygen = "生成硬件密钥失败，请重试。"
    static let errKeychain = "保存到钥匙串失败，请重试。"
    static let errServer = "服务器返回异常，请稍后重试。"
    static let errUnknown = "注册失败，请重试。"
}
