import UIKit
import AVFoundation
import Vision
import VisionKit

extension Notification.Name {
    static let ironStorageLocalStoreDidChange = Notification.Name(
        "de.rfc1437.ironstorage.local-store-did-change"
    )
    static let ironStorageAuthenticationDidChange = Notification.Name(
        "de.rfc1437.ironstorage.authentication-did-change"
    )
    static let ironStorageWatchSnapshotDidChange = Notification.Name(
        "de.rfc1437.ironstorage.watch-snapshot-did-change"
    )
    static let ironStorageKeyMaterialDidChange = Notification.Name(
        "de.rfc1437.ironstorage.key-material-did-change"
    )
}

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    private var context: AppContext?

    override init() {
        super.init()
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyMaterialDidChange),
            name: .ironStorageKeyMaterialDidChange,
            object: nil
        )
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        let window = UIWindow(frame: UIScreen.main.bounds)
        let context = AppContext()
        self.context = context
        window.rootViewController = context.makeRootController()
        window.makeKeyAndVisible()
        self.window = window
        return true
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        context?.lockForBackground()
    }

    @objc private func keyMaterialDidChange() {
        guard let window else { return }
        context?.lockForBackground()
        let context = AppContext()
        self.context = context
        window.rootViewController = context.makeRootController()
    }
}

@MainActor
private protocol MobileTabRoot: AnyObject {
    var shellTab: MobileTab { get }
}

@MainActor
private final class AppContext: NSObject, UITabBarControllerDelegate {
    private let tabs = UITabBarController()
    private let authentication = try? mobileAuthentication()
    private var navigationControllers: [UINavigationController] = []
    private var restoreTask: Task<Void, Never>?
    private var authenticationMonitor: Task<Void, Never>?

    deinit {
        restoreTask?.cancel()
        authenticationMonitor?.cancel()
    }

    func makeRootController() -> UIViewController {
        let shell = mobileShellFixture(state: .loading)
        navigationControllers = shell.pages.map { page in
            let root: UIViewController = switch page.tab {
            case .passwords:
                PasswordDirectoryViewController(shellPage: page, authentication: authentication)
            case .search:
                PasswordSearchViewController(shellPage: page, authentication: authentication)
            case .totp:
                TotpListViewController(shellPage: page, authentication: authentication)
            case .preferences:
                PreferencesViewController(page: page, authentication: authentication)
            default:
                ShellViewController(page: page)
            }
            let navigation = UINavigationController(rootViewController: root)
            navigation.navigationBar.prefersLargeTitles = true
            navigation.tabBarItem = UITabBarItem(
                title: page.title,
                image: UIImage(systemName: page.systemImage),
                selectedImage: UIImage(systemName: page.selectedSystemImage)
            )
            return navigation
        }
        tabs.viewControllers = navigationControllers
        tabs.delegate = self
        restoreSelectedTab()
        monitorAuthentication()
        return tabs
    }

    func lockForBackground() {
        guard let authentication else { return }
        try? authentication.manualLock()
        NotificationCenter.default.post(
            name: .ironStorageAuthenticationDidChange,
            object: authentication
        )
    }

    func tabBarController(
        _ tabBarController: UITabBarController,
        didSelect viewController: UIViewController
    ) {
        guard
            let navigation = viewController as? UINavigationController,
            let root = navigation.viewControllers.first as? MobileTabRoot
        else {
            return
        }
        do {
            try setSelectedMobileTab(tab: root.shellTab)
        } catch {
            let alert = UIAlertController(
                title: "Selection Was Not Saved",
                message: error.localizedDescription,
                preferredStyle: .alert
            )
            alert.addAction(UIAlertAction(title: "OK", style: .default))
            tabs.present(alert, animated: true)
        }
    }

    private func restoreSelectedTab() {
        restoreTask?.cancel()
        restoreTask = Task { [weak self] in
            let shell = await Task.detached(priority: .userInitiated) { mobileShell() }.value
            guard !Task.isCancelled, let self else { return }
            if let index = shell.pages.firstIndex(where: { $0.tab == shell.selectedTab }) {
                tabs.selectedIndex = index
            }
        }
    }

    private func monitorAuthentication() {
        authenticationMonitor?.cancel()
        guard let authentication else { return }
        authenticationMonitor = Task {
            var wasUnlocked = (try? authentication.state().unlocked) ?? false
            while !Task.isCancelled {
                do {
                    try await Task.sleep(for: .seconds(1))
                } catch {
                    return
                }
                let isUnlocked = (try? authentication.state().unlocked) ?? false
                if isUnlocked != wasUnlocked {
                    wasUnlocked = isUnlocked
                    NotificationCenter.default.post(
                        name: .ironStorageAuthenticationDidChange,
                        object: authentication
                    )
                }
            }
        }
    }
}

@MainActor
private final class ShellViewController: UITableViewController, MobileTabRoot {
    fileprivate let shellTab: MobileTab
    private var page: MobilePage
    private var loadTask: Task<Void, Never>?
    private var loadGeneration = 0
    private var homePage: MobileHomePage?
    private var homeTask: Task<Void, Never>?
    private var homeProgressTask: Task<Void, Never>?
    private var homeOperation: MobileHomeOperation?
    private var homeGeneration = 0
    private var isHomeWorking = false

    private enum HomeSection {
        case summary
        case incoming
        case outgoing
        case empty
        case notice
    }

    private enum HomeRequest: Equatable {
        case refreshIfStale
        case refresh
        case pull
    }

    init(page: MobilePage) {
        shellTab = page.tab
        self.page = page
        super.init(style: .insetGrouped)
        title = page.title
        navigationItem.largeTitleDisplayMode = .always
        refreshControl = UIRefreshControl()
        refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(localStoreDidChange),
            name: .ironStorageLocalStoreDidChange,
            object: nil
        )
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    deinit {
        loadTask?.cancel()
        homeOperation?.cancel()
        homeTask?.cancel()
        homeProgressTask?.cancel()
        NotificationCenter.default.removeObserver(self)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        apply(page)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        reloadShell()
        if shellTab == .home, page.state == .ready, homePage != nil {
            runHome(.refreshIfStale)
        }
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        loadTask?.cancel()
        cancelHomeWork()
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        guard page.state == .ready else { return 0 }
        if shellTab == .home {
            return homeSections.count
        }
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        guard shellTab == .home else { return 1 }
        switch homeSections[section] {
        case .summary: return homePage?.summaries.count ?? 0
        case .incoming: return homePage?.incoming.count ?? 0
        case .outgoing: return homePage?.outgoing.count ?? 0
        case .empty, .notice: return 1
        }
    }

    override func tableView(
        _ tableView: UITableView,
        titleForHeaderInSection section: Int
    ) -> String? {
        guard shellTab == .home else { return nil }
        switch homeSections[section] {
        case .summary: return "Status"
        case .incoming: return "Incoming Activity"
        case .outgoing: return "Outgoing Activity"
        case .empty: return "Remote Activity"
        case .notice: return "Result"
        }
    }

    override func tableView(
        _ tableView: UITableView,
        titleForFooterInSection section: Int
    ) -> String? {
        guard shellTab == .home, let homePage else { return nil }
        switch homeSections[section] {
        case .summary:
            return freshnessDescription(homePage)
        case .incoming where homePage.incomingTotal > UInt32(homePage.incoming.count):
            return "Showing \(homePage.incoming.count) of \(homePage.incomingTotal) incoming commits."
        case .outgoing where homePage.outgoingTotal > UInt32(homePage.outgoing.count):
            return "Showing \(homePage.outgoing.count) of \(homePage.outgoingTotal) outgoing commits."
        default:
            return nil
        }
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        if shellTab == .home {
            return homeCell(at: indexPath)
        }
        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
        var content = cell.defaultContentConfiguration()
        content.image = UIImage(systemName: page.systemImage)
        content.text = page.stateTitle
        content.secondaryText = page.stateDetail
        content.secondaryTextProperties.numberOfLines = 0
        cell.contentConfiguration = content
        cell.selectionStyle = .none
        cell.accessibilityLabel = "\(page.stateTitle). \(page.stateDetail)"
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: false)
        guard shellTab == .home, let homePage else { return }
        let section = homeSections[indexPath.section]
        let commit: MobileHomeCommit
        let incoming: Bool
        switch section {
        case .incoming:
            commit = homePage.incoming[indexPath.row]
            incoming = true
        case .outgoing:
            commit = homePage.outgoing[indexPath.row]
            incoming = false
        default:
            return
        }
        navigationController?.pushViewController(
            MobileCommitActivityViewController(commit: commit, incoming: incoming),
            animated: true
        )
    }

    @objc private func refreshRequested() {
        if shellTab == .home, page.state == .ready {
            guard !isHomeWorking else {
                refreshControl?.endRefreshing()
                return
            }
            runHome(.pull)
        } else {
            reloadShell()
        }
    }

    @objc private func localStoreDidChange() {
        guard shellTab != .home else { return }
        reloadShell()
    }

    private func reloadShell() {
        loadGeneration += 1
        let generation = loadGeneration
        loadTask?.cancel()
        loadTask = Task { [weak self] in
            let shell = await Task.detached(priority: .userInitiated) { mobileShell() }.value
            guard
                !Task.isCancelled,
                let self,
                generation == loadGeneration,
                let page = shell.pages.first(where: { $0.tab == self.shellTab })
            else { return }
            apply(page)
        }
    }

    private func apply(_ page: MobilePage) {
        self.page = page
        title = page.title
        if page.state == .empty, shellTab == .home {
            navigationItem.rightBarButtonItem = UIBarButtonItem(
                title: "Set Up",
                style: .done,
                target: self,
                action: #selector(setupRequested)
            )
        } else if page.state == .ready, shellTab == .home {
            navigationItem.rightBarButtonItem = UIBarButtonItem(
                image: UIImage(systemName: "arrow.clockwise"),
                style: .plain,
                target: self,
                action: #selector(statusRefreshRequested)
            )
            navigationItem.rightBarButtonItem?.accessibilityLabel = "Refresh remote status"
            navigationItem.rightBarButtonItem?.isEnabled = !isHomeWorking
        } else if page.state == .ready, shellTab == .preferences {
            navigationItem.rightBarButtonItem = UIBarButtonItem(
                title: "Update Token",
                style: .plain,
                target: self,
                action: #selector(tokenUpdateRequested)
            )
        } else {
            navigationItem.rightBarButtonItem = nil
        }
        refreshControl?.endRefreshing()
        tableView.reloadData()

        guard page.state != .ready else {
            contentUnavailableConfiguration = nil
            if shellTab == .home {
                loadHomeIfNeeded()
            }
            return
        }
        var configuration = page.state == .loading
            ? UIContentUnavailableConfiguration.loading()
            : UIContentUnavailableConfiguration.empty()
        configuration.text = page.stateTitle
        configuration.secondaryText = page.stateDetail
        configuration.image = UIImage(systemName: stateImage(page.state))
        contentUnavailableConfiguration = configuration
    }

    @objc private func setupRequested() {
        navigationController?.pushViewController(OnboardingViewController(), animated: true)
    }

    @objc private func tokenUpdateRequested() {
        navigationController?.pushViewController(TokenUpdateViewController(), animated: true)
    }

    @objc private func statusRefreshRequested() {
        runHome(.refresh)
    }

    private var homeSections: [HomeSection] {
        guard let homePage else { return [] }
        var sections: [HomeSection] = [.summary]
        if !homePage.incoming.isEmpty {
            sections.append(.incoming)
        }
        if !homePage.outgoing.isEmpty {
            sections.append(.outgoing)
        }
        if homePage.incomingTotal == 0, homePage.outgoingTotal == 0 {
            sections.append(.empty)
        }
        if homePage.notice != nil {
            sections.append(.notice)
        }
        return sections
    }

    private func homeCell(at indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
        guard let homePage else { return cell }
        var content = cell.defaultContentConfiguration()
        switch homeSections[indexPath.section] {
        case .summary:
            let row = homePage.summaries[indexPath.row]
            content.image = UIImage(systemName: row.systemImage)
            content.text = row.title
            content.secondaryText = row.detail
            cell.selectionStyle = .none
        case .incoming:
            configureCommit(
                homePage.incoming[indexPath.row],
                content: &content,
                cell: cell
            )
        case .outgoing:
            configureCommit(
                homePage.outgoing[indexPath.row],
                content: &content,
                cell: cell
            )
        case .empty:
            content.image = UIImage(systemName: "checkmark.circle")
            content.text = "No Remote Activity"
            content.secondaryText = "There are no commits to pull or push."
            cell.selectionStyle = .none
        case .notice:
            if let notice = homePage.notice {
                content.image = UIImage(systemName: notice.systemImage)
                content.text = notice.title
                content.secondaryText = notice.detail
            }
            content.secondaryTextProperties.numberOfLines = 0
            cell.selectionStyle = .none
        }
        content.secondaryTextProperties.numberOfLines = 0
        cell.contentConfiguration = content
        return cell
    }

    private func configureCommit(
        _ commit: MobileHomeCommit,
        content: inout UIListContentConfiguration,
        cell: UITableViewCell
    ) {
        content.image = UIImage(systemName: commit.systemImage)
        content.text = commit.title
        content.secondaryText = "\(commit.detail)\n\(formattedDate(commit.timestamp))"
        content.secondaryTextProperties.numberOfLines = 0
        cell.accessoryType = .disclosureIndicator
        cell.accessibilityHint = "Shows password-store changes in this commit."
    }

    private func freshnessDescription(_ page: MobileHomePage) -> String {
        let refreshed = page.refreshedAt.map(formattedDate)
        switch page.freshness {
        case .neverRefreshed:
            return "Not refreshed. Pull down to update the local clone or tap Refresh Status."
        case .cached:
            return "Cached · Last refreshed \(refreshed ?? "at an unknown time"). This is not live status."
        case .current:
            return "Current · Refreshed \(refreshed ?? "just now")."
        }
    }

    private func formattedDate(_ timestamp: Int64) -> String {
        Date(timeIntervalSince1970: TimeInterval(timestamp)).formatted(
            date: .abbreviated,
            time: .shortened
        )
    }

    private func loadHomeIfNeeded() {
        guard homePage == nil, homeTask == nil, !isHomeWorking else { return }
        homeGeneration += 1
        let current = homeGeneration
        let operation = mobileHomeOperation()
        homeOperation = operation
        isHomeWorking = true
        navigationItem.rightBarButtonItem?.isEnabled = false
        showHomeLoading()
        homeTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    return Result<MobileHomePage, HomeFailure>.success(try operation.cached())
                } catch let error as MobileHomeFfiError {
                    return .failure(HomeFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard let self, current == homeGeneration else { return }
            finishHome(result, request: nil)
            if case .success = result {
                runHome(.refreshIfStale)
            }
        }
    }

    private func runHome(_ request: HomeRequest) {
        guard page.state == .ready, shellTab == .home, !isHomeWorking else { return }
        cancelHomeWork()
        homeGeneration += 1
        let current = homeGeneration
        let operation = mobileHomeOperation()
        homeOperation = operation
        isHomeWorking = true
        navigationItem.rightBarButtonItem?.isEnabled = false
        showHomeProgress(operation.progress())
        homeProgressTask = Task { [weak self] in
            while !Task.isCancelled {
                do {
                    try await Task.sleep(for: .milliseconds(150))
                } catch {
                    return
                }
                guard !Task.isCancelled, let self, current == homeGeneration else { return }
                showHomeProgress(operation.progress())
            }
        }
        homeTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    let page: MobileHomePage = switch request {
                    case .refreshIfStale: try operation.refreshIfStale()
                    case .refresh: try operation.refresh()
                    case .pull: try operation.pull()
                    }
                    return Result<MobileHomePage, HomeFailure>.success(page)
                } catch let error as MobileHomeFfiError {
                    return .failure(HomeFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard let self, current == homeGeneration else { return }
            finishHome(result, request: request)
        }
    }

    private func finishHome(
        _ result: Result<MobileHomePage, HomeFailure>,
        request: HomeRequest?
    ) {
        homeProgressTask?.cancel()
        homeProgressTask = nil
        homeTask = nil
        homeOperation = nil
        isHomeWorking = false
        navigationItem.titleView = nil
        navigationItem.prompt = nil
        navigationItem.rightBarButtonItem?.isEnabled = true
        refreshControl?.endRefreshing()
        switch result {
        case let .success(homePage):
            self.homePage = homePage
            contentUnavailableConfiguration = nil
            tableView.reloadData()
            if request == .pull {
                NotificationCenter.default.post(name: .ironStorageLocalStoreDidChange, object: nil)
                if let notice = homePage.notice {
                    UIAccessibility.post(notification: .announcement, argument: notice.title)
                }
            }
        case let .failure(failure):
            tableView.reloadData()
            if homePage == nil {
                showHomeFailure(failure)
            } else if failure.kind != .interrupted {
                presentHomeFailure(failure)
            }
        }
    }

    private func showHomeLoading() {
        var configuration = UIContentUnavailableConfiguration.loading()
        configuration.text = "Loading Activity"
        configuration.secondaryText = "Reading cached remote-branch state from storage."
        contentUnavailableConfiguration = configuration
    }

    private func showHomeFailure(_ failure: HomeFailure) {
        var configuration = UIContentUnavailableConfiguration.empty()
        configuration.image = UIImage(systemName: "exclamationmark.triangle")
        configuration.text = failure.title
        configuration.secondaryText = failure.detail
        contentUnavailableConfiguration = configuration
    }

    private func presentHomeFailure(_ failure: HomeFailure) {
        let alert = UIAlertController(
            title: failure.title,
            message: failure.detail,
            preferredStyle: .alert
        )
        if failure.kind == .authentication || failure.kind == .secureStorage {
            alert.addAction(UIAlertAction(title: "Preferences", style: .default) { [weak self] _ in
                guard let tabs = self?.tabBarController else { return }
                tabs.selectedIndex = tabs.viewControllers?.firstIndex { controller in
                    guard let navigation = controller as? UINavigationController else { return false }
                    return (navigation.viewControllers.first as? MobileTabRoot)?.shellTab == .preferences
                } ?? tabs.selectedIndex
            })
        }
        alert.addAction(UIAlertAction(title: "OK", style: .cancel))
        present(alert, animated: true)
    }

    private func showHomeProgress(_ progress: MobileHomeProgress) {
        let spinner = UIActivityIndicatorView(style: .medium)
        spinner.startAnimating()
        spinner.accessibilityLabel = "In progress"
        let label = UILabel()
        label.text = progress.title
        label.font = .preferredFont(forTextStyle: .headline)
        label.adjustsFontForContentSizeCategory = true
        let stack = UIStackView(arrangedSubviews: [spinner, label])
        stack.spacing = 8
        navigationItem.titleView = stack
        navigationItem.prompt = progress.detail
    }

    private func cancelHomeWork() {
        homeGeneration += 1
        homeOperation?.cancel()
        homeOperation = nil
        homeTask?.cancel()
        homeTask = nil
        homeProgressTask?.cancel()
        homeProgressTask = nil
        isHomeWorking = false
        navigationItem.titleView = nil
        navigationItem.prompt = nil
        refreshControl?.endRefreshing()
        navigationItem.rightBarButtonItem?.isEnabled = true
    }

    private func stateImage(_ state: MobileShellState) -> String {
        switch state {
        case .loading: "hourglass"
        case .empty: "lock.shield"
        case .ready: page.systemImage
        case .locked: "lock.fill"
        case .error: "exclamationmark.triangle"
        }
    }
}

@MainActor
private final class PreferencesViewController: UITableViewController, MobileTabRoot {
    fileprivate let shellTab = MobileTab.preferences
    private var page: MobilePage
    private let authentication: MobileAuthentication?
    private let keyTransfer = try? mobileKeyTransfer()
    private var state: MobileAuthenticationState?
    private var gitIdentity: MobileGitIdentity?
    private var preferenceTask: Task<Void, Never>?
    private var loadTask: Task<Void, Never>?
    private var loadGeneration = 0

