//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
public import LibSignalClient
import Lottie
import PureLayout
import SafariServices
import SignalServiceKit
import UIKit

public class FingerprintViewController: OWSViewController, OWSNavigationChildController {

    public class func present(
        for theirAci: Aci?,
        from viewController: UIViewController,
    ) {
        struct DatabaseResult {
            let theirAci: Aci
            let theirName: String
            let theirVerificationState: VerificationState
            let fingerprint: CombinedFingerprints
            let keyTransparencyState: KeyTransparencyState
            let shouldShowKeyTransparencyEducation: Bool
        }

        let contactManager = SSKEnvironment.shared.contactManagerRef
        let db = DependenciesBridge.shared.db
        let identityManager = DependenciesBridge.shared.identityManager
        let keyTransparencyManager = DependenciesBridge.shared.keyTransparencyManager
        let keyTransparencyStore = KeyTransparencyStore()
        let tsAccountManager = DependenciesBridge.shared.tsAccountManager

        let databaseResult = db.read { tx -> DatabaseResult? in
            guard let theirAci else {
                return nil
            }

            let theirAddress = SignalServiceAddress(theirAci)
            let theirName = contactManager.displayName(for: theirAddress, tx: tx).resolvedValue()
            let theirVerificationState = identityManager.verificationState(for: theirAddress, tx: tx)

            guard
                let theirRecipientIdentity = identityManager.recipientIdentity(for: theirAddress, tx: tx),
                let theirAciIdentityKey = try? theirRecipientIdentity.identityKeyObject,
                let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx),
                let myAciIdentityKey = identityManager.identityKeyPair(for: .aci, tx: tx)?.keyPair.identityKey
            else {
                return nil
            }

            let keyTransparencyIsEnabled = keyTransparencyManager.isEnabled(tx: tx)
            let keyTransparencyCheckParams = keyTransparencyManager.prepareCheck(
                aci: theirAci,
                localIdentifiers: localIdentifiers,
                tx: tx,
            )
            let keyTransparencyShouldShowEducation = keyTransparencyStore.shouldShowFirstTimeEducation(tx: tx)

            return DatabaseResult(
                theirAci: theirAci,
                theirName: theirName,
                theirVerificationState: theirVerificationState,
                fingerprint: CombinedFingerprints(
                    local: .derive(forAci: localIdentifiers.aci, identityKey: myAciIdentityKey),
                    remote: .derive(forAci: theirAci, identityKey: theirAciIdentityKey),
                ),
                keyTransparencyState: KeyTransparencyState(
                    isEnabled: keyTransparencyIsEnabled,
                    checkParams: keyTransparencyCheckParams,
                    viewInitialState: keyTransparencyCheckParams == nil ? .unableToVerify : .readyToVerify,
                ),
                shouldShowKeyTransparencyEducation: keyTransparencyShouldShowEducation,
            )
        }

        guard let databaseResult else {
            let actionSheet = ActionSheetController(message: OWSLocalizedString(
                "CANT_VERIFY_IDENTITY_EXCHANGE_MESSAGES",
                comment: "Alert shown when the user needs to exchange messages to see the safety number.",
            ))

            actionSheet.addAction(.init(title: CommonStrings.learnMore, style: .default, handler: { _ in
                guard let vc = CurrentAppContext().frontmostViewController() else {
                    return
                }
                Self.showUrl(URL.Support.safetyNumbers, from: vc)
            }))
            actionSheet.addAction(OWSActionSheets.cancelAction)

            viewController.presentActionSheet(actionSheet)
            return
        }

        let fingerprintViewController = FingerprintViewController(
            recipientAci: databaseResult.theirAci,
            recipientName: databaseResult.theirName,
            recipientVerificationState: databaseResult.theirVerificationState,
            fingerprint: databaseResult.fingerprint,
            keyTransparencyState: databaseResult.keyTransparencyState,
            deps: FingerprintViewController.Deps(
                db: db,
                identityManager: identityManager,
                keyTransparencyManager: keyTransparencyManager,
            ),
        )
        let navigationController = OWSNavigationController(rootViewController: fingerprintViewController)