    init(page: MobilePage, authentication: MobileAuthentication?) {
        self.page = page
        self.authentication = authentication
        super.init(style: .insetGrouped)
        title = page.title
        navigationItem.largeTitleDisplayMode = .always
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            title: "Update Token",
            style: .plain,
            target: self,
            action: #selector(tokenUpdateRequested)
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(authenticationDidChange),
            name: .ironStorageAuthenticationDidChange,
            object: nil
        )
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    deinit {
        preferenceTask?.cancel()
        loadTask?.cancel()
        NotificationCenter.default.removeObserver(self)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        reloadShell()
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        page.state == .ready ? 4 : 0
    }

    override func tableView(
        _ tableView: UITableView,
        numberOfRowsInSection section: Int
    ) -> Int {
        section == 1 ? 2 : (section == 3 ? 2 : 1)
    }

    override func tableView(
        _ tableView: UITableView,
        titleForHeaderInSection section: Int
    ) -> String? {
        switch section {
        case 0: "Git Commit Identity"
        case 1: "GPG Key Transfer"
        case 2: "Secure Unlock"
        default: "Authentication Session"
        }
    }

    override func tableView(
        _ tableView: UITableView,
        titleForFooterInSection section: Int
    ) -> String? {
        if section == 0 {
            return "Used as the author for commits created by the iPhone app."
        }
        if section == 1 {
            return "Scan or display ASCII-armored GPG keys. Private-key transfers require explicit confirmation and passphrase validation."
        }
        if section == 2 {
            return "When enabled, the GPG passphrase is device-only, requires a device passcode, and is invalidated when enrolled biometrics change."
        }
        return "Manual lock and inactivity expiry immediately revoke the shared Rust authentication lease."
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
        var content = cell.defaultContentConfiguration()
        if indexPath.section == 0 {
            content.image = UIImage(systemName: "person.crop.circle")
            content.text = gitIdentity?.name ?? "Git Commit Identity"
            content.secondaryText = gitIdentity?.email ?? "Set the commit author name and email"
            cell.accessoryType = .disclosureIndicator
            cell.isUserInteractionEnabled = authentication != nil
            cell.contentView.alpha = authentication == nil ? 0.45 : 1
        } else if indexPath.section == 1 {
            let importing = indexPath.row == 0
            content.image = UIImage(systemName: importing ? "qrcode.viewfinder" : "qrcode")
            content.text = importing ? "Import GPG Key" : "Export GPG Key"
            content.secondaryText = importing
                ? "Scan one or more transfer QR codes"
                : "Display public or private key armor"
            cell.accessoryType = .disclosureIndicator
            cell.isUserInteractionEnabled = keyTransfer != nil
            cell.contentView.alpha = keyTransfer == nil ? 0.45 : 1
        } else if indexPath.section == 2 {
            content.image = UIImage(systemName: "faceid")
            content.text = "Biometric Unlock"
            content.secondaryText = state?.biometricUnlockEnabled == true
                ? "Protected passphrase enrolled"
                : "Manual passphrase required"
            let toggle = UISwitch()
            toggle.isOn = state?.biometricUnlockEnabled == true
            toggle.isEnabled = authentication != nil
            toggle.addTarget(self, action: #selector(biometricToggleChanged(_:)), for: .valueChanged)
            toggle.accessibilityLabel = "Biometric Unlock"
            cell.accessoryView = toggle
            cell.selectionStyle = .none
        } else if indexPath.row == 0 {
            let unlocked = state?.unlocked == true
            content.image = UIImage(systemName: unlocked ? "lock.open.fill" : "lock.fill")
            content.text = unlocked ? "Unlocked" : "Locked"
            content.secondaryText = unlocked
                ? "Locks in \(state?.remainingSeconds ?? 0) seconds without activity"
                : "Protected content is masked"
            cell.selectionStyle = .none
        } else {
            content.image = UIImage(systemName: "lock.fill")
            content.text = "Lock Now"
            content.textProperties.color = .systemRed
            content.secondaryText = "Revoke all active authentication handles"
            cell.accessoryType = .disclosureIndicator
            cell.isUserInteractionEnabled = state?.unlocked == true
            cell.contentView.alpha = state?.unlocked == true ? 1 : 0.45
        }
        content.secondaryTextProperties.numberOfLines = 0
        cell.contentConfiguration = content
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        if indexPath.section == 0 {
            editGitIdentity()
            return
        }
        if indexPath.section == 1 {
            guard let keyTransfer else { return }
            if indexPath.row == 0 {
                requestKeyScanner(keyTransfer)
            } else {
                navigationController?.pushViewController(
                    KeyExportListViewController(transfer: keyTransfer),
                    animated: true
                )
            }
            return
        }
        guard indexPath.section == 3, indexPath.row == 1, let authentication else { return }
        do {
            try authentication.manualLock()
            refreshState()
            NotificationCenter.default.post(
                name: .ironStorageAuthenticationDidChange,
                object: authentication
            )
            UIAccessibility.post(notification: .announcement, argument: "IronStorage locked")
        } catch let error as MobileAuthenticationFfiError {
            presentAuthenticationFailure(AuthenticationFailure(error))
        } catch {
            presentAuthenticationFailure(.unexpected)
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        apply(page)
    }

    private func reloadShell() {
        loadGeneration += 1
        let generation = loadGeneration
        loadTask?.cancel()
        loadTask = Task { [weak self] in
            let shell = await Task.detached(priority: .userInitiated) { mobileShell() }.value
            guard
                !Task.isCancelled,
                let self,
                generation == loadGeneration,
                let page = shell.pages.first(where: { $0.tab == .preferences })
            else { return }
            apply(page)
        }
    }

    private func apply(_ page: MobilePage) {
        self.page = page
        title = page.title
        guard page.state == .ready else {
            var configuration = UIContentUnavailableConfiguration.empty()
            configuration.image = UIImage(systemName: "gearshape")
            configuration.text = page.stateTitle
            configuration.secondaryText = page.stateDetail
            contentUnavailableConfiguration = configuration
            tableView.reloadData()
            return
        }
        contentUnavailableConfiguration = nil
        refreshState()
    }

    @objc private func tokenUpdateRequested() {
        navigationController?.pushViewController(TokenUpdateViewController(), animated: true)
    }

    private func requestKeyScanner(_ transfer: MobileKeyTransfer) {
        guard DataScannerViewController.isSupported else {
            presentMessage(
                title: "Key Scanning Is Unavailable",
                detail: "This device does not support live QR scanning."
            )
            return
        }
        switch AVCaptureDevice.authorizationStatus(for: .video) {
        case .authorized:
            showKeyScanner(transfer)
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
                Task { @MainActor in
                    guard let self else { return }
                    if granted {
                        self.showKeyScanner(transfer)
                    } else {
                        self.showCameraDenied()
                    }
                }
            }
        default:
            showCameraDenied()
        }
    }

    private func showKeyScanner(_ transfer: MobileKeyTransfer) {
        guard DataScannerViewController.isAvailable else {
            presentMessage(
                title: "Camera Is Unavailable",
                detail: "Close other camera apps and try again."
            )
            return
        }
        navigationController?.pushViewController(
            KeyImportScannerViewController(transfer: transfer),
            animated: true
        )
    }

    private func showCameraDenied() {
        let alert = UIAlertController(
            title: "Camera Access Is Off",
            message: "Allow camera access in Settings to scan GPG key QR codes.",
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        alert.addAction(UIAlertAction(title: "Open Settings", style: .default) { _ in
            guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
            UIApplication.shared.open(url)
        })
        present(alert, animated: true)
    }

    private func presentMessage(title: String, detail: String) {
        let alert = UIAlertController(title: title, message: detail, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }

    private func editGitIdentity() {
        guard let authentication else {
            presentAuthenticationFailure(.unavailable)
            return
        }
        let alert = UIAlertController(
            title: "Git Commit Identity",
            message: "Enter the author used for commits created on this iPhone.",
            preferredStyle: .alert
        )
        alert.addTextField { [gitIdentity] field in
            field.placeholder = "Name"
            field.text = gitIdentity?.name
            field.textContentType = .name
            field.autocapitalizationType = .words
            field.clearButtonMode = .whileEditing
        }
        alert.addTextField { [gitIdentity] field in
            field.placeholder = "Email"
            field.text = gitIdentity?.email
            field.textContentType = .emailAddress
            field.keyboardType = .emailAddress
            field.autocapitalizationType = .none
            field.autocorrectionType = .no
            field.clearButtonMode = .whileEditing
        }
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        alert.addAction(UIAlertAction(title: "Save", style: .default) { [weak self, weak alert] _ in
            guard
                let self,
                let fields = alert?.textFields,
                fields.count == 2
            else { return }
            saveGitIdentity(
                authentication,
                name: fields[0].text ?? "",
                email: fields[1].text ?? ""
            )
        })
        present(alert, animated: true)
    }

    private func saveGitIdentity(
        _ authentication: MobileAuthentication,
        name: String,
        email: String
    ) {
        preferenceTask?.cancel()
        preferenceTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    return Result<MobileGitIdentity, AuthenticationFailure>.success(
                        try authentication.setGitIdentity(name: name, email: email)
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self else { return }
            switch result {
            case let .success(identity):
                gitIdentity = identity
                tableView.reloadSections(IndexSet(integer: 0), with: .automatic)
                UIAccessibility.post(
                    notification: .announcement,
                    argument: "Git commit identity updated"
                )
            case let .failure(failure):
                presentAuthenticationFailure(failure)
            }
        }
    }

    @objc private func authenticationDidChange() {
        refreshState()
    }

    @objc private func biometricToggleChanged(_ sender: UISwitch) {
        guard let authentication else {
            sender.setOn(false, animated: true)
            presentAuthenticationFailure(.unavailable)
            return
        }
        sender.isEnabled = false
        let enabled = sender.isOn
        preferenceTask?.cancel()
        preferenceTask = Task { [weak self, weak sender] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    return Result<MobileAuthenticationState, AuthenticationFailure>.success(
                        try authentication.setBiometricUnlock(enabled: enabled)
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self else { return }
            sender?.isEnabled = true
            switch result {
            case let .success(state):
                self.state = state
                tableView.reloadData()
                NotificationCenter.default.post(
                    name: .ironStorageAuthenticationDidChange,
                    object: authentication
                )
            case let .failure(failure):
                sender?.setOn(!enabled, animated: true)
                presentAuthenticationFailure(failure)
            }
        }
    }

    private func refreshState() {
        state = try? authentication?.state()
        gitIdentity = try? authentication?.gitIdentity()
        tableView.reloadData()
    }
}

@MainActor
private final class KeyImportScannerViewController: UIViewController,
    DataScannerViewControllerDelegate
{
    private let importer: MobileKeyTransferImport
    private let scanner = DataScannerViewController(
        recognizedDataTypes: [.barcode(symbologies: [.qr])],
        qualityLevel: .balanced,
        recognizesMultipleItems: true,
        isHighFrameRateTrackingEnabled: false,
        isPinchToZoomEnabled: true,
        isGuidanceEnabled: true,
        isHighlightingEnabled: true
    )
    private let progressLabel = UILabel()
    private let shield = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
    private var completing = false

    init(transfer: MobileKeyTransfer) {
        importer = transfer.importer()
        super.init(nibName: nil, bundle: nil)
        title = "Import GPG Key"
        navigationItem.largeTitleDisplayMode = .never
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        scanner.delegate = self
        addChild(scanner)
        scanner.view.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(scanner.view)
        scanner.didMove(toParent: self)

        let material = UIBlurEffect(style: .systemMaterial)
        let status = UIVisualEffectView(effect: material)
        status.layer.cornerRadius = 12
        status.clipsToBounds = true
        status.translatesAutoresizingMaskIntoConstraints = false
        progressLabel.text = "Point the camera at a key-transfer QR code"
        progressLabel.font = .preferredFont(forTextStyle: .callout)
        progressLabel.adjustsFontForContentSizeCategory = true
        progressLabel.numberOfLines = 0
        progressLabel.textAlignment = .center
        progressLabel.translatesAutoresizingMaskIntoConstraints = false
        status.contentView.addSubview(progressLabel)
        view.addSubview(status)

        let shieldLabel = UILabel()
        shieldLabel.text = "Camera hidden while IronStorage is inactive or the screen is captured"
        shieldLabel.font = .preferredFont(forTextStyle: .headline)
        shieldLabel.adjustsFontForContentSizeCategory = true
        shieldLabel.numberOfLines = 0
        shieldLabel.textAlignment = .center
        shieldLabel.translatesAutoresizingMaskIntoConstraints = false
        shield.contentView.addSubview(shieldLabel)
        shield.translatesAutoresizingMaskIntoConstraints = false
        shield.isHidden = true
        shield.accessibilityViewIsModal = true
        view.addSubview(shield)

        NSLayoutConstraint.activate([
            scanner.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scanner.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scanner.view.topAnchor.constraint(equalTo: view.topAnchor),
            scanner.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            status.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
            status.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
            status.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
            progressLabel.leadingAnchor.constraint(equalTo: status.contentView.leadingAnchor, constant: 16),
            progressLabel.trailingAnchor.constraint(equalTo: status.contentView.trailingAnchor, constant: -16),
            progressLabel.topAnchor.constraint(equalTo: status.contentView.topAnchor, constant: 12),
            progressLabel.bottomAnchor.constraint(equalTo: status.contentView.bottomAnchor, constant: -12),
            shield.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            shield.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            shield.topAnchor.constraint(equalTo: view.topAnchor),
            shield.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            shieldLabel.leadingAnchor.constraint(equalTo: shield.contentView.leadingAnchor, constant: 32),
            shieldLabel.trailingAnchor.constraint(equalTo: shield.contentView.trailingAnchor, constant: -32),
            shieldLabel.centerYAnchor.constraint(equalTo: shield.contentView.centerYAnchor),
        ])

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appWillResignActive),
            name: UIApplication.willResignActiveNotification,
            object: nil
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appDidBecomeActive),
            name: UIApplication.didBecomeActiveNotification,
            object: nil
        )
        registerForTraitChanges([UITraitSceneCaptureState.self]) {
            (controller: KeyImportScannerViewController, _: UITraitCollection) in
            controller.updateCaptureShield()
        }
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        if !completing {
            do {
                try scanner.startScanning()
            } catch {
                presentFailure(error.localizedDescription)
            }
        }
    }

    override func viewWillDisappear(_ animated: Bool) {
        scanner.stopScanning()
        super.viewWillDisappear(animated)
    }

    func dataScanner(
        _ dataScanner: DataScannerViewController,
        didAdd addedItems: [RecognizedItem],
        allItems: [RecognizedItem]
    ) {
        guard !completing else { return }
        for item in addedItems {
            guard case let .barcode(barcode) = item, let payload = barcode.payloadStringValue else {
                continue
            }
            do {
                let progress = try importer.addFrame(payload: payload)
                if progress.duplicate {
                    progressLabel.text = "Already scanned — show the next QR code"
                } else {
                    progressLabel.text = progress.key == nil
                        ? "Scanned \(progress.received) of \(progress.total)"
                        : "Key transfer complete"
                }
                UIAccessibility.post(notification: .announcement, argument: progressLabel.text)
                if let key = progress.key {
                    completing = true
                    scanner.stopScanning()
                    confirmImport(key)
                    return
                }
            } catch {
                progressLabel.text = "That QR code could not be added"
                UIAccessibility.post(notification: .announcement, argument: progressLabel.text)
                presentFailure(error.localizedDescription)
            }
        }
    }

    func dataScanner(
        _ dataScanner: DataScannerViewController,
        becameUnavailableWithError error: DataScannerViewController.ScanningUnavailable
    ) {
        presentFailure("Live scanning became unavailable. Try again when the camera is free.")
    }

    @objc private func appWillResignActive() {
        shield.isHidden = false
        scanner.stopScanning()
    }

    @objc private func appDidBecomeActive() {
        updateCaptureShield()
    }

    private func updateCaptureShield() {
        let captured = traitCollection.sceneCaptureState == .active
        shield.isHidden = !captured
        if captured || completing || view.window == nil {
            scanner.stopScanning()
        } else {
            try? scanner.startScanning()
        }
    }

    private func confirmImport(_ key: MobileKeyTransferKey) {
        let privateKey = key.kind == .private
        let warning = privateKey
            ? "Import this private key only if you trust the device that displayed it."
            : "Import this public key?"
        let alert = UIAlertController(
            title: privateKey ? "Import Private GPG Key?" : "Import Public GPG Key?",
            message: "\(warning)\n\n\(key.title)\n\(key.detail)",
            preferredStyle: .alert
        )
        if privateKey && key.requiresPassphrase {
            alert.addTextField { field in
                field.placeholder = "GPG key passphrase"
                field.isSecureTextEntry = true
                field.textContentType = .password
                field.clearButtonMode = .whileEditing
            }
        }
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { [weak self] _ in
            self?.navigationController?.popViewController(animated: true)
        })
        alert.addAction(UIAlertAction(title: "Import", style: .default) { [weak self, weak alert] _ in
            self?.finishImport(passphrase: alert?.textFields?.first?.text, makeDefault: false)
        })
        if privateKey {
            alert.addAction(UIAlertAction(title: "Import as Default", style: .default) {
                [weak self, weak alert] _ in
                self?.finishImport(passphrase: alert?.textFields?.first?.text, makeDefault: true)
            })
        }
        present(alert, animated: true)
    }

    private func finishImport(passphrase: String?, makeDefault: Bool) {
        do {
            let outcome = try importer.import(passphrase: passphrase, makeDefault: makeDefault)
            let alert = UIAlertController(
                title: outcome.title,
                message: outcome.detail,
                preferredStyle: .alert
            )
            alert.addAction(UIAlertAction(title: "Done", style: .default) { [weak self] _ in
                guard self != nil else { return }
                NotificationCenter.default.post(name: .ironStorageKeyMaterialDidChange, object: nil)
            })
            present(alert, animated: true)
        } catch {
            completing = false
            presentFailure(error.localizedDescription) { [weak self] in
                guard let self else { return }
                try? scanner.startScanning()
            }
        }
    }

    private func presentFailure(_ detail: String, completion: (() -> Void)? = nil) {
        guard presentedViewController == nil else { return }
        let alert = UIAlertController(
            title: "GPG Key Transfer Failed",
            message: detail,
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in completion?() })
        present(alert, animated: true)
    }
}

@MainActor
private final class KeyExportListViewController: UITableViewController {
    private let transfer: MobileKeyTransfer
    private let keys: [MobileKeyTransferKey]

    init(transfer: MobileKeyTransfer) {
        self.transfer = transfer
        keys = transfer.keys()
        super.init(style: .insetGrouped)
        title = "Export GPG Key"
        navigationItem.largeTitleDisplayMode = .never
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        if keys.isEmpty {
            var configuration = UIContentUnavailableConfiguration.empty()
            configuration.image = UIImage(systemName: "key.slash")
            configuration.text = "No GPG Keys"
            configuration.secondaryText = "Import a key before exporting one."
            contentUnavailableConfiguration = configuration
        }
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        keys.count
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        let key = keys[indexPath.row]
        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
        var content = cell.defaultContentConfiguration()
        content.image = UIImage(systemName: key.kind == .private ? "key.fill" : "key")
        content.text = key.title
        content.secondaryText = key.detail
        content.secondaryTextProperties.numberOfLines = 0
        cell.contentConfiguration = content
        cell.accessoryType = .disclosureIndicator
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        let key = keys[indexPath.row]
        let sheet = UIAlertController(title: key.title, message: key.detail, preferredStyle: .actionSheet)
        sheet.addAction(UIAlertAction(title: "Export Public Key", style: .default) {
            [weak self] _ in self?.export(key, kind: .public, passphrase: nil)
        })
        if key.kind == .private {
            sheet.addAction(UIAlertAction(title: "Export Private Key…", style: .destructive) {
                [weak self] _ in self?.confirmPrivateExport(key)
            })
        }
        sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        if let popover = sheet.popoverPresentationController {
            popover.sourceView = tableView.cellForRow(at: indexPath)
            popover.sourceRect = tableView.cellForRow(at: indexPath)?.bounds ?? .zero
        }
        present(sheet, animated: true)
    }

    private func confirmPrivateExport(_ key: MobileKeyTransferKey) {
        let alert = UIAlertController(
            title: "Display Private GPG Key?",
            message: "Anyone who scans these QR codes can use this private key.\n\n\(key.title)\n\(key.detail)",
            preferredStyle: .alert
        )
        if key.requiresPassphrase {
            alert.addTextField { field in
                field.placeholder = "GPG key passphrase"
                field.isSecureTextEntry = true
                field.textContentType = .password
                field.clearButtonMode = .whileEditing
            }
        }
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        alert.addAction(UIAlertAction(title: "Display", style: .destructive) {
            [weak self, weak alert] _ in
            self?.export(key, kind: .private, passphrase: alert?.textFields?.first?.text)
        })
        present(alert, animated: true)
    }

    private func export(
        _ key: MobileKeyTransferKey,
        kind: MobileKeyTransferKind,
        passphrase: String?
    ) {
        do {
            let exported = try transfer.export(
                fingerprint: key.fingerprint,
                kind: kind,
                passphrase: passphrase
            )
            navigationController?.pushViewController(
                KeyQrExportViewController(exported: exported),
                animated: true
            )
        } catch {
            let alert = UIAlertController(
                title: "GPG Key Could Not Be Exported",
                message: error.localizedDescription,
                preferredStyle: .alert
            )
            alert.addAction(UIAlertAction(title: "OK", style: .default))
            present(alert, animated: true)
        }
    }
}

@MainActor
private final class KeyQrExportViewController: UIViewController {
    private let exported: MobileKeyTransferExport
    private let qrView = KeyQrView()
    private let pageControl = UIPageControl()
    private let progressLabel = UILabel()
    private let shield = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
    private var index = 0

    init(exported: MobileKeyTransferExport) {
        self.exported = exported
        super.init(nibName: nil, bundle: nil)
        title = exported.key.kind == .private ? "Private GPG Key" : "Public GPG Key"
        navigationItem.largeTitleDisplayMode = .never
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        qrView.translatesAutoresizingMaskIntoConstraints = false
        qrView.layer.cornerRadius = 16
        qrView.clipsToBounds = true

        let identity = UILabel()
        identity.text = exported.key.title
        identity.font = .preferredFont(forTextStyle: .headline)
        identity.adjustsFontForContentSizeCategory = true
        identity.numberOfLines = 0
        identity.textAlignment = .center

        let fingerprint = UILabel()
        fingerprint.text = exported.key.detail
        fingerprint.font = .preferredFont(forTextStyle: .footnote)
        fingerprint.adjustsFontForContentSizeCategory = true
        fingerprint.numberOfLines = 0
        fingerprint.textAlignment = .center
        fingerprint.textColor = .secondaryLabel

        progressLabel.font = .preferredFont(forTextStyle: .callout)
        progressLabel.adjustsFontForContentSizeCategory = true
        progressLabel.textAlignment = .center
        pageControl.numberOfPages = exported.frames.count
        pageControl.addTarget(self, action: #selector(pageChanged), for: .valueChanged)
        pageControl.isHidden = exported.frames.count == 1

        let stack = UIStackView(arrangedSubviews: [identity, fingerprint, qrView, progressLabel, pageControl])
        stack.axis = .vertical
        stack.alignment = .fill
        stack.spacing = 14
        stack.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stack)

        let shieldLabel = UILabel()
        shieldLabel.text = "Private key hidden while IronStorage is inactive or the screen is captured"
        shieldLabel.font = .preferredFont(forTextStyle: .headline)
        shieldLabel.adjustsFontForContentSizeCategory = true
        shieldLabel.numberOfLines = 0
        shieldLabel.textAlignment = .center
        shieldLabel.translatesAutoresizingMaskIntoConstraints = false
        shield.contentView.addSubview(shieldLabel)
        shield.translatesAutoresizingMaskIntoConstraints = false
        shield.isHidden = true
        shield.accessibilityViewIsModal = true
        view.addSubview(shield)

        NSLayoutConstraint.activate([
            stack.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 24),
            stack.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -24),
            stack.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
            qrView.widthAnchor.constraint(equalTo: qrView.heightAnchor),
            qrView.widthAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.widthAnchor, constant: -48),
            shield.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            shield.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            shield.topAnchor.constraint(equalTo: view.topAnchor),
            shield.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            shieldLabel.leadingAnchor.constraint(equalTo: shield.contentView.leadingAnchor, constant: 32),
            shieldLabel.trailingAnchor.constraint(equalTo: shield.contentView.trailingAnchor, constant: -32),
            shieldLabel.centerYAnchor.constraint(equalTo: shield.contentView.centerYAnchor),
        ])

        let left = UISwipeGestureRecognizer(target: self, action: #selector(swipedLeft))
        left.direction = .left
        let right = UISwipeGestureRecognizer(target: self, action: #selector(swipedRight))
        right.direction = .right
        qrView.addGestureRecognizer(left)
        qrView.addGestureRecognizer(right)
        qrView.isUserInteractionEnabled = true

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appWillResignActive),
            name: UIApplication.willResignActiveNotification,
            object: nil
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appDidBecomeActive),
            name: UIApplication.didBecomeActiveNotification,
            object: nil
        )
        registerForTraitChanges([UITraitSceneCaptureState.self]) {
            (controller: KeyQrExportViewController, _: UITraitCollection) in
            controller.updateCaptureShield()
        }
        showFrame(0)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        updateCaptureShield()
    }

    @objc private func pageChanged() {
        showFrame(pageControl.currentPage)
    }

    @objc private func swipedLeft() {
        showFrame(min(index + 1, exported.frames.count - 1))
    }

    @objc private func swipedRight() {
        showFrame(max(index - 1, 0))
    }

    private func showFrame(_ index: Int) {
        self.index = index
        pageControl.currentPage = index
        qrView.transferFrame = exported.frames[index]
        progressLabel.text = exported.frames.count == 1
            ? "Scan this QR code"
            : "QR code \(index + 1) of \(exported.frames.count)"
        qrView.accessibilityLabel = progressLabel.text
        UIAccessibility.post(notification: .pageScrolled, argument: progressLabel.text)
    }

    @objc private func appWillResignActive() {
        if exported.key.kind == .private {
            shield.isHidden = false
        }
    }

    @objc private func appDidBecomeActive() {
        updateCaptureShield()
    }

    private func updateCaptureShield() {
        shield.isHidden = exported.key.kind != .private || traitCollection.sceneCaptureState != .active
    }
}

private final class KeyQrView: UIView {
    var transferFrame: MobileKeyTransferFrame? {
        didSet { setNeedsDisplay() }
    }

    override func draw(_ rect: CGRect) {
        UIColor.white.setFill()
        UIRectFill(rect)
        guard let transferFrame, transferFrame.width > 0 else { return }
        let width = Int(transferFrame.width)
        let padded = width + 8
        let scale = floor(min(rect.width, rect.height) / CGFloat(padded))
        guard scale >= 1 else { return }
        let symbolSize = CGFloat(padded) * scale
        let origin = CGPoint(
            x: rect.midX - symbolSize / 2 + 4 * scale,
            y: rect.midY - symbolSize / 2 + 4 * scale
        )
        guard let context = UIGraphicsGetCurrentContext() else { return }
        context.setFillColor(UIColor.black.cgColor)
        context.interpolationQuality = .none
        for y in 0..<width {
            for x in 0..<width where transferFrame.modules[y * width + x] != 0 {
                context.fill(CGRect(
                    x: origin.x + CGFloat(x) * scale,
                    y: origin.y + CGFloat(y) * scale,
                    width: scale,
                    height: scale
                ))
            }
        }
    }
}

@MainActor
private final class TotpDiscoveryView: UIView {
    private let spinner = UIActivityIndicatorView(style: .medium)
    private let titleLabel = UILabel()
    private let detailLabel = UILabel()
    private let progressView = UIProgressView(progressViewStyle: .default)

    init(cancel: @escaping () -> Void) {
        super.init(frame: .zero)
        titleLabel.font = .preferredFont(forTextStyle: .headline)
        titleLabel.adjustsFontForContentSizeCategory = true
        titleLabel.textAlignment = .center
        detailLabel.font = .preferredFont(forTextStyle: .subheadline)
        detailLabel.adjustsFontForContentSizeCategory = true
        detailLabel.textColor = .secondaryLabel
        detailLabel.textAlignment = .center
        detailLabel.numberOfLines = 0
        progressView.accessibilityLabel = "TOTP discovery progress"
        let cancelButton = UIButton(type: .system, primaryAction: UIAction(title: "Cancel") { _ in
            cancel()
        })
        let progressRow = UIStackView(arrangedSubviews: [spinner, progressView])
        progressRow.alignment = .center
        progressRow.spacing = 12
        let stack = UIStackView(arrangedSubviews: [titleLabel, detailLabel, progressRow, cancelButton])
        stack.axis = .vertical
        stack.alignment = .fill
        stack.spacing = 12
        stack.translatesAutoresizingMaskIntoConstraints = false
        addSubview(stack)
        NSLayoutConstraint.activate([
            stack.centerXAnchor.constraint(equalTo: centerXAnchor),
            stack.topAnchor.constraint(equalTo: topAnchor, constant: 24),
            stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -24),
            stack.leadingAnchor.constraint(greaterThanOrEqualTo: readableContentGuide.leadingAnchor),
            stack.trailingAnchor.constraint(lessThanOrEqualTo: readableContentGuide.trailingAnchor),
            progressView.widthAnchor.constraint(greaterThanOrEqualToConstant: 180),
        ])
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    func apply(_ progress: MobileTotpDiscoveryProgress) {
        titleLabel.text = switch progress.phase {
        case .preparing: "Preparing TOTP Discovery"
        case .inspecting: "Discovering TOTP Entries"
        case .saving: "Saving Protected Cache"
        case .complete: "TOTP Discovery Complete"
        case .cancelled: "Cancelling TOTP Discovery"
        }
        if progress.total == 0 {
            spinner.startAnimating()
            progressView.isHidden = true
            detailLabel.text = "Reading the password-store inventory."
        } else {
            spinner.stopAnimating()
            progressView.isHidden = false
            progressView.progress = Float(progress.inspected) / Float(progress.total)
            progressView.accessibilityValue = "\(progress.inspected) of \(progress.total)"
            detailLabel.text =
                "\(progress.inspected) of \(progress.total) inspected • "
                + "\(progress.cacheHits) cached • \(progress.matches) TOTP"
                + (progress.unavailable == 0 ? "" : " • \(progress.unavailable) unavailable")
        }
    }
}

@MainActor
private final class TotpListViewController: UITableViewController, MobileTabRoot,
    UISearchResultsUpdating
{
    fileprivate let shellTab = MobileTab.totp
    private let authentication: MobileAuthentication?
    private let searchController = UISearchController(searchResultsController: nil)
    private var shellPage: MobilePage
    private var page: MobileTotpPage?
    private var unfilteredPage: MobileTotpPage?
    private var loadTask: Task<Void, Never>?
    private var searchTask: Task<Void, Never>?
    private var unlockTask: Task<Void, Never>?
    private var shellTask: Task<Void, Never>?
    private var progressTask: Task<Void, Never>?
    private var operation: MobileTotpOperation?
    private var loadGeneration = 0
    private var searchGeneration = 0
    private var refreshAfterUnlock = false

    init(shellPage: MobilePage, authentication: MobileAuthentication?) {
        self.shellPage = shellPage
        self.authentication = authentication
        super.init(style: .insetGrouped)
        title = shellPage.title
        navigationItem.largeTitleDisplayMode = .always
        searchController.searchResultsUpdater = self
        searchController.obscuresBackgroundDuringPresentation = false
        searchController.searchBar.placeholder = "TOTP entry names and folders"
        searchController.searchBar.autocapitalizationType = .none
        searchController.searchBar.autocorrectionType = .no
        searchController.searchBar.spellCheckingType = .no
        navigationItem.searchController = searchController
        navigationItem.hidesSearchBarWhenScrolling = false
        definesPresentationContext = true
        refreshControl = UIRefreshControl()
        refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    deinit {
        operation?.cancel()
        loadTask?.cancel()
        searchTask?.cancel()
        unlockTask?.cancel()
        shellTask?.cancel()
        progressTask?.cancel()
        NotificationCenter.default.removeObserver(self)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(authenticationDidChange),
            name: .ironStorageAuthenticationDidChange,
            object: nil
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(localStoreDidChange),
            name: .ironStorageLocalStoreDidChange,
            object: nil
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(localStoreDidChange),
            name: .ironStorageWatchSnapshotDidChange,
            object: nil
        )
        refreshState()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        reloadShell()
    }

    func updateSearchResults(for searchController: UISearchController) {
        search(searchController.searchBar.text ?? "")
    }

    override func numberOfSections(in tableView: UITableView) -> Int { 1 }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        page?.rows.count ?? 0
    }

    override func tableView(
        _ tableView: UITableView,
        titleForFooterInSection section: Int
    ) -> String? {
        guard let page else { return nil }
        let unavailable = page.unavailableEntries == 0
            ? ""
            : " \(page.unavailableEntries) entries could not be inspected with the active key."
        let cache = page.cacheNotice.map { " \($0)" } ?? ""
        return page.watch.detail + unavailable + cache
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        let row = page?.rows[indexPath.row]
        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
        var content = cell.defaultContentConfiguration()
        content.image = UIImage(systemName: "timer")
        content.text = row?.title
        content.secondaryText = row?.detail
        content.secondaryTextProperties.numberOfLines = 2
        if row?.sharedWithWatch == true {
            content.secondaryText = [row?.detail, "Apple Watch selected"]
                .compactMap { $0 }
                .joined(separator: " • ")
        }
        cell.contentConfiguration = content
        cell.accessoryType = .disclosureIndicator
        cell.accessibilityLabel = [row?.title, row?.detail].compactMap { $0 }.joined(separator: ", ")
        cell.accessibilityValue = row?.sharedWithWatch == true ? "Selected for Apple Watch" : nil
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        guard let row = page?.rows[indexPath.row] else { return }
        guard (try? authentication?.state().unlocked) == true else {
            unlock(passphrase: nil, row: row)
            return
        }
        open(row)
    }

    private func open(_ row: MobileTotpRow) {
        guard let authentication else { return }
        loadTask?.cancel()
        loadTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    try authentication.touchUserActivity()
                    return Result<MobileTotpDetail, AuthenticationFailure>.success(
                        try authentication.totpDetail(
                            path: row.path,
                            unixSeconds: currentUnixSeconds()
                        )
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self else { return }
            switch result {
            case let .success(detail):
                navigationController?.pushViewController(
                    TotpDetailViewController(authentication: authentication, detail: detail),
                    animated: true
                )
            case let .failure(failure):
                handle(failure)
            }
        }
    }

    @objc private func refreshRequested() {
        if (try? authentication?.state().unlocked) == true {
            loadPage()
        } else {
            refreshAfterUnlock = true
            refreshControl?.endRefreshing()
            unlockRequested()
        }
    }

    @objc private func authenticationDidChange() {
        if (try? authentication?.state().unlocked) != true {
            operation?.cancel()
        }
        refreshState()
    }

    @objc private func localStoreDidChange() {
        operation?.cancel()
        loadTask?.cancel()
        searchTask?.cancel()
        progressTask?.cancel()
        loadGeneration += 1
        searchGeneration += 1
        operation = nil
        progressTask = nil
        tableView.tableHeaderView = nil
        page = nil
        unfilteredPage = nil
        loadCachedPage()
    }

    @objc private func unlockRequested() {
        unlock(passphrase: nil, row: nil)
    }

    @objc private func lockRequested() {
        guard let authentication else { return }
        do {
            try authentication.manualLock()
            NotificationCenter.default.post(
                name: .ironStorageAuthenticationDidChange,
                object: authentication
            )
        } catch let error as MobileAuthenticationFfiError {
            presentAuthenticationFailure(AuthenticationFailure(error))
        } catch {
            presentAuthenticationFailure(.unexpected)
        }
    }

    private func refreshState(force: Bool = false) {
        guard shellPage.state == .ready else {
            searchTask?.cancel()
            searchGeneration += 1
            page = nil
            unfilteredPage = nil
            showUnavailable(
                title: shellPage.stateTitle,
                detail: shellPage.stateDetail,
                image: shellPage.systemImage
            )
            return
        }
        if force, (try? authentication?.state().unlocked) == true {
            loadPage()
        } else if page == nil, loadTask == nil {
            loadCachedPage()
        } else if page != nil {
            configureLockButton()
        }
    }

    private func reloadShell() {
        shellTask?.cancel()
        shellTask = Task { [weak self] in
            let shell = await Task.detached(priority: .userInitiated) { mobileShell() }.value
            guard
                !Task.isCancelled,
                let self,
                let page = shell.pages.first(where: { $0.tab == .totp })
            else { return }
            shellPage = page
            refreshState()
        }
    }

    private func unlock(passphrase: String?, row: MobileTotpRow?) {
        guard let authentication else {
            presentAuthenticationFailure(.unavailable)
            return
        }
        unlockTask?.cancel()
        showLoading("Unlocking TOTP")
        unlockTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    let state = if let row {
                        try authentication.unlockEntry(path: row.path, passphrase: passphrase)
                    } else {
                        try authentication.unlockTotp(passphrase: passphrase)
                    }
                    return Result<MobileAuthenticationState, AuthenticationFailure>.success(
                        state
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self else { return }
            contentUnavailableConfiguration = nil
            switch result {
            case .success:
                NotificationCenter.default.post(
                    name: .ironStorageAuthenticationDidChange,
                    object: authentication
                )
                if refreshAfterUnlock || page == nil {
                    refreshAfterUnlock = false
                    loadPage()
                } else if let row {
                    open(row)
                } else {
                    configureLockButton()
                }
                UIAccessibility.post(notification: .announcement, argument: "TOTP unlocked")
            case let .failure(failure)
                where passphrase == nil
                    && (failure.kind == .passphraseRequired
                        || failure.kind == .biometryUnavailable):
                promptForPassphrase(message: failure.detail, row: row)
            case let .failure(failure):
                if failure.kind != .cancelled { handle(failure) }
            }
        }
    }

    private func promptForPassphrase(message: String, row: MobileTotpRow?) {
        let alert = UIAlertController(
            title: "GPG Key Passphrase",
            message: message,
            preferredStyle: .alert
        )
        alert.addTextField { field in
            field.isSecureTextEntry = true
            field.textContentType = .password
            field.placeholder = "Passphrase"
            field.returnKeyType = .go
        }
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { [weak self] _ in
            self?.refreshState()
        })
        alert.addAction(UIAlertAction(title: "Unlock", style: .default) { [weak self, weak alert] _ in
            guard let value = alert?.textFields?.first?.text, !value.isEmpty else { return }
            alert?.textFields?.first?.text = nil
            self?.unlock(passphrase: value, row: row)
        })
        present(alert, animated: true)
    }

    private func loadPage() {
        guard let authentication else {
            presentAuthenticationFailure(.unavailable)
            return
        }
        operation?.cancel()
        loadTask?.cancel()
        progressTask?.cancel()
        loadGeneration += 1
        let current = loadGeneration
        let operation = mobileTotpOperation()
        self.operation = operation
        navigationItem.rightBarButtonItems = nil
        let discoveryView = TotpDiscoveryView { [weak self] in
            self?.cancelDiscovery()
        }
        apply(operation.progress(), to: discoveryView)
        tableView.tableHeaderView = discoveryView
        contentUnavailableConfiguration = nil
        progressTask = Task { [weak self] in
            while !Task.isCancelled {
                do {
                    try await Task.sleep(for: .milliseconds(150))
                } catch {
                    return
                }
                guard !Task.isCancelled, let self, current == loadGeneration else { return }
                apply(operation.progress(), to: discoveryView)
            }
        }
        loadTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    return Result<MobileTotpPage, AuthenticationFailure>.success(
                        try authentication.totpPage(operation: operation)
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self, current == loadGeneration else { return }
            progressTask?.cancel()
            progressTask = nil
            self.operation = nil
            loadTask = nil
            tableView.tableHeaderView = nil
            refreshControl?.endRefreshing()
            configureLockButton()
            switch result {
            case let .success(page):
                apply(page)
            case let .failure(failure):
                if failure.kind == .cancelled {
                    loadCachedPage()
                } else {
                    handle(failure)
                }
            }
        }
    }

    private func loadCachedPage() {
        guard let authentication else {
            presentAuthenticationFailure(.unavailable)
            return
        }
        loadTask?.cancel()
        loadTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    return Result<MobileTotpPage?, AuthenticationFailure>.success(
                        try authentication.cachedTotpPage()
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self else { return }
            loadTask = nil
            switch result {
            case let .success(.some(page)):
                apply(page)
            case .success(.none):
                if (try? authentication.state().unlocked) == true {
                    loadPage()
                } else {
                    showLocked()
                }
            case let .failure(failure):
                handle(failure)
            }
        }
    }

    private func apply(_ page: MobileTotpPage, remember: Bool = true) {
        if remember {
            unfilteredPage = page
            let query = searchController.searchBar.text ?? ""
            if !query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
                search(query)
                return
            }
        }
        self.page = page
        configureLockButton()
        tableView.reloadData()
        if page.rows.isEmpty {
            let query = searchController.searchBar.text ?? ""
            let searching = !query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
            showUnavailable(
                title: searching ? "No Results" : "No TOTP Codes",
                detail: searching
                    ? "No cached TOTP entries match “\(query)”."
                    : "No valid time-based OTP entries were found in the password store.",
                image: searching ? "magnifyingglass" : "timer"
            )
        } else {
            contentUnavailableConfiguration = nil
        }
    }

    private func search(_ query: String) {
        searchGeneration += 1
        let generation = searchGeneration
        searchTask?.cancel()
        guard !query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
            if let unfilteredPage { apply(unfilteredPage, remember: false) }
            return
        }
        guard let authentication else { return }
        searchTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    return Result<MobileTotpPage?, AuthenticationFailure>.success(
                        try authentication.searchCachedTotpPage(query: query)
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self, generation == searchGeneration else { return }
            switch result {
            case let .success(.some(page)):
                apply(page, remember: false)
            case .success(.none):
                break
            case let .failure(failure):
                handle(failure)
            }
        }
    }

    private func configureLockButton() {
        let unlocked = (try? authentication?.state().unlocked) == true
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            image: UIImage(systemName: unlocked ? "lock.open.fill" : "lock.fill"),
            style: .plain,
            target: self,
            action: unlocked ? #selector(lockRequested) : #selector(unlockRequested)
        )
        navigationItem.rightBarButtonItem?.accessibilityLabel =
            unlocked ? "Lock IronStorage" : "Unlock IronStorage"
    }

    private func showLocked() {
        page = nil
        unfilteredPage = nil
        tableView.reloadData()
        navigationItem.rightBarButtonItem = nil
        var configuration = UIContentUnavailableConfiguration.empty()
        configuration.image = UIImage(systemName: "lock.fill")
        configuration.text = "TOTP Is Locked"
        configuration.secondaryText =
            "Unlock once to discover time-based one-time-password entries."
        configuration.button = .filled()
        configuration.button.title = "Unlock"
        configuration.buttonProperties.primaryAction = UIAction { [weak self] _ in
            self?.unlockRequested()
        }
        contentUnavailableConfiguration = configuration
        refreshControl?.endRefreshing()
    }

    private func cancelDiscovery() {
        operation?.cancel()
    }

    private func apply(_ progress: MobileTotpDiscoveryProgress, to view: TotpDiscoveryView) {
        view.apply(progress)
        let width = tableView.bounds.width
        let height = view.systemLayoutSizeFitting(
            CGSize(width: width, height: UIView.layoutFittingCompressedSize.height),
            withHorizontalFittingPriority: .required,
            verticalFittingPriority: .fittingSizeLevel
        ).height
        view.frame = CGRect(x: 0, y: 0, width: width, height: height)
        if tableView.tableHeaderView === view {
            tableView.tableHeaderView = view
        }
    }

    private func handle(_ failure: AuthenticationFailure) {
        if failure.kind == .expired {
            NotificationCenter.default.post(
                name: .ironStorageAuthenticationDidChange,
                object: authentication
            )
            refreshState()
        } else {
            showUnavailable(title: failure.title, detail: failure.detail, image: "exclamationmark.triangle")
        }
        presentAuthenticationFailure(failure)
    }

    private func showLoading(_ title: String) {
        tableView.backgroundView = nil
        var configuration = UIContentUnavailableConfiguration.loading()
        configuration.text = title
        configuration.secondaryText = "Reading OTP metadata in secure storage."
        contentUnavailableConfiguration = configuration
    }

    private func showUnavailable(title: String, detail: String, image: String) {
        tableView.backgroundView = nil
        var configuration = UIContentUnavailableConfiguration.empty()
        configuration.image = UIImage(systemName: image)
        configuration.text = title
        configuration.secondaryText = detail
        contentUnavailableConfiguration = configuration
        refreshControl?.endRefreshing()
    }
}