        if databaseResult.shouldShowKeyTransparencyEducation {
            let educationSheet = KeyTransparencyFirstTimeEducationHeroSheet {
                db.write { tx in
                    keyTransparencyStore.setShouldShowFirstTimeEducation(false, tx: tx)
                }

                viewController.present(navigationController, animated: true)
            }
            viewController.present(educationSheet, animated: true)
        } else {
            viewController.present(navigationController, animated: true)
        }
    }

    // MARK: -

    fileprivate struct Deps {
        let db: DB
        let identityManager: OWSIdentityManager
        let keyTransparencyManager: KeyTransparencyManager
    }

    fileprivate struct KeyTransparencyState {
        let isEnabled: Bool
        let checkParams: KeyTransparencyManager.CheckParams?
        let viewInitialState: KeyTransparencyView.State
    }

    private let recipientAci: Aci
    private let recipientName: String
    private var recipientVerificationState: VerificationState {
        didSet {
            self.fingerprintCard.theirVerificationState = recipientVerificationState
        }
    }

    private let fingerprint: CombinedFingerprints
    private let keyTransparencyState: KeyTransparencyState

    private let deps: Deps?

    fileprivate init(
        recipientAci: Aci,
        recipientName: String,
        recipientVerificationState: VerificationState,
        fingerprint: CombinedFingerprints,
        keyTransparencyState: KeyTransparencyState,
        deps: Deps?,
    ) {
        // We snapshot state when we present this view and dismiss the view when
        // there's an identity change, to avoid edge cases related to state
        // changing while this view is presented. (E.g., you verified them on
        // another device; you learned their identity key changed; etc.)
        self.recipientAci = recipientAci
        self.recipientName = recipientName
        self.recipientVerificationState = recipientVerificationState
        self.fingerprint = fingerprint
        self.keyTransparencyState = keyTransparencyState

        self.deps = deps

        super.init()

        title = NSLocalizedString("PRIVACY_VERIFICATION_TITLE", comment: "Navbar title")
        navigationItem.rightBarButtonItem = .doneButton(dismissingFrom: self)

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(self.identityStateDidChange(_:)),
            name: .identityStateDidChange,
            object: nil,
        )
    }

    public var preferredNavigationBarStyle: OWSNavigationBarStyle {
        return .solid
    }

    override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return .portrait
    }

    override public func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .Signal.groupedBackground
        configureUI()
    }

    @objc
    private func identityStateDidChange(_ notification: Notification) {
        let databaseStorage = SSKEnvironment.shared.databaseStorageRef
        let identityManager = DependenciesBridge.shared.identityManager

        let recipientIdentity = databaseStorage.read { tx in
            return identityManager.recipientIdentity(for: SignalServiceAddress(recipientAci), tx: tx)
        }

        // Ensure the identity key hasn't changed (this influences the Safety
        // Number). If it changes, we want to dismiss the view so that we can
        // present it again with the new Safety Number.
        guard
            let recipientIdentity,
            (try? recipientIdentity.identityKeyObject) == fingerprint.remote.identityKey
        else {
            self.dismiss(animated: true)
            return
        }

        // The verification state may change (e.g., after scanning a QR code). We
        // want the UI to reflect the latest value.
        self.recipientVerificationState = VerificationState(recipientIdentity.verificationState)
    }

    // MARK: UI

    private lazy var fingerprintCard = FingerprintCard(
        fingerprint: fingerprint,
        theirVerificationState: recipientVerificationState,
        controller: self,
    )

    private lazy var instructionsTextView: UITextView = {
        let instructions = String.nonPluralLocalizedStringWithFormat(
            OWSLocalizedString(
                "VERIFY_SAFETY_NUMBER_INSTRUCTIONS",
                comment: "Instructions for verifying your safety number. Embeds {{contact's name}}",
            ),
            self.recipientName,
        )
        // Link doesn't matter, we will override tap behavior.
        let learnMore = CommonStrings.learnMore.styled(with: .link(URL(string: "https://signal.org")!))

        let textView = LinkingTextView { [weak self] in
            self?.didTapSafetyNumbersLearnMore()
        }
        textView.attributedText = NSAttributedString.composed(of: [
            instructions,
            " ",
            learnMore,
        ]).styled(
            with: .font(.dynamicTypeFootnote),
            .color(.Signal.secondaryLabel),
            .alignment(.center),
        )
        textView.linkTextAttributes = [.foregroundColor: UIColor.Signal.label]

        return textView
    }()

    private lazy var keyTransparencyView = KeyTransparencyView(
        initialState: keyTransparencyState.viewInitialState,
        controller: self,
    )

    private func configureUI() {
        let scrollView = UIScrollView()
        view.addSubview(scrollView)
        scrollView.autoPinEdgesToSuperviewEdges()

        let containerView = UIView()
        scrollView.addSubview(containerView)
        containerView.autoPinEdges(toEdgesOf: scrollView)
        containerView.autoPinWidth(toWidthOf: view)

        containerView.addSubview(fingerprintCard)
        containerView.addSubview(instructionsTextView)
        if keyTransparencyState.isEnabled {
            containerView.addSubview(keyTransparencyView)
        }

        fingerprintCard.autoPinEdge(toSuperviewSafeArea: .top, withInset: 10)
        fingerprintCard.autoPinWidth(toWidthOf: containerView, offset: -.scaleFromIPhone5To7Plus(60, 105))
        fingerprintCard.autoHCenterInSuperview()

        instructionsTextView.autoPinEdge(.top, to: .bottom, of: fingerprintCard, withOffset: 24)
        instructionsTextView.autoPinEdge(.leading, to: .leading, of: containerView, withOffset: .scaleFromIPhone5To7Plus(18, 28))
        instructionsTextView.autoPinEdge(.trailing, to: .trailing, of: containerView, withOffset: -.scaleFromIPhone5To7Plus(18, 28))

        if keyTransparencyState.isEnabled {
            keyTransparencyView.autoPinEdge(.top, to: .bottom, of: instructionsTextView, withOffset: 44)
            keyTransparencyView.autoPinEdge(.leading, to: .leading, of: containerView, withOffset: 16)
            keyTransparencyView.autoPinEdge(.trailing, to: .trailing, of: containerView, withOffset: -16)
            keyTransparencyView.autoPinEdge(.bottom, to: .bottom, of: scrollView, withOffset: -8)
        } else {
            instructionsTextView.autoPinEdge(.bottom, to: .bottom, of: scrollView, withOffset: -8)
        }
    }

    // MARK: - Fingerprint Card

    private final class FingerprintCard: UIView {
        private let fingerprint: CombinedFingerprints
        var theirVerificationState: VerificationState {
            didSet {
                updateVerifyUnverifyButtonTitle()
            }
        }

        private weak var controller: FingerprintViewController?

        init(
            fingerprint: CombinedFingerprints,
            theirVerificationState: VerificationState,
            controller: FingerprintViewController,
        ) {
            self.fingerprint = fingerprint
            self.theirVerificationState = theirVerificationState
            self.controller = controller
            super.init(frame: .zero)

            layer.cornerRadius = Constants.cornerRadius

            self.backgroundColor = UIColor(rgbHex: 0x506ecd)

            addSubview(shareButton)
            addSubview(qrCodeView)
            addSubview(safetyNumberLabel)
            addSubview(verifyUnverifyButton)
            updateVerifyUnverifyButtonTitle()

            shareButton.autoPinEdge(.top, to: .top, of: self, withOffset: 16)
            shareButton.autoPinEdge(.trailing, to: .trailing, of: self, withOffset: -16)

            qrCodeView.autoPinEdge(.top, to: .bottom, of: shareButton, withOffset: 8)
            // Set a minimum horizontal margin
            qrCodeView.autoPinEdge(.leading, to: .leading, of: self, withOffset: .scaleFromIPhone5To7Plus(44, 64), relation: .greaterThanOrEqual)
            qrCodeView.autoPinEdge(.trailing, to: .trailing, of: self, withOffset: -.scaleFromIPhone5To7Plus(44, 64), relation: .lessThanOrEqual)
            qrCodeView.autoHCenterInSuperview()

            safetyNumberLabel.autoPinEdge(.top, to: .bottom, of: qrCodeView, withOffset: 30)
            safetyNumberLabel.autoPinEdge(.leading, to: .leading, of: self, withOffset: .scaleFromIPhone5To7Plus(20, 35), relation: .greaterThanOrEqual)
            safetyNumberLabel.autoPinEdge(.trailing, to: .trailing, of: self, withOffset: -.scaleFromIPhone5To7Plus(20, 35), relation: .lessThanOrEqual)
            safetyNumberLabel.autoHCenterInSuperview()

            verifyUnverifyButton.autoPinEdge(.top, to: .bottom, of: safetyNumberLabel, withOffset: 30)
            verifyUnverifyButton.autoPinEdge(.leading, to: .leading, of: self, withOffset: .scaleFromIPhone5To7Plus(20, 35), relation: .greaterThanOrEqual)
            verifyUnverifyButton.autoPinEdge(.trailing, to: .trailing, of: self, withOffset: -.scaleFromIPhone5To7Plus(20, 35), relation: .lessThanOrEqual)
            verifyUnverifyButton.autoHCenterInSuperview()
            verifyUnverifyButton.autoPinEdge(.bottom, to: .bottom, of: self, withOffset: -20)

            // Cap QR code width to the width of the safety number
            // Prevents it from being too large on iPad
            let qrCodeWidthConstraint = qrCodeView.widthAnchor.constraint(equalTo: safetyNumberLabel.widthAnchor)
            qrCodeWidthConstraint.priority = .defaultHigh
            qrCodeWidthConstraint.autoInstall()
            safetyNumberLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
        }

        required init?(coder: NSCoder) {
            fatalError()
        }

        private lazy var shareButton: UIButton = {
            let button = UIButton()
            button.setTemplateImage(
                UIImage(named: "share"),
                tintColor: .white,
            )
            button.addTarget(self, action: #selector(didTapShare), for: .touchUpInside)
            return button
        }()

        private lazy var qrCodeView: UIView = {
            let containerView = UIView()
            containerView.backgroundColor = .white
            containerView.layer.cornerRadius = Constants.cornerRadius
            containerView.layer.masksToBounds = true

            let fingerprintImageView = UIImageView()
            fingerprintImageView.image = fingerprint.image()
            // Don't antialias QR Codes.
            fingerprintImageView.layer.magnificationFilter = .nearest
            fingerprintImageView.layer.minificationFilter = .nearest
            fingerprintImageView.setCompressionResistanceLow()
            containerView.addSubview(fingerprintImageView)
            fingerprintImageView.autoPin(toAspectRatio: 1)
            fingerprintImageView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(margin: 20), excludingEdge: .bottom)

            let scanLabel = UILabel()
            scanLabel.text = NSLocalizedString("PRIVACY_TAP_TO_SCAN", comment: "Button that shows the 'scan with camera' view.")
            scanLabel.font = .systemFont(ofSize: .scaleFromIPhone5To7Plus(13, 15))
            scanLabel.textColor = .Signal.label.resolvedColor(with: UITraitCollection(userInterfaceStyle: .light))
            scanLabel.numberOfLines = 0
            scanLabel.textAlignment = .center
            containerView.addSubview(scanLabel)
            scanLabel.autoPinWidthToSuperviewMargins()
            scanLabel.autoPinEdge(.top, to: .bottom, of: fingerprintImageView, withOffset: 12)
            scanLabel.autoPinEdge(.bottom, to: .bottom, of: containerView, withOffset: -14)

            containerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapToScan)))

            return containerView
        }()

        private lazy var safetyNumberLabel: UILabel = {
            let label = UILabel()
            label.text = fingerprint.displayableText()
            label.font = UIFont(name: "Menlo-Regular", size: 23)
            label.textAlignment = .center
            label.textColor = .white
            label.numberOfLines = 3
            label.lineBreakMode = .byTruncatingTail
            label.adjustsFontSizeToFitWidth = true
            label.isUserInteractionEnabled = true
            label.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapSafetyNumber)))
            return label
        }()

        private lazy var verifyUnverifyButton: UIButton = {
            let lightTheme = UITraitCollection(userInterfaceStyle: .light)

            var configuration = UIButton.Configuration.filled()
            configuration.titleAlignment = .center
            configuration.titleTextAttributesTransformer = .defaultFont(.dynamicTypeSubheadlineClamped.semibold())
            configuration.baseBackgroundColor = .Signal.background.resolvedColor(with: lightTheme)
            configuration.baseForegroundColor = .Signal.label.resolvedColor(with: lightTheme)
            configuration.contentInsets = NSDirectionalEdgeInsets(hMargin: 16, vMargin: 12)
            configuration.cornerStyle = .capsule

            return UIButton(
                configuration: configuration,
                primaryAction: UIAction { [weak self] _ in
                    self?.controller?.didTapVerifyUnverify()
                },
            )
        }()

        private func updateVerifyUnverifyButtonTitle() {
            var configuration = self.verifyUnverifyButton.configuration
            switch theirVerificationState {
            case .verified:
                configuration?.title = OWSLocalizedString(
                    "PRIVACY_UNVERIFY_BUTTON",
                    comment: "Button that lets user mark another user's identity as unverified.",
                )
            case .noLongerVerified, .implicit:
                configuration?.title = OWSLocalizedString(
                    "PRIVACY_VERIFY_BUTTON",
                    comment: "Button that lets user mark another user's identity as verified.",
                )
            }
            self.verifyUnverifyButton.configuration = configuration
        }

        @objc
        func didTapToScan() {
            controller?.didTapToScan()
        }

        @objc
        func didTapShare() {
            controller?.shareFingerprint(from: shareButton)
        }

        @objc
        func didTapSafetyNumber() {
            controller?.shareFingerprint(from: safetyNumberLabel)
        }

        private enum Constants {
            static let cornerRadius: CGFloat = 18
        }
    }

    // MARK: -

    fileprivate final class KeyTransparencyView: UIView {
        enum State {
            case unableToVerify
            case readyToVerify
            case verifying
            case verifiedSuccess
            case verifiedFailure
        }

        var state: State {
            didSet { updateForCurrentState() }
        }

        private weak var controller: FingerprintViewController?

        init(
            initialState: State,
            controller: FingerprintViewController,
        ) {
            self.state = initialState
            self.controller = controller
            super.init(frame: .zero)

            addSubview(sectionHeaderLabel)
            addSubview(verifyButton)
            addSubview(footerTextView)

            sectionHeaderLabel.autoPinEdge(toSuperviewEdge: .top, withInset: 12)
            sectionHeaderLabel.autoPinEdge(toSuperviewEdge: .leading, withInset: 26)
            sectionHeaderLabel.autoPinEdge(toSuperviewEdge: .trailing, withInset: 26)

            verifyButton.autoPinEdge(.top, to: .bottom, of: sectionHeaderLabel, withOffset: 10)
            verifyButton.autoPinEdge(toSuperviewEdge: .leading, withInset: 16)
            verifyButton.autoPinEdge(toSuperviewEdge: .trailing, withInset: 16)

            footerTextView.autoPinEdge(.top, to: .bottom, of: verifyButton, withOffset: 12)
            footerTextView.autoPinEdge(toSuperviewEdge: .leading, withInset: 32)
            footerTextView.autoPinEdge(toSuperviewEdge: .trailing, withInset: 32)
            footerTextView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 24)

            updateForCurrentState()
        }

        @available(*, unavailable)
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }

        private func updateForCurrentState() {
            let leadingView: UIView
            let titleText: String
            let foregroundColor: UIColor
            let showChevron: Bool
            switch state {
            case .readyToVerify:
                leadingView = verifyButtonLeadingViewKey
                titleText = OWSLocalizedString(
                    "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_BUTTON_VERIFY",
                    comment: "Title for a button offering automatic key verification.",
                )
                foregroundColor = .Signal.label
                showChevron = false
            case .verifying:
                leadingView = verifyButtonLeadingViewSpinner
                titleText = OWSLocalizedString(
                    "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_BUTTON_VERIFYING",
                    comment: "Title for a button while automatic key verification is ongoing.",
                )
                foregroundColor = .Signal.label
                showChevron = false
            case .verifiedSuccess:
                leadingView = verifyButtonLeadingViewSuccess
                titleText = OWSLocalizedString(
                    "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_BUTTON_VERIFY_SUCCESS",
                    comment: "Title for a button when automatic key verification succeeds.",
                )
                foregroundColor = .Signal.label
                showChevron = true
            case .verifiedFailure, .unableToVerify:
                leadingView = verifyButtonLeadingViewFailure
                titleText = OWSLocalizedString(
                    "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_BUTTON_VERIFY_FAILURE",
                    comment: "Title for a button when automatic key verification fails.",
                )
                foregroundColor = .Signal.secondaryLabel
                showChevron = true
            }

            for view in verifyButtonLeadingViews {
                view.isHidden = view !== leadingView
            }
            verifyButton.configuration!.title = titleText
            verifyButton.configuration!.baseForegroundColor = foregroundColor
            if showChevron {
                verifyButton.configuration!.image = UIImage(named: "chevron-right-20")!
                verifyButton.contentHorizontalAlignment = .fill
            } else {
                verifyButton.configuration!.image = nil
                verifyButton.contentHorizontalAlignment = .leading
            }
        }

        // MARK: - Views

        private static let leadingViewSize: CGFloat = 24

        private lazy var sectionHeaderLabel: UILabel = {
            let label = UILabel()
            label.text = OWSLocalizedString(
                "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_HEADER",
                comment: "Header for automatic key verification",
            )
            label.font = .dynamicTypeBody.semibold()
            label.textColor = .Signal.label
            label.numberOfLines = 0
            return label
        }()

        private lazy var verifyButtonLeadingViewKey: UIImageView = {
            let imageView = UIImageView(image: UIImage(named: "key")!)
            imageView.tintColor = .Signal.label
            return imageView
        }()

        private lazy var verifyButtonLeadingViewSpinner: UIActivityIndicatorView = {
            let view = UIActivityIndicatorView(style: .medium)
            view.startAnimating()
            return view
        }()

        private lazy var verifyButtonLeadingViewSuccess: UIImageView = {
            let imageView = UIImageView(image: Theme.iconImage(.checkCircleFill))
            imageView.tintColor = .Signal.green
            return imageView
        }()

        private lazy var verifyButtonLeadingViewFailure: UIImageView = {
            let imageView = UIImageView(image: UIImage(named: "info")!)
            imageView.tintColor = .Signal.secondaryLabel
            return imageView
        }()

        private var verifyButtonLeadingViews: [UIView] {
            [
                verifyButtonLeadingViewKey,
                verifyButtonLeadingViewSpinner,
                verifyButtonLeadingViewSuccess,
                verifyButtonLeadingViewFailure,
            ]
        }

        private lazy var verifyButton: UIButton = {
            // Define overall insets for the button, with extra inset at the
            // leading edge since we'll be manually overlaying a view there.
            let inset: CGFloat = 16
            var buttonInsets = NSDirectionalEdgeInsets(margin: inset)
            buttonInsets.leading += Self.leadingViewSize + 12

            // This configuration is updated in updateForCurrentState() as well.
            var configuration = UIButton.Configuration.filled()
            configuration.imagePadding = 12
            configuration.imagePlacement = .trailing
            configuration.contentInsets = buttonInsets
            configuration.baseBackgroundColor = .Signal.tertiaryBackground
            configuration.cornerStyle = .capsule
            configuration.titleTextAttributesTransformer = .defaultFont(.dynamicTypeBody)

            let button = UIButton(
                configuration: configuration,
                primaryAction: UIAction { [weak self] _ in
                    guard let self else { return }
                    controller?.didTapKeyTransparencyButton(state: state)
                },
            )

            for view in verifyButtonLeadingViews {
                button.addSubview(view)
                view.autoSetDimensions(to: .square(Self.leadingViewSize))
                view.autoPinEdge(.leading, to: .leading, of: button, withOffset: inset)
                view.autoVCenterInSuperview()
            }

            return button
        }()

        private lazy var footerTextView: LinkingTextView = {
            let textView = LinkingTextView { [weak self] in
                self?.controller?.didTapKeyTransparencyLearnMore()
            }

            let footerText = OWSLocalizedString(
                "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_FOOTER",
                comment: "Footer explaining that automatic verification is not available for all chats",
            )

            // Link doesn't matter, we override tap behavior
            let learnMoreLink = CommonStrings.learnMore.styled(with: .link(URL(string: "https://signal.org")!))

            textView.attributedText = NSAttributedString.composed(of: [
                footerText,
                " ",
                learnMoreLink,
            ]).styled(
                with: .font(.dynamicTypeCaption1),
                .color(.Signal.secondaryLabel),
            )
            textView.linkTextAttributes = [.foregroundColor: UIColor.Signal.label]

            return textView
        }()
    }

    // MARK: -

    private func didTapSafetyNumbersLearnMore() {
        Self.showUrl(URL.Support.safetyNumbers, from: self)
    }

    fileprivate func didTapKeyTransparencyLearnMore() {
        Self.showUrl(URL.Support.keyTransparency, from: self)
    }

    fileprivate static func showUrl(_ url: URL, from viewController: UIViewController) {
        let safariVC = SFSafariViewController(url: url)
        viewController.present(safariVC, animated: true)
    }

    fileprivate func didTapVerifyUnverify() {
        guard let deps else { return }

        deps.db.write { tx in
            let identityKey = fingerprint.remote.identityKey

            let newVerificationState: VerificationState
            switch recipientVerificationState {
            case .verified:
                newVerificationState = .implicit(isAcknowledged: false)
            case .noLongerVerified, .implicit:
                newVerificationState = .verified
            }

            deps.identityManager.saveIdentityKey(
                identityKey,
                for: recipientAci,
                shouldUpdateStorageService: true,
                tx: tx,
            )
            _ = deps.identityManager.setVerificationState(
                newVerificationState,
                of: identityKey.publicKey.keyBytes,
                for: SignalServiceAddress(recipientAci),
                isUserInitiatedChange: true,
                tx: tx,
            )
        }

        dismiss(animated: true)
    }

    private func shareFingerprint(from fromView: UIView) {
        let compareActivity = CompareSafetyNumbersActivity(delegate: self)

        let shareFormat = NSLocalizedString(
            "SAFETY_NUMBER_SHARE_FORMAT",
            comment: "Snippet to share {{safety number}} with a friend. sent e.g. via SMS",
        )
        let shareString = String.nonPluralLocalizedStringWithFormat(shareFormat, fingerprint.displayableText())

        let activityController = UIActivityViewController(
            activityItems: [shareString],
            applicationActivities: [compareActivity],
        )

        if let popoverPresentationController = activityController.popoverPresentationController {
            popoverPresentationController.sourceView = fromView
        }

        // This value was extracted by inspecting `activityType` in the activityController.completionHandler
        let iCloudActivityType = "com.apple.CloudDocsUI.AddToiCloudDrive"
        activityController.excludedActivityTypes = [
            .postToFacebook,
            .postToWeibo,
            .airDrop,
            .postToTwitter,
            .init(rawValue: iCloudActivityType), // This isn't being excluded. RADAR https://openradar.appspot.com/27493621
        ]

        present(activityController, animated: true)
    }

    fileprivate func didTapToScan() {
        let viewController = FingerprintScanViewController(
            recipientAci: self.recipientAci,
            recipientName: self.recipientName,
            fingerprint: self.fingerprint,
        )
        navigationController?.pushViewController(viewController, animated: true)
    }

    fileprivate func didTapKeyTransparencyButton(state: KeyTransparencyView.State) {
        owsPrecondition(keyTransparencyState.isEnabled)

        switch state {
        case .unableToVerify:
            present(KeyTransparencyNotAvailableHeroSheet(), animated: true)
        case .readyToVerify:
            guard
                let deps,
                let checkParams = keyTransparencyState.checkParams
            else { return }

            keyTransparencyView.state = .verifying
            Task { @MainActor [weak self] in
                do {
                    try await deps.keyTransparencyManager.performCheck(params: checkParams)
                    self?.keyTransparencyView.state = .verifiedSuccess
                } catch {
                    self?.keyTransparencyView.state = .verifiedFailure
                }
            }
        case .verifying:
            break
        case .verifiedSuccess:
            present(KeyTransparencySuccessHeroSheet(), animated: true)
        case .verifiedFailure:
            present(KeyTransparencyFailureHeroSheet(theirName: recipientName), animated: true)
        }
    }
}