@MainActor
private final class TotpCodeView: UIView {
    private let issuerLabel = UILabel()
    private let accountLabel = UILabel()
    private let codeLabel = UILabel()
    private let countdownLabel = UILabel()
    private let progress = UIProgressView(progressViewStyle: .default)
    var copyRequested: (() -> Void)?

    override init(frame: CGRect) {
        super.init(frame: frame)
        issuerLabel.font = .preferredFont(forTextStyle: .title2)
        issuerLabel.adjustsFontForContentSizeCategory = true
        issuerLabel.textAlignment = .center
        accountLabel.font = .preferredFont(forTextStyle: .body)
        accountLabel.textColor = .secondaryLabel
        accountLabel.adjustsFontForContentSizeCategory = true
        accountLabel.textAlignment = .center
        accountLabel.numberOfLines = 0
        codeLabel.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(
            for: .monospacedDigitSystemFont(ofSize: 48, weight: .semibold)
        )
        codeLabel.adjustsFontForContentSizeCategory = true
        codeLabel.textAlignment = .center
        codeLabel.minimumScaleFactor = 0.55
        codeLabel.adjustsFontSizeToFitWidth = true
        codeLabel.layer.cornerRadius = 12
        codeLabel.layer.masksToBounds = true
        codeLabel.isAccessibilityElement = true
        codeLabel.isUserInteractionEnabled = true
        codeLabel.accessibilityTraits.insert(.button)
        codeLabel.accessibilityHint = "Copies the current code"
        countdownLabel.font = .preferredFont(forTextStyle: .footnote)
        countdownLabel.textColor = .secondaryLabel
        countdownLabel.adjustsFontForContentSizeCategory = true
        countdownLabel.textAlignment = .center

        let stack = UIStackView(
            arrangedSubviews: [issuerLabel, accountLabel, codeLabel, progress, countdownLabel]
        )
        stack.axis = .vertical
        stack.spacing = 12
        stack.translatesAutoresizingMaskIntoConstraints = false
        addSubview(stack)
        NSLayoutConstraint.activate([
            stack.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
            stack.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
            stack.topAnchor.constraint(equalTo: topAnchor, constant: 20),
            stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20),
        ])
        codeLabel.addGestureRecognizer(
            UITapGestureRecognizer(target: self, action: #selector(copyCode))
        )
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    func apply(_ detail: MobileTotpDetail) {
        issuerLabel.text = detail.issuer ?? "TOTP"
        accountLabel.text = detail.account
        codeLabel.text = groupedCode(detail.code)
        codeLabel.accessibilityLabel =
            "Current code \(detail.code.map(String.init).joined(separator: " "))"
        updateCountdown(detail)
    }

    func updateCountdown(_ detail: MobileTotpDetail) {
        let remaining = detail.validUntil.saturatingSubtracting(currentUnixSeconds())
        let fraction = detail.period == 0 ? 0 : Float(remaining) / Float(detail.period)
        progress.setProgress(min(max(fraction, 0), 1), animated: true)
        countdownLabel.text = "\(remaining) seconds remaining"
        progress.accessibilityLabel = "Code validity"
        progress.accessibilityValue = countdownLabel.text
    }

    func clear() {
        issuerLabel.text = nil
        accountLabel.text = nil
        codeLabel.text = nil
        countdownLabel.text = nil
        progress.progress = 0
    }

    func flashCopied() {
        UINotificationFeedbackGenerator().notificationOccurred(.success)
        UIAccessibility.post(notification: .announcement, argument: "TOTP code copied")
        UIView.animate(withDuration: 0.12, animations: {
            self.codeLabel.backgroundColor = .systemGreen.withAlphaComponent(0.28)
        }) { _ in
            UIView.animate(withDuration: 0.55) { self.codeLabel.backgroundColor = .clear }
        }
    }

    @objc private func copyCode() {
        copyRequested?()
    }
}

@MainActor
private final class TotpDetailViewController: UITableViewController {
    private let authentication: MobileAuthentication
    private var detail: MobileTotpDetail
    private let codeView = TotpCodeView()
    private let watchSwitch = UISwitch()
    private var timerTask: Task<Void, Never>?
    private var loadTask: Task<Void, Never>?
    private var clipboardTask: Task<Void, Never>?
    private var copiedValue: String?

    init(authentication: MobileAuthentication, detail: MobileTotpDetail) {
        self.authentication = authentication
        self.detail = detail
        super.init(style: .insetGrouped)
        navigationItem.largeTitleDisplayMode = .never
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    deinit {
        timerTask?.cancel()
        loadTask?.cancel()
        clipboardTask?.cancel()
        NotificationCenter.default.removeObserver(self)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        configureHeader()
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            image: UIImage(systemName: "doc.on.doc"),
            style: .plain,
            target: self,
            action: #selector(copyRequested)
        )
        navigationItem.rightBarButtonItem?.accessibilityLabel = "Copy current TOTP code"
        watchSwitch.addTarget(self, action: #selector(watchSwitchChanged(_:)), for: .valueChanged)
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(authenticationDidChange),
            name: .ironStorageAuthenticationDidChange,
            object: nil
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(entryDidChange),
            name: .ironStorageLocalStoreDidChange,
            object: nil
        )
        apply(detail)
        startTimer()
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        guard let header = tableView.tableHeaderView else { return }
        let height = header.systemLayoutSizeFitting(
            CGSize(width: tableView.bounds.width, height: 0),
            withHorizontalFittingPriority: .required,
            verticalFittingPriority: .fittingSizeLevel
        ).height
        guard abs(header.frame.height - height) > 0.5 else { return }
        header.frame.size.height = height
        tableView.tableHeaderView = header
    }

    override func numberOfSections(in tableView: UITableView) -> Int { 1 }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 }

    override func tableView(
        _ tableView: UITableView,
        titleForHeaderInSection section: Int
    ) -> String? {
        "Apple Watch"
    }

    override func tableView(
        _ tableView: UITableView,
        titleForFooterInSection section: Int
    ) -> String? {
        detail.watch.detail
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
        var content = cell.defaultContentConfiguration()
        content.text = "Share with Apple Watch"
        content.secondaryText = "Time-based codes only"
        cell.contentConfiguration = content
        cell.accessoryView = watchSwitch
        cell.selectionStyle = .none
        return cell
    }

    @objc private func authenticationDidChange() {
        guard (try? authentication.state().unlocked) == true else {
            lockDetail()
            return
        }
    }

    @objc private func entryDidChange() {
        refreshCode()
    }

    @objc private func copyRequested() {
        loadTask?.cancel()
        let authentication = authentication
        let path = detail.path
        loadTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    try authentication.touchUserActivity()
                    return Result<MobileEntryCopy, AuthenticationFailure>.success(
                        try authentication.copyTotpCode(
                            path: path,
                            unixSeconds: currentUnixSeconds()
                        )
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self else { return }
            loadTask = nil
            switch result {
            case let .success(copy):
                UIPasteboard.general.string = copy.value
                copiedValue = copy.value
                flashCopied()
                clipboardTask?.cancel()
                clipboardTask = Task { @MainActor in
                    do {
                        try await Task.sleep(for: .seconds(copy.timeoutSeconds))
                    } catch {
                        return
                    }
                    if UIPasteboard.general.string == copy.value {
                        UIPasteboard.general.items = []
                    }
                    self.copiedValue = nil
                }
            case let .failure(failure):
                handle(failure)
            }
        }
    }

    @objc private func watchSwitchChanged(_ sender: UISwitch) {
        let requested = sender.isOn
        sender.isEnabled = false
        loadTask?.cancel()
        let authentication = authentication
        let path = detail.path
        loadTask = Task { [weak self, weak sender] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    try authentication.touchUserActivity()
                    return Result<MobileTotpDetail, AuthenticationFailure>.success(
                        try authentication.setTotpWatchShared(
                            path: path,
                            shared: requested,
                            unixSeconds: currentUnixSeconds()
                        )
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self else { return }
            loadTask = nil
            sender?.isEnabled = true
            switch result {
            case let .success(detail):
                apply(detail)
                UINotificationFeedbackGenerator().notificationOccurred(.success)
                NotificationCenter.default.post(
                    name: .ironStorageWatchSnapshotDidChange,
                    object: nil
                )
            case let .failure(failure):
                sender?.setOn(!requested, animated: true)
                handle(failure)
            }
        }
    }

    private func configureHeader() {
        codeView.frame = CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 1)
        codeView.copyRequested = { [weak self] in self?.copyRequested() }
        tableView.tableHeaderView = codeView
    }

    private func apply(_ detail: MobileTotpDetail) {
        self.detail = detail
        title = detail.issuer ?? detail.account
        codeView.apply(detail)
        watchSwitch.setOn(detail.sharedWithWatch, animated: false)
        tableView.reloadData()
        updateCountdown()
    }

    private func startTimer() {
        timerTask?.cancel()
        timerTask = Task { [weak self] in
            while !Task.isCancelled {
                do {
                    try await Task.sleep(for: .seconds(1))
                } catch {
                    return
                }
                guard let self else { return }
                if currentUnixSeconds() >= detail.validUntil {
                    refreshCode()
                } else {
                    updateCountdown()
                }
            }
        }
    }

    private func updateCountdown() {
        codeView.updateCountdown(detail)
    }

    private func refreshCode() {
        guard loadTask == nil else { return }
        let authentication = authentication
        let path = detail.path
        loadTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    return Result<MobileTotpDetail, AuthenticationFailure>.success(
                        try authentication.totpDetail(
                            path: path,
                            unixSeconds: currentUnixSeconds()
                        )
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard let self else { return }
            loadTask = nil
            guard !Task.isCancelled else { return }
            switch result {
            case let .success(detail): apply(detail)
            case let .failure(failure): showDetailUnavailable(failure)
            }
        }
    }

    private func flashCopied() {
        codeView.flashCopied()
    }

    private func handle(_ failure: AuthenticationFailure) {
        if failure.kind == .expired {
            lockDetail()
        }
        presentAuthenticationFailure(failure)
    }

    private func showDetailUnavailable(_ failure: AuthenticationFailure) {
        if failure.kind == .expired {
            lockDetail()
        } else {
            timerTask?.cancel()
            detail.code = ""
            codeView.clear()
            navigationItem.rightBarButtonItem = nil
            watchSwitch.isEnabled = false
            var configuration = UIContentUnavailableConfiguration.empty()
            configuration.image = UIImage(systemName: "exclamationmark.triangle")
            configuration.text = failure.title
            configuration.secondaryText = failure.detail
            configuration.button = .plain()
            configuration.button.title = "Back to TOTP"
            configuration.buttonProperties.primaryAction = UIAction { [weak self] _ in
                self?.navigationController?.popViewController(animated: true)
            }
            contentUnavailableConfiguration = configuration
        }
        presentAuthenticationFailure(failure)
    }

    private func lockDetail() {
        timerTask?.cancel()
        loadTask?.cancel()
        clipboardTask?.cancel()
        detail.code = ""
        codeView.clear()
        if UIPasteboard.general.string == copiedValue {
            UIPasteboard.general.items = []
        }
        copiedValue = nil
        navigationItem.rightBarButtonItem = nil
        watchSwitch.isEnabled = false
        var configuration = UIContentUnavailableConfiguration.empty()
        configuration.image = UIImage(systemName: "lock.fill")
        configuration.text = "TOTP Is Locked"
        configuration.secondaryText = "The code was removed when IronStorage locked."
        configuration.button = .plain()
        configuration.button.title = "Back to TOTP"
        configuration.buttonProperties.primaryAction = UIAction { [weak self] _ in
            self?.navigationController?.popViewController(animated: true)
        }
        contentUnavailableConfiguration = configuration
        UIAccessibility.post(notification: .announcement, argument: "TOTP locked")
    }
}

private func currentUnixSeconds() -> UInt64 {
    UInt64(max(Date().timeIntervalSince1970, 0))
}

private func groupedCode(_ code: String) -> String {
    let midpoint = code.index(code.startIndex, offsetBy: code.count / 2)
    return String(code[..<midpoint]) + " " + String(code[midpoint...])
}

private extension UInt64 {
    func saturatingSubtracting(_ value: UInt64) -> UInt64 {
        self > value ? self - value : 0
    }
}

@MainActor
private final class PasswordMutationCoordinator {
    private weak var presenter: UIViewController?
    private let authentication: MobileAuthentication?
    private var operationTask: Task<Void, Never>?
    private var working = false
    private var contextualCompletion: ((Bool) -> Void)?

    init(presenter: UIViewController, authentication: MobileAuthentication?) {
        self.presenter = presenter
        self.authentication = authentication
    }

    deinit {
        operationTask?.cancel()
    }

    func accessoryView(for row: MobilePasswordRow) -> UIView {
        let button = UIButton(type: .system)
        button.frame = CGRect(x: 0, y: 0, width: 44, height: 44)
        button.configuration = .plain()
        button.configuration?.image = UIImage(systemName: "ellipsis.circle")
        button.configuration?.buttonSize = .medium
        button.menu = menu(for: row)
        button.showsMenuAsPrimaryAction = true
        button.accessibilityLabel = "Actions for \(row.title)"
        return button
    }

    func accessibilityActions(for row: MobilePasswordRow) -> [UIAccessibilityCustomAction] {
        [
            accessibilityAction("Move", image: "folder", action: .move, row: row),
            accessibilityAction("Copy", image: "doc.on.doc", action: .copy, row: row),
            accessibilityAction("Delete", image: "trash", action: .delete, row: row),
        ]
    }

    func swipeConfiguration(for row: MobilePasswordRow) -> UISwipeActionsConfiguration {
        let delete = UIContextualAction(style: .destructive, title: "Delete") {
            [weak self] _, _, completion in
            self?.begin(.delete, row: row, completion: completion)
        }
        delete.image = UIImage(systemName: "trash")

        let copy = UIContextualAction(style: .normal, title: "Copy") {
            [weak self] _, _, completion in
            self?.begin(.copy, row: row, completion: completion)
        }
        copy.image = UIImage(systemName: "doc.on.doc")
        copy.backgroundColor = .systemBlue

        let move = UIContextualAction(style: .normal, title: "Move") {
            [weak self] _, _, completion in
            self?.begin(.move, row: row, completion: completion)
        }
        move.image = UIImage(systemName: "folder")
        move.backgroundColor = .systemOrange

        let configuration = UISwipeActionsConfiguration(actions: [delete, copy, move])
        configuration.performsFirstActionWithFullSwipe = true
        return configuration
    }

    private func menu(for row: MobilePasswordRow) -> UIMenu {
        UIMenu(children: [
            UIAction(title: "Move", image: UIImage(systemName: "folder")) { [weak self] _ in
                self?.begin(.move, row: row)
            },
            UIAction(title: "Copy", image: UIImage(systemName: "doc.on.doc")) { [weak self] _ in
                self?.begin(.copy, row: row)
            },
            UIAction(
                title: "Delete",
                image: UIImage(systemName: "trash"),
                attributes: .destructive
            ) { [weak self] _ in
                self?.begin(.delete, row: row)
            },
        ])
    }

    private func accessibilityAction(
        _ title: String,
        image: String,
        action: MobileMutationAction,
        row: MobilePasswordRow
    ) -> UIAccessibilityCustomAction {
        UIAccessibilityCustomAction(
            name: title,
            image: UIImage(systemName: image)
        ) { [weak self] _ in
            self?.begin(action, row: row)
            return true
        }
    }

    private func begin(
        _ action: MobileMutationAction,
        row: MobilePasswordRow,
        completion: ((Bool) -> Void)? = nil
    ) {
        guard !working else {
            completion?(false)
            return
        }
        guard let authentication else {
            completion?(false)
            presenter?.presentAuthenticationFailure(.unavailable)
            return
        }
        working = true
        contextualCompletion = completion
        operationTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    return Result<MobileMutationPlan, AuthenticationFailure>.success(
                        try authentication.prepareEntryMutation(path: row.path, action: action)
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self else { return }
            operationTask = nil
            switch result {
            case let .success(plan):
                present(plan)
            case let .failure(failure):
                fail(failure)
            }
        }
    }