// MARK: -

extension FingerprintViewController: CompareSafetyNumbersActivityDelegate {

    func compareSafetyNumbersActivitySucceeded(activity: CompareSafetyNumbersActivity) {
        FingerprintScanViewController.showVerificationSucceeded(
            from: self,
            identityKey: fingerprint.remote.identityKey,
            recipientAci: recipientAci,
            contactName: recipientName,
            tag: "[\(type(of: self))]",
        )
    }

    func compareSafetyNumbersActivity(_ activity: CompareSafetyNumbersActivity, failedWithError error: CompareSafetyNumberError) {
        FingerprintScanViewController.showVerificationFailed(
            from: self,
            isUserError: error == .userError,
            localizedErrorDescription: error.localizedError,
            tag: "[\(type(of: self))]",
        )
    }
}

// MARK: -

private final class KeyTransparencyNotAvailableHeroSheet: HeroSheetViewController {
    init() {
        super.init(
            hero: .image(UIImage(named: "info")!, tintColor: .Signal.label),
            title: OWSLocalizedString(
                "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_NOT_AVAILABLE_SHEET_TITLE",
                comment: "Title for a sheet explaining that encryption auto-verification is not available.",
            ),
            body: OWSLocalizedString(
                "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_NOT_AVAILABLE_SHEET_BODY",
                comment: "Body for a sheet explaining that encryption auto-verification is not available.",
            ),
            primaryButton: .dismissing(title: CommonStrings.okButton),
        )
    }
}