    private func present(_ plan: MobileMutationPlan) {
        switch plan.action {
        case .delete:
            confirmDelete(plan)
        case .move, .copy:
            guard !plan.destinations.isEmpty else {
                fail(AuthenticationFailure(
                    kind: .entry,
                    title: "No Destination Available",
                    detail: "Create another password folder before moving or copying this entry."
                ))
                return
            }
            let destinations = PasswordDestinationViewController(
                plan: plan,
                selected: { [weak self] destination in
                    self?.presenter?.dismiss(animated: true) {
                        self?.destinationSelected(destination, plan: plan)
                    }
                },
                cancelled: { [weak self] in
                    self?.presenter?.dismiss(animated: true) { self?.finish(false) }
                }
            )
            let navigation = UINavigationController(rootViewController: destinations)
            navigation.modalPresentationStyle = .formSheet
            presenter?.present(navigation, animated: true)
        }
    }

    private func destinationSelected(
        _ destination: MobileMutationDestination,
        plan: MobileMutationPlan
    ) {
        if destination.requiresOverwrite {
            let verb = plan.action == .move ? "Move" : "Copy"
            let alert = UIAlertController(
                title: "Replace Existing Password?",
                message: "\(destination.title) already contains “\(plan.sourceTitle)”. \(verb) and replace it?",
                preferredStyle: .alert
            )
            alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) {
                [weak self] _ in self?.finish(false)
            })
            alert.addAction(UIAlertAction(title: "Replace", style: .destructive) {
                [weak self] _ in
                self?.confirmEditorIfNeeded(plan, destination: destination.path, overwrite: true)
            })
            presenter?.present(alert, animated: true)
        } else {
            confirmEditorIfNeeded(plan, destination: destination.path, overwrite: false)
        }
    }

    private func confirmDelete(_ plan: MobileMutationPlan) {
        let suffix = plan.hasDirtyEditor
            ? " Unsaved changes in its open editor will also be discarded."
            : " This can’t be undone."
        let alert = UIAlertController(
            title: "Delete “\(plan.sourceTitle)”?",
            message: "The encrypted password entry will be permanently deleted.\(suffix)",
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) {
            [weak self] _ in self?.finish(false)
        })
        alert.addAction(UIAlertAction(title: "Delete", style: .destructive) {
            [weak self] _ in
            self?.perform(
                plan,
                destination: nil,
                overwrite: false,
                discardEditor: plan.hasOpenEditor,
                mayAuthenticate: true
            )
        })
        presenter?.present(alert, animated: true)
    }

    private func confirmEditorIfNeeded(
        _ plan: MobileMutationPlan,
        destination: String,
        overwrite: Bool
    ) {
        guard plan.hasOpenEditor else {
            perform(
                plan,
                destination: destination,
                overwrite: overwrite,
                discardEditor: false,
                mayAuthenticate: true
            )
            return
        }
        let verb = plan.action == .move ? "Move" : "Copy"
        let alert = UIAlertController(
            title: plan.hasDirtyEditor ? "Discard Changes and \(verb)?" : "Close Editor and \(verb)?",
            message: plan.hasDirtyEditor
                ? "Unsaved changes in the open editor for “\(plan.sourceTitle)” will be discarded."
                : "The open editor for “\(plan.sourceTitle)” must close before this action.",
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) {
            [weak self] _ in self?.finish(false)
        })
        alert.addAction(UIAlertAction(
            title: plan.hasDirtyEditor ? "Discard and \(verb)" : "Close and \(verb)",
            style: plan.hasDirtyEditor ? .destructive : .default
        ) { [weak self] _ in
            self?.perform(
                plan,
                destination: destination,
                overwrite: overwrite,
                discardEditor: true,
                mayAuthenticate: true
            )
        })
        presenter?.present(alert, animated: true)
    }

    private func perform(
        _ plan: MobileMutationPlan,
        destination: String?,
        overwrite: Bool,
        discardEditor: Bool,
        mayAuthenticate: Bool
    ) {
        guard let authentication else {
            fail(.unavailable)
            return
        }
        let request = MobileMutationRequest(
            action: plan.action,
            source: plan.source,
            revision: plan.revision,
            destination: destination,
            confirmed: plan.action == .delete,
            overwrite: overwrite,
            discardEditor: discardEditor
        )
        operationTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    return Result<MobileMutationOutcome, AuthenticationFailure>.success(
                        try authentication.performEntryMutation(request: request)
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self else { return }
            operationTask = nil
            switch result {
            case let .success(outcome):
                UINotificationFeedbackGenerator().notificationOccurred(.success)
                UIAccessibility.post(notification: .announcement, argument: outcome.detail)
                NotificationCenter.default.post(name: .ironStorageLocalStoreDidChange, object: nil)
                finish(true)
            case let .failure(failure)
                where mayAuthenticate && failure.kind == .expired:
                unlockAndRetry(
                    plan,
                    destination: destination,
                    overwrite: overwrite,
                    discardEditor: discardEditor,
                    passphrase: nil
                )
            case let .failure(failure):
                fail(failure)
            }
        }
    }

    private func unlockAndRetry(
        _ plan: MobileMutationPlan,
        destination: String?,
        overwrite: Bool,
        discardEditor: Bool,
        passphrase: String?
    ) {
        guard let authentication else {
            fail(.unavailable)
            return
        }
        operationTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    _ = try authentication.unlockEntry(path: plan.source, passphrase: passphrase)
                    return Result<Void, AuthenticationFailure>.success(())
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self else { return }
            operationTask = nil
            switch result {
            case .success:
                perform(
                    plan,
                    destination: destination,
                    overwrite: overwrite,
                    discardEditor: discardEditor,
                    mayAuthenticate: false
                )
            case let .failure(failure)
                where passphrase == nil
                    && (failure.kind == .passphraseRequired
                        || failure.kind == .biometryUnavailable):
                promptForPassphrase(
                    plan,
                    destination: destination,
                    overwrite: overwrite,
                    discardEditor: discardEditor,
                    message: failure.detail
                )
            case let .failure(failure):
                fail(failure)
            }
        }
    }

    private func promptForPassphrase(
        _ plan: MobileMutationPlan,
        destination: String?,
        overwrite: Bool,
        discardEditor: Bool,
        message: String
    ) {
        let alert = UIAlertController(
            title: "GPG Key Passphrase",
            message: message,
            preferredStyle: .alert
        )
        alert.addTextField { field in
            field.isSecureTextEntry = true
            field.textContentType = .password
            field.placeholder = "Passphrase"
            field.returnKeyType = .go
        }
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) {
            [weak self] _ in self?.finish(false)
        })
        alert.addAction(UIAlertAction(title: "Unlock", style: .default) {
            [weak self, weak alert] _ in
            guard let value = alert?.textFields?.first?.text, !value.isEmpty else {
                self?.finish(false)
                return
            }
            alert?.textFields?.first?.text = nil
            self?.unlockAndRetry(
                plan,
                destination: destination,
                overwrite: overwrite,
                discardEditor: discardEditor,
                passphrase: value
            )
        })
        presenter?.present(alert, animated: true)
    }

    private func fail(_ failure: AuthenticationFailure) {
        presenter?.presentAuthenticationFailure(failure)
        finish(false)
    }

    private func finish(_ applied: Bool) {
        contextualCompletion?(applied)
        contextualCompletion = nil
        working = false
    }
}

@MainActor
private final class PasswordDestinationViewController: UITableViewController {
    private let plan: MobileMutationPlan
    private let selected: (MobileMutationDestination) -> Void
    private let cancelled: () -> Void

    init(
        plan: MobileMutationPlan,
        selected: @escaping (MobileMutationDestination) -> Void,
        cancelled: @escaping () -> Void
    ) {
        self.plan = plan
        self.selected = selected
        self.cancelled = cancelled
        super.init(style: .insetGrouped)
        title = plan.action == .move ? "Move to Folder" : "Copy to Folder"
        navigationItem.largeTitleDisplayMode = .never
        navigationItem.leftBarButtonItem = UIBarButtonItem(
            systemItem: .cancel,
            primaryAction: UIAction { [weak self] _ in self?.cancelled() }
        )
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        plan.destinations.count
    }

    override func tableView(
        _ tableView: UITableView,
        titleForHeaderInSection section: Int
    ) -> String? {
        plan.sourceTitle
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        let destination = plan.destinations[indexPath.row]
        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
        var content = cell.defaultContentConfiguration()
        content.image = UIImage(systemName: "folder")
        content.text = destination.title
        content.secondaryText = destination.requiresOverwrite
            ? "\(destination.detail) · Replaces existing password"
            : destination.detail
        content.secondaryTextProperties.numberOfLines = 2
        cell.contentConfiguration = content
        cell.accessoryType = .disclosureIndicator
        cell.accessibilityHint = destination.requiresOverwrite
            ? "Requires confirmation before replacing the existing password."
            : "Selects this destination folder."
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        selected(plan.destinations[indexPath.row])
    }
}

@MainActor
private final class PasswordSearchViewController: UITableViewController, MobileTabRoot,
    UISearchResultsUpdating
{
    fileprivate let shellTab = MobileTab.search
    private let authentication: MobileAuthentication?
    private let searchController = UISearchController(searchResultsController: nil)
    private var shellPage: MobilePage
    private var searchPage: MobilePasswordPage?
    private var searchTask: Task<Void, Never>?
    private var shellTask: Task<Void, Never>?
    private var generation = 0
    private lazy var mutationCoordinator = PasswordMutationCoordinator(
        presenter: self,
        authentication: authentication
    )

    init(shellPage: MobilePage, authentication: MobileAuthentication?) {
        self.shellPage = shellPage
        self.authentication = authentication
        super.init(style: .insetGrouped)
        title = shellPage.title
        navigationItem.largeTitleDisplayMode = .always
        searchController.searchResultsUpdater = self
        searchController.obscuresBackgroundDuringPresentation = false
        searchController.searchBar.placeholder = "Entry names and folders"
        searchController.searchBar.autocapitalizationType = .none
        searchController.searchBar.autocorrectionType = .no
        searchController.searchBar.spellCheckingType = .no
        navigationItem.searchController = searchController
        navigationItem.hidesSearchBarWhenScrolling = false
        definesPresentationContext = true
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(localStoreDidChange),
            name: .ironStorageLocalStoreDidChange,
            object: nil
        )
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    deinit {
        searchTask?.cancel()
        shellTask?.cancel()
        NotificationCenter.default.removeObserver(self)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        apply(shellPage)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        reloadShell()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        if searchController.searchBar.text?.isEmpty != false {
            searchController.isActive = true
            searchController.searchBar.becomeFirstResponder()
        }
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        searchPage?.rows.isEmpty == false ? 1 : 0
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        searchPage?.rows.count ?? 0
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
        guard let row = searchPage?.rows[indexPath.row] else { return cell }
        var content = cell.defaultContentConfiguration()
        content.image = UIImage(systemName: row.systemImage)
        content.text = row.title
        content.secondaryText = row.detail
        content.textProperties.numberOfLines = 2
        content.secondaryTextProperties.numberOfLines = 2
        cell.contentConfiguration = content
        cell.accessoryView = mutationCoordinator.accessoryView(for: row)
        cell.accessibilityCustomActions = mutationCoordinator.accessibilityActions(for: row)
        cell.accessibilityLabel = "\(row.title), in \(row.detail)"
        cell.accessibilityHint = "Opens the locked password viewer. More actions follow."
        return cell
    }

    override func tableView(
        _ tableView: UITableView,
        trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
    ) -> UISwipeActionsConfiguration? {
        guard let rows = searchPage?.rows, rows.indices.contains(indexPath.row) else { return nil }
        let row = rows[indexPath.row]
        return mutationCoordinator.swipeConfiguration(for: row)
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        guard let row = searchPage?.rows[indexPath.row] else { return }
        navigationController?.pushViewController(
            LockedPasswordViewController(entry: row, authentication: authentication),
            animated: true
        )
    }

    func updateSearchResults(for searchController: UISearchController) {
        search(searchController.searchBar.text ?? "")
    }

    @objc private func localStoreDidChange() {
        guard viewIfLoaded?.window != nil else { return }
        search(searchController.searchBar.text ?? "")
    }

    private func reloadShell() {
        shellTask?.cancel()
        shellTask = Task { [weak self] in
            let shell = await Task.detached(priority: .userInitiated) { mobileShell() }.value
            guard
                !Task.isCancelled,
                let self,
                let page = shell.pages.first(where: { $0.tab == .search })
            else { return }
            apply(page)
        }
    }

    private func apply(_ page: MobilePage) {
        shellPage = page
        title = page.title
        guard page.state == .ready else {
            searchTask?.cancel()
            searchPage = nil
            tableView.reloadData()
            var configuration = page.state == .loading
                ? UIContentUnavailableConfiguration.loading()
                : UIContentUnavailableConfiguration.empty()
            configuration.image = page.state == .loading
                ? nil
                : UIImage(systemName: page.state == .error ? "exclamationmark.triangle" : "lock.shield")
            configuration.text = page.stateTitle
            configuration.secondaryText = page.stateDetail
            contentUnavailableConfiguration = configuration
            return
        }
        search(searchController.searchBar.text ?? "")
    }

    private func search(_ query: String) {
        generation += 1
        let currentGeneration = generation
        searchTask?.cancel()
        guard shellPage.state == .ready else { return }
        guard !query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
            searchPage = nil
            tableView.reloadData()
            var configuration = UIContentUnavailableConfiguration.empty()
            configuration.image = UIImage(systemName: "magnifyingglass")
            configuration.text = "Search Passwords"
            configuration.secondaryText = "Search by entry name or folder."
            contentUnavailableConfiguration = configuration
            return
        }

        searchPage = nil
        tableView.reloadData()
        var loading = UIContentUnavailableConfiguration.loading()
        loading.text = "Searching Passwords"
        contentUnavailableConfiguration = loading
        searchTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    return Result<MobilePasswordPage, PasswordFailure>.success(
                        try mobilePasswordSearch(query: query)
                    )
                } catch let error as MobilePasswordFfiError {
                    return .failure(PasswordFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self, currentGeneration == generation else { return }
            finish(result, query: query)
        }
    }

    private func finish(
        _ result: Result<MobilePasswordPage, PasswordFailure>,
        query: String
    ) {
        switch result {
        case let .success(page):
            searchPage = page
            tableView.reloadData()
            if page.rows.isEmpty {
                var configuration = UIContentUnavailableConfiguration.empty()
                configuration.image = UIImage(systemName: "magnifyingglass")
                configuration.text = "No Results"
                configuration.secondaryText = "No password entries match “\(query)”."
                contentUnavailableConfiguration = configuration
            } else {
                contentUnavailableConfiguration = nil
                UIAccessibility.post(
                    notification: .announcement,
                    argument: "\(page.rows.count) password results"
                )
            }
        case let .failure(failure):
            searchPage = nil
            tableView.reloadData()
            var configuration = UIContentUnavailableConfiguration.empty()
            configuration.image = UIImage(systemName: "exclamationmark.triangle")
            configuration.text = failure.title
            configuration.secondaryText = failure.detail
            contentUnavailableConfiguration = configuration
        }
    }
}

@MainActor
private final class PasswordDirectoryViewController: UITableViewController, MobileTabRoot {
    fileprivate let shellTab = MobileTab.passwords
    private let path: String?
    private let authentication: MobileAuthentication?
    private var shellPage: MobilePage?
    private var directoryPage: MobilePasswordPage?
    private var loadTask: Task<Void, Never>?
    private var createTask: Task<Void, Never>?
    private var loadGeneration = 0
    private lazy var mutationCoordinator = PasswordMutationCoordinator(
        presenter: self,
        authentication: authentication
    )

    init(
        shellPage: MobilePage,
        authentication: MobileAuthentication?,
        path: String? = nil
    ) {
        self.shellPage = shellPage
        self.authentication = authentication
        self.path = path
        super.init(style: .insetGrouped)
        title = shellPage.title
        navigationItem.largeTitleDisplayMode = path == nil ? .always : .never
        refreshControl = UIRefreshControl()
        refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(localStoreDidChange),
            name: .ironStorageLocalStoreDidChange,
            object: nil
        )
    }

    private init(path: String, title: String, authentication: MobileAuthentication?) {
        self.path = path
        self.authentication = authentication
        shellPage = nil
        super.init(style: .insetGrouped)
        self.title = title
        navigationItem.largeTitleDisplayMode = .never
        refreshControl = UIRefreshControl()
        refreshControl?.addTarget(self, action: #selector(refreshRequested), for: .valueChanged)
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(localStoreDidChange),
            name: .ironStorageLocalStoreDidChange,
            object: nil
        )
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    deinit {
        loadTask?.cancel()
        createTask?.cancel()
        NotificationCenter.default.removeObserver(self)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        if let shellPage {
            apply(shellPage)
        } else {
            showLoading()
        }
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        reload()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        loadGeneration += 1
        loadTask?.cancel()
        refreshControl?.endRefreshing()
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        directoryPage?.rows.isEmpty == false ? 1 : 0
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        directoryPage?.rows.count ?? 0
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
        guard let row = directoryPage?.rows[indexPath.row] else { return cell }
        var content = cell.defaultContentConfiguration()
        content.image = UIImage(systemName: row.systemImage)
        content.text = row.title
        content.secondaryText = row.detail
        content.textProperties.numberOfLines = 2
        content.secondaryTextProperties.numberOfLines = 1
        cell.contentConfiguration = content
        if row.kind == .entry {
            cell.accessoryView = mutationCoordinator.accessoryView(for: row)
            cell.accessibilityCustomActions = mutationCoordinator.accessibilityActions(for: row)
        } else {
            cell.accessoryType = .disclosureIndicator
        }
        cell.accessibilityLabel = "\(row.title), \(row.detail)"
        cell.accessibilityHint = row.kind == .directory
            ? "Opens this password folder."
            : "Opens the locked password viewer. More actions follow."
        return cell
    }

    override func tableView(
        _ tableView: UITableView,
        trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
    ) -> UISwipeActionsConfiguration? {
        guard let rows = directoryPage?.rows, rows.indices.contains(indexPath.row) else { return nil }
        let row = rows[indexPath.row]
        guard row.kind == .entry else { return nil }
        return mutationCoordinator.swipeConfiguration(for: row)
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let row = directoryPage?.rows[indexPath.row] else { return }
        let controller: UIViewController = switch row.kind {
        case .directory:
            PasswordDirectoryViewController(
                path: row.path,
                title: row.title,
                authentication: authentication
            )
        case .entry:
            LockedPasswordViewController(entry: row, authentication: authentication)
        }
        navigationController?.pushViewController(controller, animated: true)
    }

    @objc private func refreshRequested() {
        reload()
    }

    @objc private func localStoreDidChange() {
        guard viewIfLoaded?.window != nil else { return }
        reload()
    }

    @objc private func createRequested() {
        let alert = UIAlertController(
            title: "New Password",
            message: "Enter a name for the password entry in this folder.",
            preferredStyle: .alert
        )
        alert.addTextField { field in
            field.placeholder = "Entry Name"
            field.autocapitalizationType = .words
            field.clearButtonMode = .whileEditing
            field.returnKeyType = .next
        }
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        alert.addAction(UIAlertAction(title: "Create", style: .default) { [weak self, weak alert] _ in
            guard let name = alert?.textFields?.first?.text, !name.isEmpty else { return }
            self?.beginCreate(name: name, passphrase: nil)
        })
        present(alert, animated: true)
    }

    private func beginCreate(name: String, passphrase: String?) {
        guard let authentication else {
            presentAuthenticationFailure(.unavailable)
            return
        }
        createTask?.cancel()
        let directory = path ?? ""
        createTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    return Result<MobileEntryEditorSession, AuthenticationFailure>.success(
                        try authentication.beginCreateEntry(
                            directory: directory,
                            name: name,
                            passphrase: passphrase
                        )
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self else { return }
            switch result {
            case let .success(session):
                presentEditor(session, focusedField: nil)
            case let .failure(failure)
                where passphrase == nil
                    && (failure.kind == .passphraseRequired
                        || failure.kind == .biometryUnavailable):
                promptForCreationPassphrase(name: name, message: failure.detail)
            case let .failure(failure):
                presentAuthenticationFailure(failure)
            }
        }
    }

    private func promptForCreationPassphrase(name: String, message: String) {
        let alert = UIAlertController(
            title: "GPG Key Passphrase",
            message: message,
            preferredStyle: .alert
        )
        alert.addTextField { field in
            field.isSecureTextEntry = true
            field.textContentType = .password
            field.placeholder = "Passphrase"
            field.returnKeyType = .go
        }
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        alert.addAction(UIAlertAction(title: "Unlock", style: .default) { [weak self, weak alert] _ in
            guard let value = alert?.textFields?.first?.text, !value.isEmpty else { return }
            alert?.textFields?.first?.text = nil
            self?.beginCreate(name: name, passphrase: value)
        })
        present(alert, animated: true)
    }

    private func presentEditor(_ session: MobileEntryEditorSession, focusedField: UInt64?) {
        guard let authentication else { return }
        let editor = MobileEntryEditorViewController(
            authentication: authentication,
            session: session,
            focusedField: focusedField
        ) { [weak self] _ in
            NotificationCenter.default.post(name: .ironStorageLocalStoreDidChange, object: nil)
            self?.reload()
        }
        let navigation = UINavigationController(rootViewController: editor)
        navigation.modalPresentationStyle = .formSheet
        present(navigation, animated: true)
    }

    private func reload() {
        loadGeneration += 1
        let generation = loadGeneration
        let selectedID = tableView.indexPathForSelectedRow.flatMap {
            directoryPage?.rows[$0.row].id
        }
        loadTask?.cancel()
        loadTask = Task { [weak self] in
            guard let self else { return }
            if path == nil {
                let shell = await Task.detached(priority: .userInitiated) { mobileShell() }.value
                guard
                    !Task.isCancelled,
                    generation == loadGeneration,
                    let page = shell.pages.first(where: { $0.tab == .passwords })
                else { return }
                apply(page)
                guard page.state == .ready else { return }
            } else {
                showLoading()
            }
            let path = self.path
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    return Result<MobilePasswordPage, PasswordFailure>.success(
                        try mobilePasswordPage(path: path)
                    )
                } catch let error as MobilePasswordFfiError {
                    return .failure(PasswordFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, generation == loadGeneration else { return }
            finish(result, selectedID: selectedID)
        }
    }

    private func apply(_ page: MobilePage) {
        shellPage = page
        title = page.title
        guard page.state == .ready else {
            directoryPage = nil
            navigationItem.rightBarButtonItem = nil
            tableView.reloadData()
            refreshControl?.endRefreshing()
            var configuration = page.state == .loading
                ? UIContentUnavailableConfiguration.loading()
                : UIContentUnavailableConfiguration.empty()
            configuration.image = page.state == .loading
                ? nil
                : UIImage(systemName: page.state == .error ? "exclamationmark.triangle" : "lock.shield")
            configuration.text = page.stateTitle
            configuration.secondaryText = page.stateDetail
            contentUnavailableConfiguration = configuration
            return
        }
        showLoading()
    }

    private func finish(
        _ result: Result<MobilePasswordPage, PasswordFailure>,
        selectedID: String?
    ) {
        refreshControl?.endRefreshing()
        switch result {
        case let .success(page):
            directoryPage = page
            title = page.title
            navigationItem.rightBarButtonItem = UIBarButtonItem(
                barButtonSystemItem: .add,
                target: self,
                action: #selector(createRequested)
            )
            navigationItem.rightBarButtonItem?.accessibilityLabel = "Create password entry"
            contentUnavailableConfiguration = nil
            tableView.reloadData()
            if page.rows.isEmpty {
                var configuration = UIContentUnavailableConfiguration.empty()
                configuration.image = UIImage(systemName: "folder")
                configuration.text = "Empty Folder"
                configuration.secondaryText = "This password folder contains no entries."
                contentUnavailableConfiguration = configuration
            } else if let selectedID,
                      let row = page.rows.firstIndex(where: { $0.id == selectedID }) {
                tableView.selectRow(at: IndexPath(row: row, section: 0), animated: false, scrollPosition: .none)
            }
        case let .failure(failure):
            navigationItem.rightBarButtonItem = nil
            if failure.kind == .directoryMissing, path != nil {
                UIAccessibility.post(
                    notification: .announcement,
                    argument: "The password folder no longer exists."
                )
                navigationController?.popViewController(animated: true)
                return
            }
            directoryPage = nil
            tableView.reloadData()
            var configuration = UIContentUnavailableConfiguration.empty()
            configuration.image = UIImage(systemName: "exclamationmark.triangle")
            configuration.text = failure.title
            configuration.secondaryText = failure.detail
            contentUnavailableConfiguration = configuration
        }
    }

    private func showLoading() {
        var configuration = UIContentUnavailableConfiguration.loading()
        configuration.text = "Loading Passwords"
        configuration.secondaryText = "Reading the locked folder model from storage."
        contentUnavailableConfiguration = configuration
    }
}

private struct PasswordFailure: Error, Sendable {
    let kind: MobilePasswordErrorKind
    let title: String
    let detail: String

    init(_ error: MobilePasswordFfiError) {
        switch error {
        case let .Failed(kind, title, detail):
            self.kind = kind
            self.title = title
            self.detail = detail
        }
    }

    static let unexpected = PasswordFailure(
        kind: .repository,
        title: "Passwords Are Unavailable",
        detail: "IronStorage could not load the storage-provided folder."
    )

    private init(kind: MobilePasswordErrorKind, title: String, detail: String) {
        self.kind = kind
        self.title = title
        self.detail = detail
    }
}

@MainActor
private final class LockedPasswordViewController: UITableViewController {
    private let entry: MobilePasswordRow
    private let authentication: MobileAuthentication?
    private let totpCodeView = TotpCodeView()
    private var unlockTask: Task<Void, Never>?
    private var entryTask: Task<Void, Never>?
    private var totpTask: Task<Void, Never>?
    private var totpTimerTask: Task<Void, Never>?
    private var clipboardTask: Task<Void, Never>?
    private var feedbackTask: Task<Void, Never>?
    private var state: MobileAuthenticationState?
    private var page: MobileEntryPage?
    private var totp: MobileTotpDetail?
    private var cacheNotice: String?
    private var copiedTotpValue: String?
    private var revealedValues: [UInt64: String] = [:]

    init(entry: MobilePasswordRow, authentication: MobileAuthentication?) {
        self.entry = entry
        self.authentication = authentication
        super.init(style: .insetGrouped)
        title = entry.title
        navigationItem.largeTitleDisplayMode = .never
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(MobileEntryFieldCell.self, forCellReuseIdentifier: "EntryField")
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 92
        view.accessibilityIdentifier = entry.id
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(authenticationDidChange),
            name: .ironStorageAuthenticationDidChange,
            object: nil
        )
        refreshState()
    }

    deinit {
        unlockTask?.cancel()
        entryTask?.cancel()
        totpTask?.cancel()
        totpTimerTask?.cancel()
        clipboardTask?.cancel()
        feedbackTask?.cancel()
        NotificationCenter.default.removeObserver(self)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        refreshState()
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        guard let header = tableView.tableHeaderView else { return }
        let height = header.systemLayoutSizeFitting(
            CGSize(width: tableView.bounds.width, height: 0),
            withHorizontalFittingPriority: .required,
            verticalFittingPriority: .fittingSizeLevel
        ).height
        guard abs(header.frame.height - height) > 0.5 else { return }
        header.frame.size.height = height
        tableView.tableHeaderView = header
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        page?.sections.count ?? 0
    }

    override func tableView(
        _ tableView: UITableView,
        numberOfRowsInSection section: Int
    ) -> Int {
        page?.sections[section].fields.count ?? 0
    }

    override func tableView(
        _ tableView: UITableView,
        titleForHeaderInSection section: Int
    ) -> String? {
        page?.sections[section].title
    }

    override func tableView(
        _ tableView: UITableView,
        titleForFooterInSection section: Int
    ) -> String? {
        guard let page, section == page.sections.indices.last else { return nil }
        return cacheNotice
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        guard
            let cell = tableView.dequeueReusableCell(
                withIdentifier: "EntryField",
                for: indexPath
            ) as? MobileEntryFieldCell,
            let field = field(at: indexPath)
        else { return UITableViewCell() }
        cell.configure(
            field: field,
            revealedValue: revealedValues[field.id],
            copy: { [weak self] tappedCell in self?.copy(field, in: tappedCell) },
            reveal: { [weak self] in self?.revealOrHide(field) },
            edit: { [weak self] in self?.edit(field) }
        )
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        guard
            let field = field(at: indexPath),
            let cell = tableView.cellForRow(at: indexPath) as? MobileEntryFieldCell
        else { return }
        copy(field, in: cell)
    }

    @objc private func authenticationDidChange() {
        refreshState()
    }

    @objc private func unlockRequested() {
        unlock(passphrase: nil)
    }

    @objc private func lockRequested() {
        guard let authentication else { return }
        do {
            try authentication.manualLock()
            state = try authentication.state()
            NotificationCenter.default.post(
                name: .ironStorageAuthenticationDidChange,
                object: authentication
            )
        } catch let error as MobileAuthenticationFfiError {
            presentAuthenticationFailure(AuthenticationFailure(error))
        } catch {
            presentAuthenticationFailure(.unexpected)
        }
    }

    private func refreshState() {
        state = try? authentication?.state()
        if state?.unlocked == true {
            if page == nil { loadEntry() }
        } else {
            maskAndDiscardEntry()
            showLocked()
        }
    }

    private func unlock(passphrase: String?) {
        guard let authentication else {
            presentAuthenticationFailure(.unavailable)
            return
        }
        unlockTask?.cancel()
        showLoading(title: "Unlocking Password")
        let path = entry.path
        unlockTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    return Result<MobileAuthenticationState, AuthenticationFailure>.success(
                        try authentication.unlockEntry(path: path, passphrase: passphrase)
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self else { return }
            switch result {
            case let .success(state):
                self.state = state
                NotificationCenter.default.post(
                    name: .ironStorageAuthenticationDidChange,
                    object: authentication
                )
                loadEntry()
                UIAccessibility.post(notification: .announcement, argument: "Password unlocked")
            case let .failure(failure):
                if passphrase == nil,
                   failure.kind == .passphraseRequired || failure.kind == .biometryUnavailable {
                    promptForPassphrase(message: failure.detail)
                } else if failure.kind != .cancelled {
                    presentAuthenticationFailure(failure)
                    showLocked()
                }
            }
        }
    }

    private func promptForPassphrase(message: String) {
        let alert = UIAlertController(
            title: "GPG Key Passphrase",
            message: message,
            preferredStyle: .alert
        )
        alert.addTextField { field in
            field.isSecureTextEntry = true
            field.textContentType = .password
            field.placeholder = "Passphrase"
            field.returnKeyType = .go
        }
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        alert.addAction(UIAlertAction(title: "Unlock", style: .default) { [weak self, weak alert] _ in
            guard let field = alert?.textFields?.first, let value = field.text, !value.isEmpty else {
                return
            }
            field.text = nil
            self?.unlock(passphrase: value)
        })
        present(alert, animated: true)
    }

    private func loadEntry() {
        guard let authentication else {
            showLocked()
            return
        }
        entryTask?.cancel()
        showLoading(title: "Opening Password")
        let path = entry.path
        entryTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    return Result<MobileEntryPresentation, AuthenticationFailure>.success(
                        try authentication.entryPage(
                            path: path,
                            unixSeconds: currentUnixSeconds()
                        )
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self else { return }
            switch result {
            case let .success(presentation):
                page = presentation.page
                totp = presentation.totp
                cacheNotice = presentation.cacheNotice
                title = presentation.page.title
                contentUnavailableConfiguration = nil
                installLockButton()
                configureTotpHeader()
                tableView.reloadData()
            case let .failure(failure):
                handle(failure)
            }
        }
    }

    private func field(at indexPath: IndexPath) -> MobileEntryField? {
        guard
            let page,
            page.sections.indices.contains(indexPath.section),
            page.sections[indexPath.section].fields.indices.contains(indexPath.row)
        else { return nil }
        return page.sections[indexPath.section].fields[indexPath.row]
    }

    private func revealOrHide(_ field: MobileEntryField) {
        if revealedValues.removeValue(forKey: field.id) != nil {
            tableView.reloadData()
            UIAccessibility.post(notification: .announcement, argument: "\(field.label) hidden")
            return
        }
        performFieldAction(field) { authentication, path in
            try authentication.revealEntryField(path: path, field: field.id)
        } success: { [weak self] value in
            self?.revealedValues[field.id] = value
            self?.tableView.reloadData()
            UIAccessibility.post(notification: .announcement, argument: "\(field.label) revealed")
        }
    }

    private func copy(_ field: MobileEntryField, in cell: MobileEntryFieldCell?) {
        cell?.flashCopied()
        performFieldAction(field) { authentication, path in
            try authentication.copyEntryField(path: path, field: field.id)
        } success: { [weak self] copy in
            guard let self else { return }
            UIPasteboard.general.string = copy.value
            showFeedback("\(field.label) copied")
            UIAccessibility.post(notification: .announcement, argument: "\(field.label) copied")
            clipboardTask?.cancel()
            clipboardTask = Task { @MainActor in
                do {
                    try await Task.sleep(for: .seconds(copy.timeoutSeconds))
                } catch {
                    return
                }
                if UIPasteboard.general.string == copy.value {
                    UIPasteboard.general.items = []
                }
            }
        }
    }

    private func configureTotpHeader() {
        guard let totp else {
            totpTimerTask?.cancel()
            totpCodeView.clear()
            tableView.tableHeaderView = nil
            return
        }
        totpCodeView.frame = CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 1)
        totpCodeView.copyRequested = { [weak self] in self?.copyTotpCode() }
        totpCodeView.apply(totp)
        tableView.tableHeaderView = totpCodeView
        startTotpTimer()
    }

    private func startTotpTimer() {
        totpTimerTask?.cancel()
        totpTimerTask = Task { [weak self] in
            while !Task.isCancelled {
                do {
                    try await Task.sleep(for: .seconds(1))
                } catch {
                    return
                }
                guard let self, let totp else { return }
                if currentUnixSeconds() >= totp.validUntil {
                    refreshTotpCode()
                } else {
                    totpCodeView.updateCountdown(totp)
                }
            }
        }
    }

    private func refreshTotpCode() {
        guard totpTask == nil, let authentication else { return }
        let path = entry.path
        totpTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    return Result<MobileTotpDetail, AuthenticationFailure>.success(
                        try authentication.totpDetail(
                            path: path,
                            unixSeconds: currentUnixSeconds()
                        )
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard let self else { return }
            totpTask = nil
            guard !Task.isCancelled else { return }
            switch result {
            case let .success(detail):
                totp = detail
                totpCodeView.apply(detail)
            case let .failure(failure):
                handle(failure)
            }
        }
    }

    private func copyTotpCode() {
        guard totpTask == nil, let authentication else { return }
        let path = entry.path
        totpTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    try authentication.touchUserActivity()
                    return Result<MobileEntryCopy, AuthenticationFailure>.success(
                        try authentication.copyTotpCode(
                            path: path,
                            unixSeconds: currentUnixSeconds()
                        )
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard let self else { return }
            totpTask = nil
            guard !Task.isCancelled else { return }
            switch result {
            case let .success(copy):
                UIPasteboard.general.string = copy.value
                copiedTotpValue = copy.value
                totpCodeView.flashCopied()
                clipboardTask?.cancel()
                clipboardTask = Task { @MainActor in
                    do {
                        try await Task.sleep(for: .seconds(copy.timeoutSeconds))
                    } catch {
                        return
                    }
                    if UIPasteboard.general.string == copy.value {
                        UIPasteboard.general.items = []
                    }
                    self.copiedTotpValue = nil
                }
            case let .failure(failure):
                handle(failure)
            }
        }
    }

    private func edit(_ field: MobileEntryField) {
        guard let authentication else { return }
        entryTask?.cancel()
        let path = entry.path
        entryTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    try authentication.touchUserActivity()
                    return Result<MobileEntryEditorSession, AuthenticationFailure>.success(
                        try authentication.beginEntryEditor(path: path)
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self else { return }
            switch result {
            case let .success(session):
                let editor = MobileEntryEditorViewController(
                    authentication: authentication,
                    session: session,
                    focusedField: field.id
                ) { [weak self] _ in
                    NotificationCenter.default.post(
                        name: .ironStorageLocalStoreDidChange,
                        object: nil
                    )
                    self?.revealedValues.removeAll(keepingCapacity: false)
                    self?.loadEntry()
                }
                let navigation = UINavigationController(rootViewController: editor)
                navigation.modalPresentationStyle = .formSheet
                present(navigation, animated: true)
            case let .failure(failure):
                handle(failure)
            }
        }
    }

    private func performFieldAction<Value: Sendable>(
        _ field: MobileEntryField,
        operation: @escaping @Sendable (MobileAuthentication, String) throws -> Value,
        success: @escaping @MainActor (Value) -> Void
    ) {
        guard let authentication else {
            presentAuthenticationFailure(.unavailable)
            return
        }
        let path = entry.path
        Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    try authentication.touchUserActivity()
                    return Result<Value, AuthenticationFailure>.success(
                        try operation(authentication, path)
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self else { return }
            switch result {
            case let .success(value): success(value)
            case let .failure(failure): handle(failure)
            }
        }
    }

    private func handle(_ failure: AuthenticationFailure) {
        if failure.kind == .expired {
            state = try? authentication?.state()
            maskAndDiscardEntry()
            showLocked()
            NotificationCenter.default.post(
                name: .ironStorageAuthenticationDidChange,
                object: authentication
            )
        }
        presentAuthenticationFailure(failure)
    }

    private func maskAndDiscardEntry() {
        entryTask?.cancel()
        totpTask?.cancel()
        totpTimerTask?.cancel()
        feedbackTask?.cancel()
        page = nil
        totp = nil
        cacheNotice = nil
        totpCodeView.clear()
        tableView.tableHeaderView = nil
        if UIPasteboard.general.string == copiedTotpValue {
            UIPasteboard.general.items = []
        }
        copiedTotpValue = nil
        revealedValues.removeAll(keepingCapacity: false)
        tableView.reloadData()
        navigationItem.rightBarButtonItem = nil
        navigationItem.prompt = nil
        title = entry.title
    }

    private func showLocked() {
        var configuration = UIContentUnavailableConfiguration.empty()
        configuration.image = UIImage(systemName: "lock.fill")
        configuration.text = "Locked Password"
        configuration.secondaryText =
            "Authenticate to decrypt this entry. Browsing remains available while locked."
        configuration.button = .filled()
        configuration.button.title = "Unlock"
        configuration.buttonProperties.primaryAction = UIAction { [weak self] _ in
            self?.unlockRequested()
        }
        contentUnavailableConfiguration = configuration
    }

    private func showLoading(title: String) {
        var configuration = UIContentUnavailableConfiguration.loading()
        configuration.text = title
        configuration.secondaryText = "Reading the structured entry from secure storage."
        contentUnavailableConfiguration = configuration
    }

    private func showFeedback(_ message: String) {
        feedbackTask?.cancel()
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            image: UIImage(systemName: "checkmark.circle.fill"),
            style: .plain,
            target: nil,
            action: nil
        )
        navigationItem.rightBarButtonItem?.accessibilityLabel = message
        UINotificationFeedbackGenerator().notificationOccurred(.success)
        feedbackTask = Task { [weak self] in
            do {
                try await Task.sleep(for: .seconds(2))
            } catch {
                return
            }
            guard let self else { return }
            if page != nil { installLockButton() }
        }
    }

    private func installLockButton() {
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            image: UIImage(systemName: "lock.fill"),
            style: .plain,
            target: self,
            action: #selector(lockRequested)
        )
        navigationItem.rightBarButtonItem?.accessibilityLabel = "Lock IronStorage"
    }
}