// MARK: -

private final class KeyTransparencySuccessHeroSheet: HeroSheetViewController {
    init() {
        super.init(
            hero: .image(UIImage(named: "check-circle")!, tintColor: .Signal.label),
            title: OWSLocalizedString(
                "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_SUCCESS_SHEET_TITLE",
                comment: "Title for a sheet explaining that encryption auto-verification succeeded.",
            ),
            body: OWSLocalizedString(
                "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_SUCCESS_SHEET_BODY",
                comment: "Body for a sheet explaining that encryption auto-verification succeeded.",
            ),
            primaryButton: .dismissing(title: CommonStrings.okButton),
        )
    }
}

// MARK: -

private final class KeyTransparencyFailureHeroSheet: HeroSheetViewController {
    init(theirName: String) {
        super.init(
            hero: .image(UIImage(named: "check-circle")!, tintColor: .Signal.label),
            title: OWSLocalizedString(
                "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_FAILURE_SHEET_TITLE",
                comment: "Title for a sheet explaining that encryption auto-verification did not succeed.",
            ),
            body: String.nonPluralLocalizedStringWithFormat(
                OWSLocalizedString(
                    "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_FAILURE_SHEET_BODY_FORMAT",
                    comment: "Body for a sheet explaining that encryption auto-verification did not succeed. Embeds {{ 1: the contact's name }}.",
                ),
                theirName,
            ),
            primaryButton: .dismissing(title: CommonStrings.okButton),
        )
    }
}

// MARK: -

private final class KeyTransparencyFirstTimeEducationHeroSheet: HeroSheetViewController {
    init(onContinue: @MainActor @escaping () -> Void) {
        super.init(
            hero: .image(UIImage(named: "safety-number-verification")!),
            title: OWSLocalizedString(
                "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_EDUCATION_SHEET_TITLE",
                comment: "Title for a sheet introducing Key Transparency.",
            ),
            body: OWSLocalizedString(
                "SAFETY_NUMBERS_AUTOMATIC_VERIFICATION_EDUCATION_SHEET_BODY",
                comment: "Body for a sheet introducing Key Transparency.",
            ),
            primaryButton: HeroSheetViewController.Button(
                title: CommonStrings.continueButton,
                action: { sheet in
                    sheet.dismiss(animated: true) {
                        onContinue()
                    }
                },
            ),
        )
    }
}

// MARK: -

#if DEBUG

private extension IdentityKey {
    static func forPreview() -> IdentityKey {
        let randomBytes = Randomness.generateRandomBytes(32)
        return IdentityKey(publicKey: try! PublicKey(keyData: randomBytes))
    }
}

private final class FingerprintPreviewViewController: UINavigationController {
    init(
        theirVerificationState: VerificationState = .verified,
        keyTransparencyIsEnabled: Bool = true,
        keyTransparencyViewInitialState: FingerprintViewController.KeyTransparencyView.State = .readyToVerify,
    ) {
        let recipientAci = Aci.randomForTesting()
        let recipientIdentityKey = IdentityKey.forPreview()

        let fingerprintViewController = FingerprintViewController(
            recipientAci: recipientAci,
            recipientName: "Boba Fett",
            recipientVerificationState: theirVerificationState,
            fingerprint: CombinedFingerprints(
                local: .derive(forAci: .randomForTesting(), identityKey: .forPreview()),
                remote: .derive(forAci: recipientAci, identityKey: recipientIdentityKey),
            ),
            keyTransparencyState: FingerprintViewController.KeyTransparencyState(
                isEnabled: keyTransparencyIsEnabled,
                checkParams: nil,
                viewInitialState: keyTransparencyViewInitialState,
            ),
            deps: nil,
        )

        super.init(rootViewController: fingerprintViewController)
    }

    required init?(coder aDecoder: NSCoder) { fatalError("") }
}

@available(iOS 17, *)
#Preview("Not Verified") {
    FingerprintPreviewViewController(theirVerificationState: .noLongerVerified)
}

@available(iOS 17, *)
#Preview("Verified") {
    FingerprintPreviewViewController(theirVerificationState: .verified)
}

@available(iOS 17, *)
#Preview("KT Unavailable") {
    FingerprintPreviewViewController(keyTransparencyViewInitialState: .unableToVerify)
}

@available(iOS 17, *)
#Preview("KT Running") {
    FingerprintPreviewViewController(keyTransparencyViewInitialState: .verifying)
}

@available(iOS 17, *)
#Preview("KT Success") {
    FingerprintPreviewViewController(keyTransparencyViewInitialState: .verifiedSuccess)
}

@available(iOS 17, *)
#Preview("KT Failure") {
    FingerprintPreviewViewController(keyTransparencyViewInitialState: .verifiedFailure)
}

@available(iOS 17, *)
#Preview("KT Disabled") {
    FingerprintPreviewViewController(keyTransparencyIsEnabled: false)
}

#endif