@MainActor
private final class MobileEntryFieldCell: UITableViewCell {
    private let iconView = UIImageView()
    private let labelView = UILabel()
    private let valueView = UITextView()
    private let detailView = UILabel()
    private let diagnosticView = UILabel()
    private let revealButton = UIButton(type: .system)
    private let editButton = UIButton(type: .system)
    private var copyValue: ((MobileEntryFieldCell) -> Void)?
    private var highlightTask: Task<Void, Never>?

    private static let revealAction = UIAction.Identifier("reveal-entry-field")
    private static let editAction = UIAction.Identifier("edit-entry-field")

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        selectionStyle = .default
        iconView.tintColor = .secondaryLabel
        iconView.setContentHuggingPriority(.required, for: .horizontal)
        iconView.translatesAutoresizingMaskIntoConstraints = false
        labelView.font = .preferredFont(forTextStyle: .caption1)
        labelView.textColor = .secondaryLabel
        labelView.adjustsFontForContentSizeCategory = true
        valueView.font = .preferredFont(forTextStyle: .body)
        valueView.adjustsFontForContentSizeCategory = true
        valueView.backgroundColor = .clear
        valueView.isEditable = false
        valueView.isScrollEnabled = false
        valueView.textContainerInset = .zero
        valueView.textContainer.lineFragmentPadding = 0
        let valueTap = UITapGestureRecognizer(target: self, action: #selector(valueTapped))
        valueTap.cancelsTouchesInView = false
        valueView.addGestureRecognizer(valueTap)
        detailView.font = .preferredFont(forTextStyle: .footnote)
        detailView.textColor = .secondaryLabel
        detailView.adjustsFontForContentSizeCategory = true
        detailView.numberOfLines = 0
        diagnosticView.font = .preferredFont(forTextStyle: .footnote)
        diagnosticView.textColor = .systemOrange
        diagnosticView.adjustsFontForContentSizeCategory = true
        diagnosticView.numberOfLines = 0
        revealButton.configuration = .plain()
        editButton.configuration = .plain()
        editButton.configuration?.image = UIImage(systemName: "pencil")
        editButton.accessibilityLabel = "Edit field"
        let labels = UIStackView(arrangedSubviews: [labelView, valueView, detailView, diagnosticView])
        labels.axis = .vertical
        labels.spacing = 3
        labels.setContentHuggingPriority(.defaultLow, for: .horizontal)
        labels.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        let actions = UIStackView(arrangedSubviews: [revealButton, editButton])
        actions.distribution = .fillEqually
        NSLayoutConstraint.activate([
            actions.widthAnchor.constraint(equalToConstant: 88),
            actions.heightAnchor.constraint(greaterThanOrEqualToConstant: 44),
        ])
        actions.setContentHuggingPriority(.required, for: .horizontal)
        actions.setContentCompressionResistancePriority(.required, for: .horizontal)
        let row = UIStackView(arrangedSubviews: [iconView, labels, actions])
        row.alignment = .top
        row.spacing = 12
        row.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(row)
        NSLayoutConstraint.activate([
            iconView.widthAnchor.constraint(equalToConstant: 24),
            iconView.heightAnchor.constraint(equalToConstant: 24),
            row.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
            row.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
            row.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
            row.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor),
        ])
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    func configure(
        field: MobileEntryField,
        revealedValue: String?,
        copy: @escaping (MobileEntryFieldCell) -> Void,
        reveal: @escaping () -> Void,
        edit: @escaping () -> Void
    ) {
        let value = revealedValue ?? field.value ?? field.maskedValue
        iconView.image = UIImage(systemName: field.systemImage)
        labelView.text = field.label
        valueView.text = value
        copyValue = copy
        valueView.textColor = field.sensitive && revealedValue == nil ? .secondaryLabel : .label
        valueView.isSelectable = field.selectable
        detailView.text = field.detail
        detailView.isHidden = field.detail == nil
        diagnosticView.text = field.diagnostic
        diagnosticView.isHidden = field.diagnostic == nil
        revealButton.configuration?.image = UIImage(
            systemName: revealedValue == nil ? "eye" : "eye.slash"
        )
        revealButton.accessibilityLabel = revealedValue == nil ? "Reveal field" : "Hide field"
        revealButton.removeAction(identifiedBy: Self.revealAction, for: .touchUpInside)
        revealButton.addAction(
            UIAction(identifier: Self.revealAction) { _ in reveal() },
            for: .touchUpInside
        )
        revealButton.alpha = field.sensitive ? 1 : 0
        revealButton.isEnabled = field.sensitive
        revealButton.accessibilityElementsHidden = !field.sensitive
        editButton.removeAction(identifiedBy: Self.editAction, for: .touchUpInside)
        editButton.addAction(
            UIAction(identifier: Self.editAction) { _ in edit() },
            for: .touchUpInside
        )
        editButton.alpha = field.editable ? 1 : 0
        editButton.isEnabled = field.editable
        editButton.accessibilityElementsHidden = !field.editable
        accessibilityLabel = [field.label, value, field.detail, field.diagnostic]
            .compactMap { $0 }
            .joined(separator: ", ")
        accessibilityHint = field.sensitive
            ? "Double tap the row to copy. Reveal and Edit buttons follow."
            : "Double tap the row to copy. An Edit button follows."
    }

    func flashCopied() {
        highlightTask?.cancel()
        setHighlighted(true, animated: false)
        highlightTask = Task { [weak self] in
            do {
                try await Task.sleep(for: .milliseconds(350))
            } catch {
                return
            }
            self?.setHighlighted(false, animated: true)
        }
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        highlightTask?.cancel()
        setHighlighted(false, animated: false)
    }

    @objc private func valueTapped() {
        copyValue?(self)
    }
}

@MainActor
private final class MobileEntryEditorViewController: UITableViewController,
    UIAdaptivePresentationControllerDelegate
{
    private struct FieldState {
        var name: String?
        var value: String?
    }

    private let authentication: MobileAuthentication
    private var editorID: UInt64
    private var page: MobileEntryEditorPage
    private let focusedField: UInt64?
    private let saved: (String) -> Void
    private var states: [UInt64: FieldState] = [:]
    private var localDirty = false
    private var focused = false
    private var operationTask: Task<Void, Never>?

    init(
        authentication: MobileAuthentication,
        session: MobileEntryEditorSession,
        focusedField: UInt64?,
        saved: @escaping (String) -> Void
    ) {
        self.authentication = authentication
        editorID = session.id
        page = session.page
        self.focusedField = focusedField
        self.saved = saved
        super.init(style: .insetGrouped)
        resetStates()
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    deinit {
        operationTask?.cancel()
        NotificationCenter.default.removeObserver(self)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        title = page.creating ? "New \(page.title)" : "Edit \(page.title)"
        navigationItem.largeTitleDisplayMode = .never
        tableView.register(MobileEntryEditorCell.self, forCellReuseIdentifier: "EditorField")
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 120
        tableView.setEditing(true, animated: false)
        navigationItem.leftBarButtonItem = UIBarButtonItem(
            systemItem: .cancel,
            primaryAction: UIAction { [weak self] _ in self?.cancelRequested() }
        )
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            systemItem: .save,
            primaryAction: UIAction { [weak self] _ in self?.saveRequested() }
        )
        let add = UIBarButtonItem(
            systemItem: .add,
            primaryAction: UIAction { [weak self] _ in self?.addRequested() }
        )
        add.accessibilityLabel = "Add entry field"
        let generate = UIBarButtonItem(
            title: "Generate Password",
            image: UIImage(systemName: "wand.and.stars"),
            primaryAction: UIAction { [weak self] _ in self?.generateRequested() }
        )
        toolbarItems = [
            add,
            UIBarButtonItem(systemItem: .flexibleSpace),
            generate,
        ]
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(authenticationDidChange),
            name: .ironStorageAuthenticationDidChange,
            object: nil
        )
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.setToolbarHidden(false, animated: animated)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        navigationController?.presentationController?.delegate = self
        guard !focused, let focusedField,
              let index = page.fields.firstIndex(where: { $0.id == focusedField })
        else { return }
        focused = true
        let path = IndexPath(row: index, section: 0)
        tableView.scrollToRow(at: path, at: .middle, animated: true)
        (tableView.cellForRow(at: path) as? MobileEntryEditorCell)?.focusValue()
    }

    override func numberOfSections(in tableView: UITableView) -> Int { 1 }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        page.fields.count
    }

    override func tableView(
        _ tableView: UITableView,
        titleForHeaderInSection section: Int
    ) -> String? {
        "Entry Fields"
    }

    override func tableView(
        _ tableView: UITableView,
        titleForFooterInSection section: Int
    ) -> String? {
        "Drag the reorder controls to preserve the exact field order saved by Rust."
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(
            withIdentifier: "EditorField",
            for: indexPath
        ) as? MobileEntryEditorCell else { return UITableViewCell() }
        let field = page.fields[indexPath.row]
        let state = states[field.id] ?? FieldState(name: field.name, value: field.value)
        cell.configure(field: field, name: state.name, value: state.value) { [weak self] name, value in
            self?.states[field.id] = FieldState(name: name, value: value)
            self?.localDirty = true
        }
        cell.showsReorderControl = field.reorderable
        return cell
    }

    override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        page.fields[indexPath.row].removable
    }

    override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
        page.fields[indexPath.row].reorderable
    }

    override func tableView(
        _ tableView: UITableView,
        editingStyleForRowAt indexPath: IndexPath
    ) -> UITableViewCell.EditingStyle {
        page.fields[indexPath.row].removable ? .delete : .none
    }

    override func tableView(
        _ tableView: UITableView,
        commit editingStyle: UITableViewCell.EditingStyle,
        forRowAt indexPath: IndexPath
    ) {
        guard editingStyle == .delete else { return }
        do {
            try synchronizeDraft()
            page = try authentication.removeEntryEditorField(
                editor: editorID,
                field: page.fields[indexPath.row].id
            )
            resetStates()
            tableView.deleteRows(at: [indexPath], with: .automatic)
        } catch {
            presentEditorFailure(error)
            tableView.reloadData()
        }
    }

    override func tableView(
        _ tableView: UITableView,
        moveRowAt sourceIndexPath: IndexPath,
        to destinationIndexPath: IndexPath
    ) {
        let field = page.fields[sourceIndexPath.row]
        do {
            try synchronizeDraft()
            page = try authentication.reorderEntryEditorField(
                editor: editorID,
                field: field.id,
                index: UInt32(destinationIndexPath.row)
            )
            resetStates()
            tableView.reloadData()
        } catch {
            presentEditorFailure(error)
            tableView.reloadData()
        }
    }

    func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
        guard isDirty else {
            try? authentication.discardEntryEditor(editor: editorID)
            return true
        }
        return false
    }

    func presentationControllerDidAttemptToDismiss(
        _ presentationController: UIPresentationController
    ) {
        confirmDiscard()
    }

    @objc private func authenticationDidChange() {
        guard (try? authentication.state().unlocked) != true else { return }
        states = states.mapValues { _ in FieldState(name: nil, value: nil) }
        tableView.visibleCells
            .compactMap { $0 as? MobileEntryEditorCell }
            .forEach { $0.clearSensitiveContent() }
        dismiss(animated: true)
        UIAccessibility.post(notification: .announcement, argument: "Entry editor locked")
    }

    private var isDirty: Bool { localDirty || page.dirty }

    private func resetStates() {
        states = Dictionary(uniqueKeysWithValues: page.fields.map {
            ($0.id, FieldState(name: $0.name, value: $0.value))
        })
        localDirty = false
    }

    private func inputs() -> [MobileEntryEditorInput] {
        page.fields.map { field in
            let state = states[field.id] ?? FieldState(name: field.name, value: field.value)
            return MobileEntryEditorInput(id: field.id, name: state.name, value: state.value)
        }
    }

    private func synchronizeDraft() throws {
        view.endEditing(true)
        try authentication.touchUserActivity()
        page = try authentication.updateEntryEditor(editor: editorID, fields: inputs())
        resetStates()
    }

    private func cancelRequested() {
        if isDirty {
            confirmDiscard()
        } else {
            discardAndDismiss()
        }
    }

    private func confirmDiscard() {
        let sheet = UIAlertController(
            title: "Discard Changes?",
            message: "Unsaved entry changes will be lost.",
            preferredStyle: .actionSheet
        )
        sheet.addAction(UIAlertAction(title: "Discard Changes", style: .destructive) {
            [weak self] _ in self?.discardAndDismiss()
        })
        sheet.addAction(UIAlertAction(title: "Keep Editing", style: .cancel))
        sheet.popoverPresentationController?.barButtonItem = navigationItem.leftBarButtonItem
        present(sheet, animated: true)
    }

    private func discardAndDismiss() {
        try? authentication.discardEntryEditor(editor: editorID)
        states.removeAll(keepingCapacity: false)
        dismiss(animated: true)
    }

    private func addRequested() {
        let sheet = UIAlertController(title: "Add Field", message: nil, preferredStyle: .actionSheet)
        sheet.addAction(UIAlertAction(title: "Named Field", style: .default) { [weak self] _ in
            self?.promptForNamedField()
        })
        sheet.addAction(UIAlertAction(title: "Notes", style: .default) { [weak self] _ in
            self?.addField(kind: .note, name: nil, value: "")
        })
        sheet.addAction(UIAlertAction(title: "OTP URI", style: .default) { [weak self] _ in
            self?.promptForOtpUri()
        })
        sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        sheet.popoverPresentationController?.barButtonItem = toolbarItems?.first
        present(sheet, animated: true)
    }

    private func promptForNamedField() {
        let alert = UIAlertController(
            title: "Named Field",
            message: "Enter a field name. You can enter its value in the form.",
            preferredStyle: .alert
        )
        alert.addTextField { field in
            field.placeholder = "Field Name"
            field.autocapitalizationType = .none
            field.autocorrectionType = .no
            field.clearButtonMode = .whileEditing
        }
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        alert.addAction(UIAlertAction(title: "Add", style: .default) { [weak self, weak alert] _ in
            guard let name = alert?.textFields?.first?.text, !name.isEmpty else { return }
            self?.addField(kind: .field, name: name, value: "")
        })
        present(alert, animated: true)
    }

    private func promptForOtpUri() {
        let alert = UIAlertController(
            title: "OTP URI",
            message: "Enter the complete otpauth URI.",
            preferredStyle: .alert
        )
        alert.addTextField { field in
            field.placeholder = "otpauth://…"
            field.keyboardType = .URL
            field.autocapitalizationType = .none
            field.autocorrectionType = .no
        }
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        alert.addAction(UIAlertAction(title: "Add", style: .default) { [weak self, weak alert] _ in
            guard let value = alert?.textFields?.first?.text, !value.isEmpty else { return }
            self?.addField(kind: .otpUri, name: nil, value: value)
        })
        present(alert, animated: true)
    }

    private func addField(kind: MobileEntryEditorFieldKind, name: String?, value: String) {
        do {
            try synchronizeDraft()
            page = try authentication.addEntryEditorField(
                editor: editorID,
                kind: kind,
                name: name,
                value: value
            )
            resetStates()
            tableView.reloadData()
            if !page.fields.isEmpty {
                let path = IndexPath(row: page.fields.count - 1, section: 0)
                tableView.scrollToRow(at: path, at: .bottom, animated: true)
            }
        } catch {
            presentEditorFailure(error)
        }
    }

    private func generateRequested() {
        let alert = UIAlertController(
            title: "Generate Password",
            message: "Choose a validated length from 1 through \(page.maximumPasswordLength).",
            preferredStyle: .alert
        )
        alert.addTextField { [defaultLength = page.defaultPasswordLength] field in
            field.text = String(defaultLength)
            field.keyboardType = .numberPad
            field.clearButtonMode = .whileEditing
            field.accessibilityLabel = "Password length"
        }
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        alert.addAction(UIAlertAction(title: "With Symbols", style: .default) { [weak self, weak alert] _ in
            self?.generate(from: alert, noSymbols: false)
        })
        alert.addAction(UIAlertAction(title: "Letters & Numbers", style: .default) {
            [weak self, weak alert] _ in self?.generate(from: alert, noSymbols: true)
        })
        present(alert, animated: true)
    }

    private func generate(from alert: UIAlertController?, noSymbols: Bool) {
        guard let text = alert?.textFields?.first?.text,
              let length = UInt32(text)
        else {
            let failure = UIAlertController(
                title: "Invalid Password Length",
                message: "Enter a whole number from 1 through \(page.maximumPasswordLength).",
                preferredStyle: .alert
            )
            failure.addAction(UIAlertAction(title: "OK", style: .default))
            present(failure, animated: true)
            return
        }
        do {
            try synchronizeDraft()
            page = try authentication.generateEntryEditorPassword(
                editor: editorID,
                length: length,
                noSymbols: noSymbols
            )
            resetStates()
            tableView.reloadData()
            UIAccessibility.post(notification: .announcement, argument: "Password generated")
        } catch {
            presentEditorFailure(error)
        }
    }

    private func saveRequested() {
        view.endEditing(true)
        let fields = inputs()
        let authentication = authentication
        let editorID = editorID
        navigationItem.leftBarButtonItem?.isEnabled = false
        navigationItem.rightBarButtonItem?.isEnabled = false
        operationTask?.cancel()
        operationTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    try authentication.touchUserActivity()
                    return Result<MobileEntryPage, AuthenticationFailure>.success(
                        try authentication.saveEntryEditor(editor: editorID, fields: fields)
                    )
                } catch let error as MobileAuthenticationFfiError {
                    return .failure(AuthenticationFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard !Task.isCancelled, let self else { return }
            navigationItem.leftBarButtonItem?.isEnabled = true
            navigationItem.rightBarButtonItem?.isEnabled = true
            switch result {
            case let .success(savedPage):
                states.removeAll(keepingCapacity: false)
                dismiss(animated: true) { self.saved(savedPage.id) }
            case let .failure(failure):
                presentSaveFailure(failure)
            }
        }
    }

    private func presentSaveFailure(_ failure: AuthenticationFailure) {
        let alert = UIAlertController(
            title: failure.title,
            message: failure.detail,
            preferredStyle: .alert
        )
        if failure.kind == .conflict {
            alert.addAction(UIAlertAction(title: "Reload Latest", style: .destructive) {
                [weak self] _ in self?.reloadLatest()
            })
        }
        alert.addAction(UIAlertAction(title: "Keep Editing", style: .cancel))
        present(alert, animated: true)
    }

    private func reloadLatest() {
        let path = page.path
        try? authentication.discardEntryEditor(editor: editorID)
        do {
            let session = try authentication.beginEntryEditor(path: path)
            editorID = session.id
            page = session.page
            resetStates()
            tableView.reloadData()
        } catch {
            presentEditorFailure(error)
        }
    }

    private func presentEditorFailure(_ error: Error) {
        let failure = (error as? MobileAuthenticationFfiError).map(AuthenticationFailure.init)
            ?? .unexpected
        presentAuthenticationFailure(failure)
    }
}

@MainActor
private final class MobileEntryEditorCell: UITableViewCell, UITextViewDelegate {
    private let iconView = UIImageView()
    private let labelView = UILabel()
    private let nameField = UITextField()
    private let valueField = UITextField()
    private let valueView = UITextView()
    private let diagnosticView = UILabel()
    private let revealButton = UIButton(type: .system)
    private var changed: ((String?, String?) -> Void)?
    private var currentName: String?
    private var currentValue: String?
    private var sensitive = false

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        selectionStyle = .none
        iconView.tintColor = .secondaryLabel
        iconView.translatesAutoresizingMaskIntoConstraints = false
        labelView.font = .preferredFont(forTextStyle: .caption1)
        labelView.textColor = .secondaryLabel
        labelView.adjustsFontForContentSizeCategory = true
        for field in [nameField, valueField] {
            field.font = .preferredFont(forTextStyle: .body)
            field.adjustsFontForContentSizeCategory = true
            field.clearButtonMode = .whileEditing
            field.addTarget(self, action: #selector(textFieldChanged(_:)), for: .editingChanged)
        }
        nameField.placeholder = "Field Name"
        nameField.autocapitalizationType = .none
        nameField.autocorrectionType = .no
        valueView.font = .preferredFont(forTextStyle: .body)
        valueView.adjustsFontForContentSizeCategory = true
        valueView.backgroundColor = .clear
        valueView.isScrollEnabled = false
        valueView.textContainerInset = .zero
        valueView.textContainer.lineFragmentPadding = 0
        valueView.delegate = self
        diagnosticView.font = .preferredFont(forTextStyle: .footnote)
        diagnosticView.textColor = .systemOrange
        diagnosticView.adjustsFontForContentSizeCategory = true
        diagnosticView.numberOfLines = 0
        revealButton.configuration = .plain()
        revealButton.configuration?.image = UIImage(systemName: "eye")
        revealButton.addAction(UIAction { [weak self] _ in self?.toggleReveal() }, for: .touchUpInside)
        let heading = UIStackView(arrangedSubviews: [labelView, nameField])
        heading.axis = .vertical
        heading.spacing = 2
        let body = UIStackView(arrangedSubviews: [heading, valueField, valueView, diagnosticView])
        body.axis = .vertical
        body.spacing = 6
        let row = UIStackView(arrangedSubviews: [iconView, body, revealButton])
        row.alignment = .top
        row.spacing = 12
        row.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(row)
        NSLayoutConstraint.activate([
            iconView.widthAnchor.constraint(equalToConstant: 24),
            iconView.heightAnchor.constraint(equalToConstant: 24),
            revealButton.widthAnchor.constraint(equalToConstant: 44),
            revealButton.heightAnchor.constraint(equalToConstant: 44),
            valueView.heightAnchor.constraint(greaterThanOrEqualToConstant: 72),
            row.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
            row.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
            row.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
            row.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor),
        ])
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    func configure(
        field: MobileEntryEditorField,
        name: String?,
        value: String?,
        changed: @escaping (String?, String?) -> Void
    ) {
        self.changed = changed
        currentName = name
        currentValue = value
        sensitive = field.sensitive
        iconView.image = UIImage(systemName: field.systemImage)
        labelView.text = field.label
        nameField.text = name
        nameField.isHidden = !field.nameEditable
        labelView.isHidden = field.nameEditable
        let multiline = field.multiline || field.kind == .note
        valueField.isHidden = multiline
        valueView.isHidden = !multiline
        valueField.text = value ?? field.maskedValue
        valueView.text = value ?? field.maskedValue
        let editable = value != nil
        nameField.isEnabled = editable
        valueField.isEnabled = editable
        valueView.isEditable = editable
        valueField.isSecureTextEntry = field.sensitive
        valueField.textContentType = field.kind == .password ? .password : nil
        valueField.keyboardType = field.kind == .otpUri ? .URL : .default
        valueField.autocapitalizationType = field.sensitive ? .none : .sentences
        valueField.autocorrectionType = field.sensitive ? .no : .default
        valueView.autocapitalizationType = .sentences
        diagnosticView.text = field.diagnostic
        diagnosticView.isHidden = field.diagnostic == nil
        revealButton.isHidden = !field.sensitive
        revealButton.accessibilityLabel = "Reveal \(field.label)"
        nameField.accessibilityLabel = "\(field.label) name"
        valueField.accessibilityLabel = field.label
        valueView.accessibilityLabel = field.label
    }

    func focusValue() {
        if valueView.isHidden {
            valueField.becomeFirstResponder()
        } else {
            valueView.becomeFirstResponder()
        }
    }

    func clearSensitiveContent() {
        currentValue = nil
        valueField.text = nil
        valueView.text = nil
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        changed = nil
        currentName = nil
        currentValue = nil
        valueField.isSecureTextEntry = false
    }

    @objc private func textFieldChanged(_ sender: UITextField) {
        if sender === nameField {
            currentName = sender.text
        } else {
            currentValue = sender.text
        }
        changed?(currentName, currentValue)
    }

    func textViewDidChange(_ textView: UITextView) {
        currentValue = textView.text
        changed?(currentName, currentValue)
    }

    private func toggleReveal() {
        guard sensitive else { return }
        valueField.isSecureTextEntry.toggle()
        revealButton.configuration?.image = UIImage(
            systemName: valueField.isSecureTextEntry ? "eye" : "eye.slash"
        )
        revealButton.accessibilityLabel = valueField.isSecureTextEntry
            ? "Reveal field"
            : "Hide field"
    }
}

private struct AuthenticationFailure: Error, Sendable {
    let kind: MobileAuthenticationErrorKind
    let title: String
    let detail: String

    init(_ error: MobileAuthenticationFfiError) {
        switch error {
        case let .Failed(kind, title, detail):
            self.kind = kind
            self.title = title
            self.detail = detail
        }
    }

    static let unexpected = AuthenticationFailure(
        kind: .secureStorage,
        title: "Unlock Failed",
        detail: "IronStorage could not complete authentication."
    )

    static let unavailable = AuthenticationFailure(
        kind: .configuration,
        title: "Authentication Is Unavailable",
        detail: "Finish password-store setup before unlocking entries."
    )

    fileprivate init(kind: MobileAuthenticationErrorKind, title: String, detail: String) {
        self.kind = kind
        self.title = title
        self.detail = detail
    }
}

@MainActor
private extension UIViewController {
    func presentAuthenticationFailure(_ failure: AuthenticationFailure) {
        let alert = UIAlertController(
            title: failure.title,
            message: failure.detail,
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }
}

private struct HomeFailure: Error, Sendable {
    let kind: MobileHomeErrorKind
    let title: String
    let detail: String

    init(_ error: MobileHomeFfiError) {
        switch error {
        case let .Failed(kind, title, detail):
            self.kind = kind
            self.title = title
            self.detail = detail
        }
    }

    static let unexpected = HomeFailure(
        kind: .repository,
        title: "Remote Activity Failed",
        detail: "IronStorage could not load the storage-provided Home page."
    )

    private init(kind: MobileHomeErrorKind, title: String, detail: String) {
        self.kind = kind
        self.title = title
        self.detail = detail
    }
}

@MainActor
private final class MobileCommitActivityViewController: UITableViewController {
    private let commit: MobileHomeCommit

    init(commit: MobileHomeCommit, incoming: Bool) {
        self.commit = commit
        super.init(style: .insetGrouped)
        title = incoming ? "Incoming Commit" : "Outgoing Commit"
        navigationItem.largeTitleDisplayMode = .never
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    override func numberOfSections(in tableView: UITableView) -> Int { 2 }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        section == 0 ? 1 : max(commit.changes.count, 1)
    }

    override func tableView(
        _ tableView: UITableView,
        titleForHeaderInSection section: Int
    ) -> String? {
        section == 0 ? "Commit" : "Password Store Changes"
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
        var content = cell.defaultContentConfiguration()
        if indexPath.section == 0 {
            content.image = UIImage(systemName: commit.systemImage)
            content.text = commit.title
            content.secondaryText = "\(commit.detail)\n\(formattedDate)"
        } else if commit.changes.isEmpty {
            content.image = UIImage(systemName: "minus.circle")
            content.text = "No File Changes"
            content.secondaryText = "This commit does not change password-store files."
        } else {
            let change = commit.changes[indexPath.row]
            content.image = UIImage(systemName: change.systemImage)
            content.text = change.title
            content.secondaryText = change.detail
        }
        content.secondaryTextProperties.numberOfLines = 0
        cell.contentConfiguration = content
        cell.selectionStyle = .none
        return cell
    }

    private var formattedDate: String {
        Date(timeIntervalSince1970: TimeInterval(commit.timestamp)).formatted(
            date: .abbreviated,
            time: .shortened
        )
    }
}

@MainActor
private final class TokenUpdateViewController: UITableViewController {
    private let accountField = UITextField()
    private let tokenField = UITextField()
    private var updateTask: Task<Void, Never>?
    private var generation = 0
    private var isWorking = false

    init() {
        super.init(style: .insetGrouped)
        title = "Update Token"
        navigationItem.largeTitleDisplayMode = .never
        for field in [accountField, tokenField] {
            field.borderStyle = .none
            field.clearButtonMode = .whileEditing
            field.autocorrectionType = .no
            field.autocapitalizationType = .none
            field.adjustsFontForContentSizeCategory = true
            field.font = .preferredFont(forTextStyle: .body)
        }
        accountField.placeholder = "Account name"
        tokenField.placeholder = "New application token"
        tokenField.textContentType = .oneTimeCode
        tokenField.isSecureTextEntry = true
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    deinit {
        updateTask?.cancel()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        generation += 1
        updateTask?.cancel()
    }

    override func numberOfSections(in tableView: UITableView) -> Int { 2 }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        section == 0 ? 2 : 1
    }

    override func tableView(
        _ tableView: UITableView,
        titleForHeaderInSection section: Int
    ) -> String? {
        section == 0 ? "HTTPS Credential" : nil
    }

    override func tableView(
        _ tableView: UITableView,
        titleForFooterInSection section: Int
    ) -> String? {
        section == 0
            ? "The existing token is never displayed. Updating replaces it only in protected system storage."
            : nil
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
        if indexPath.section == 1 {
            cell.textLabel?.text = isWorking ? "Updating…" : "Replace Token"
            cell.textLabel?.textColor = isWorking ? .secondaryLabel : view.tintColor
            cell.textLabel?.textAlignment = .center
            cell.isUserInteractionEnabled = !isWorking
            return cell
        }
        let field = indexPath.row == 0 ? accountField : tokenField
        field.translatesAutoresizingMaskIntoConstraints = false
        field.isEnabled = !isWorking
        cell.contentView.addSubview(field)
        NSLayoutConstraint.activate([
            field.leadingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.leadingAnchor),
            field.trailingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.trailingAnchor),
            field.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 10),
            field.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -10)
        ])
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        guard indexPath.section == 1, !isWorking else { return }
        replaceToken()
    }

    private func replaceToken() {
        generation += 1
        let current = generation
        isWorking = true
        navigationItem.prompt = "Saving in protected system storage."
        tableView.reloadData()
        let account = accountField.text ?? ""
        let token = tokenField.text ?? ""
        updateTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    try replaceConfiguredMobileApplicationToken(
                        account: account,
                        applicationToken: token
                    )
                    return Result<Void, OnboardingFailure>.success(())
                } catch let error as MobileOnboardingFfiError {
                    return .failure(OnboardingFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard let self, current == generation else { return }
            isWorking = false
            navigationItem.prompt = nil
            tableView.reloadData()
            switch result {
            case .success:
                tokenField.text = nil
                let alert = UIAlertController(
                    title: "Token Updated",
                    message: "The new application token is ready for HTTPS Git operations.",
                    preferredStyle: .alert
                )
                alert.addAction(UIAlertAction(title: "Done", style: .default) { [weak self] _ in
                    self?.navigationController?.popViewController(animated: true)
                })
                present(alert, animated: true)
            case let .failure(failure):
                let alert = UIAlertController(
                    title: failure.title,
                    message: failure.detail,
                    preferredStyle: .alert
                )
                alert.addAction(UIAlertAction(title: "OK", style: .cancel))
                present(alert, animated: true)
            }
        }
    }
}

@MainActor
private final class OnboardingViewController: UITableViewController {
    private let serverField = UITextField()
    private let accountField = UITextField()
    private let repositoryField = UITextField()
    private let tokenField = UITextField()
    private var branches: [String] = []
    private var selectedBranch = 0
    private var operation: MobileOnboardingOperation?
    private var workTask: Task<Void, Never>?
    private var progressTask: Task<Void, Never>?
    private var generation = 0
    private var isWorking = false

    init() {
        super.init(style: .insetGrouped)
        title = "Connect Store"
        navigationItem.largeTitleDisplayMode = .never
        configureFields()
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }

    deinit {
        operation?.cancel()
        workTask?.cancel()
        progressTask?.cancel()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        cancelWork()
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        branches.isEmpty ? 2 : 3
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        switch section {
        case 0: 4
        case 1: 1
        default: 2
        }
    }

    override func tableView(
        _ tableView: UITableView,
        titleForHeaderInSection section: Int
    ) -> String? {
        switch section {
        case 0: "HTTPS Repository"
        case 1: branches.isEmpty ? nil : "Remote"
        default: "Local Clone"
        }
    }

    override func tableView(
        _ tableView: UITableView,
        titleForFooterInSection section: Int
    ) -> String? {
        section == 0 ? "The application token is stored in protected system storage, never in configuration." : nil
    }

    override func tableView(
        _ tableView: UITableView,
        cellForRowAt indexPath: IndexPath
    ) -> UITableViewCell {
        if indexPath.section == 0 {
            return fieldCell(indexPath.row)
        }
        let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
        if branches.isEmpty {
            cell.textLabel?.text = "Discover Branches"
            cell.textLabel?.textColor = view.tintColor
            cell.accessoryType = .disclosureIndicator
        } else if indexPath.section == 1 {
            cell.textLabel?.text = "Branch"
            cell.detailTextLabel?.text = branches[selectedBranch]
            cell.accessoryType = .disclosureIndicator
        } else if indexPath.row == 0 {
            cell.textLabel?.text = "Clone Password Store"
            cell.textLabel?.textColor = view.tintColor
        } else {
            cell.textLabel?.text = "Use Existing Clone"
            cell.textLabel?.textColor = .secondaryLabel
        }
        cell.isUserInteractionEnabled = !isWorking
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        guard !isWorking else { return }
        if branches.isEmpty, indexPath.section == 1 {
            discoverBranches()
        } else if indexPath.section == 1 {
            chooseBranch(from: tableView.cellForRow(at: indexPath))
        } else if indexPath.section == 2 {
            runSetup(useExisting: indexPath.row == 1)
        }
    }

    private func configureFields() {
        for field in [serverField, accountField, repositoryField, tokenField] {
            field.borderStyle = .none
            field.clearButtonMode = .whileEditing
            field.autocorrectionType = .no
            field.returnKeyType = .next
            field.adjustsFontForContentSizeCategory = true
            field.font = .preferredFont(forTextStyle: .body)
        }
        serverField.placeholder = "https://git.example.com"
        serverField.textContentType = .URL
        serverField.keyboardType = .URL
        serverField.autocapitalizationType = .none
        accountField.placeholder = "Account name"
        accountField.autocapitalizationType = .none
        repositoryField.placeholder = "owner/password-store"
        repositoryField.autocapitalizationType = .none
        tokenField.placeholder = "Application token"
        tokenField.textContentType = .oneTimeCode
        tokenField.isSecureTextEntry = true
    }

    private func fieldCell(_ row: Int) -> UITableViewCell {
        let field = [serverField, accountField, repositoryField, tokenField][row]
        let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
        field.translatesAutoresizingMaskIntoConstraints = false
        cell.contentView.addSubview(field)
        NSLayoutConstraint.activate([
            field.leadingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.leadingAnchor),
            field.trailingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.trailingAnchor),
            field.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 10),
            field.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -10)
        ])
        return cell
    }

    private func makeOperation() throws -> MobileOnboardingOperation {
        try mobileOnboardingOperation(
            serverUrl: serverField.text ?? "",
            account: accountField.text ?? "",
            repositoryPath: repositoryField.text ?? "",
            applicationToken: tokenField.text ?? ""
        )
    }

    private func discoverBranches() {
        do {
            let operation = try makeOperation()
            begin(operation, title: "Checking Repository") { operation in
                let discovery = try operation.discover()
                return .discovered(discovery)
            }
        } catch {
            present(error)
        }
    }

    private func runSetup(useExisting: Bool) {
        guard branches.indices.contains(selectedBranch) else { return }
        do {
            let operation = try makeOperation()
            let branch = branches[selectedBranch]
            begin(operation, title: useExisting ? "Opening Store" : "Cloning Store") { operation in
                let outcome = try operation.setup(branch: branch, useExisting: useExisting)
                return .completed(outcome)
            }
        } catch {
            present(error)
        }
    }

    private func begin(
        _ operation: MobileOnboardingOperation,
        title: String,
        work: @escaping @Sendable (MobileOnboardingOperation) throws -> WorkResult
    ) {
        cancelWork()
        generation += 1
        let current = generation
        self.operation = operation
        isWorking = true
        showProgress(title: title, detail: "Preparing secure storage.")
        tableView.reloadData()
        progressTask = Task { [weak self] in
            while !Task.isCancelled {
                do {
                    try await Task.sleep(for: .milliseconds(150))
                } catch {
                    return
                }
                guard !Task.isCancelled, let self, current == generation else { return }
                let progress = operation.progress()
                showProgress(title: progress.title, detail: progress.detail)
            }
        }
        workTask = Task { [weak self] in
            let result = await Task.detached(priority: .userInitiated) {
                do {
                    return Result<WorkResult, OnboardingFailure>.success(try work(operation))
                } catch let error as MobileOnboardingFfiError {
                    return .failure(OnboardingFailure(error))
                } catch {
                    return .failure(.unexpected)
                }
            }.value
            guard let self, current == generation else { return }
            finish(result)
        }
    }

    private func finish(_ result: Result<WorkResult, OnboardingFailure>) {
        progressTask?.cancel()
        progressTask = nil
        operation = nil
        isWorking = false
        navigationItem.titleView = nil
        navigationItem.prompt = nil
        switch result {
        case let .success(.discovered(discovery)):
            branches = discovery.branches
            selectedBranch = min(Int(discovery.selectedBranch), max(branches.count - 1, 0))
            tableView.reloadData()
        case let .success(.completed(outcome)):
            tokenField.text = nil
            let alert = UIAlertController(title: outcome.title, message: outcome.detail, preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "Done", style: .default) { [weak self] _ in
                self?.navigationController?.popViewController(animated: true)
            })
            present(alert, animated: true)
        case let .failure(failure):
            tableView.reloadData()
            present(failure)
        }
    }

    private func chooseBranch(from source: UIView?) {
        let sheet = UIAlertController(title: "Branch", message: nil, preferredStyle: .actionSheet)
        for (index, branch) in branches.enumerated() {
            sheet.addAction(UIAlertAction(title: branch, style: .default) { [weak self] _ in
                self?.selectedBranch = index
                self?.tableView.reloadSections(IndexSet(integer: 1), with: .automatic)
            })
        }
        sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        sheet.popoverPresentationController?.sourceView = source
        sheet.popoverPresentationController?.sourceRect = source?.bounds ?? .zero
        present(sheet, animated: true)
    }

    private func present(_ error: Error) {
        if let error = error as? MobileOnboardingFfiError {
            present(OnboardingFailure(error))
        } else {
            present(.unexpected)
        }
    }

    private func present(_ failure: OnboardingFailure) {
        let alert = UIAlertController(title: failure.title, message: failure.detail, preferredStyle: .alert)
        if failure.kind == .existingClone, !branches.isEmpty {
            alert.addAction(UIAlertAction(title: "Use Existing", style: .default) { [weak self] _ in
                self?.runSetup(useExisting: true)
            })
        }
        alert.addAction(UIAlertAction(title: "OK", style: .cancel))
        present(alert, animated: true)
    }

    private func showProgress(title: String, detail: String) {
        let spinner = UIActivityIndicatorView(style: .medium)
        spinner.startAnimating()
        spinner.accessibilityLabel = "In progress"
        let label = UILabel()
        label.text = title
        label.font = .preferredFont(forTextStyle: .headline)
        label.adjustsFontForContentSizeCategory = true
        let stack = UIStackView(arrangedSubviews: [spinner, label])
        stack.spacing = 8
        navigationItem.titleView = stack
        navigationItem.prompt = detail
    }

    private func cancelWork() {
        generation += 1
        operation?.cancel()
        operation = nil
        workTask?.cancel()
        workTask = nil
        progressTask?.cancel()
        progressTask = nil
        navigationItem.prompt = nil
        navigationItem.titleView = nil
        isWorking = false
    }
}

private enum WorkResult: Sendable {
    case discovered(MobileOnboardingDiscovery)
    case completed(MobileOnboardingOutcome)
}

private struct OnboardingFailure: Error, Sendable {
    let kind: MobileOnboardingErrorKind?
    let title: String
    let detail: String

    init(_ error: MobileOnboardingFfiError) {
        switch error {
        case let .Failed(kind, title, detail):
            self.kind = kind
            self.title = title
            self.detail = detail
        }
    }

    static let unexpected = OnboardingFailure(
        kind: nil,
        title: "Setup Failed",
        detail: "IronStorage could not complete setup."
    )

    private init(kind: MobileOnboardingErrorKind?, title: String, detail: String) {
        self.kind = kind
        self.title = title
        self.detail = detail
    }
}
