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

import SignalServiceKit
import SignalUI

// MARK: - RegistrationVerificationValidationError

public enum RegistrationVerificationValidationError: Equatable {
    case invalidVerificationCode(invalidCode: String)

    /// We tried to send via sms and failed, but voice code might work
    /// so we are on this screen now. An error should be shown.
    case failedInitialTransport(failedTransport: Registration.CodeTransport)

    /// A third party provider failed to send an sms or call to the session's number.
    /// May be permanent (the user should probably use a different number)
    /// or transient (the user should try again later).
    /// Regardless we let the user submit a code or retry.
    case providerFailure(isPermanent: Bool)

    /// Requesting a code failed with some unknown error; show a
    /// generic dialog and let the user dismiss. They might have actually
    /// gotten a code, so let them submit or resend.
    case genericCodeRequestError(isNetworkError: Bool)

    // These three errors are what happens when we try and
    // take the three respective actions but are rejected
    // with a timeout. The State should have timeout information.
    case smsResendTimeout
    case voiceResendTimeout
    case submitCodeTimeout
}

// MARK: - RegistrationVerificationState

public struct RegistrationVerificationState: Equatable {
    let e164: E164
    let nextSMSDate: Date?
    let nextCallDate: Date?
    let nextVerificationAttemptDate: Date?
    // If false, no option to go back and change e164 will be shown.
    let canChangeE164: Bool
    let showHelpText: Bool
    let validationError: RegistrationVerificationValidationError?

    public enum ExitConfiguration: Equatable {
        case noExitAllowed
        case exitReRegistration
        case exitChangeNumber
    }

    let exitConfiguration: ExitConfiguration
}

// MARK: - RegistrationVerificationPresenter

protocol RegistrationVerificationPresenter: AnyObject {
    func returnToPhoneNumberEntry()
    func requestSMSCode()
    func requestVoiceCode()
    func submitVerificationCode(_ code: String)
    func exitRegistration()
}

// MARK: - RegistrationVerificationViewController

class RegistrationVerificationViewController: OWSViewController {
    public init(
        state: RegistrationVerificationState,
        presenter: RegistrationVerificationPresenter
    ) {
        self.state = state
        self.presenter = presenter

        super.init()
    }

    @available(*, unavailable)
    public override init() {
        owsFail("This should not be called")
    }

    public func updateState(_ state: RegistrationVerificationState) {
        self.state = state
    }

    deinit {
        nowTimer?.invalidate()
        nowTimer = nil
    }

    // MARK: Internal state

    private var state: RegistrationVerificationState {
        didSet { render() }
    }

    private weak var presenter: RegistrationVerificationPresenter?

    private var now = Date() {
        didSet { render() }
    }
    private var nowTimer: Timer?

    private var canRequestSMSCode: Bool {
        guard let nextDate = state.nextSMSDate else { return false }
        return nextDate <= now
    }

    private var canRequestVoiceCode: Bool {
        guard let nextDate = state.nextCallDate else { return false }
        return nextDate <= now
    }

    private var previouslyRenderedValidationError: RegistrationVerificationValidationError?

    // MARK: Rendering

    private func button(
        title: String = "",
        selector: Selector,
        accessibilityIdentifierSuffix: String
    ) -> UIButton {
        let result = UIButton(type: .system)

        result.addTarget(self, action: selector, for: .touchUpInside)

        result.setTitle(title, for: .normal)
        if let titleLabel = result.titleLabel {
            titleLabel.font = .dynamicTypeSubheadlineClamped
            titleLabel.numberOfLines = 0
            titleLabel.lineBreakMode = .byWordWrapping
            titleLabel.textAlignment = .center
            result.heightAnchor.constraint(
                greaterThanOrEqualTo: titleLabel.heightAnchor
            ).isActive = true
        } else {
            owsFailBeta("Button has no title label")
        }

        result.accessibilityIdentifier = "registration.verification.\(accessibilityIdentifierSuffix)"

        return result
    }

    private lazy var titleLabel: UILabel = {
        let result = UILabel.titleLabelForRegistration(text: OWSLocalizedString(
            "ONBOARDING_VERIFICATION_TITLE_LABEL",
            comment: "Title label for the onboarding verification page"
        ))
        result.accessibilityIdentifier = "registration.verification.titleLabel"
        return result
    }()

    private var explanationLabelText: String {
        let format = OWSLocalizedString(
            "ONBOARDING_VERIFICATION_TITLE_DEFAULT_FORMAT",
            comment: "Format for the title of the 'onboarding verification' view. Embeds {{the user's phone number}}."
        )
        return String(format: format, state.e164.stringValue.e164FormattedAsPhoneNumberWithoutBreaks)
    }

    private lazy var explanationLabel: UILabel = {
        let result = UILabel.explanationLabelForRegistration(text: explanationLabelText)
        result.accessibilityIdentifier = "registration.verification.explanationLabel"
        return result
    }()

    private lazy var wrongNumberButton = button(
        title: OWSLocalizedString(
            "ONBOARDING_VERIFICATION_BACK_LINK",
            comment: "Label for the link that lets users change their phone number in the onboarding views."
        ),
        selector: #selector(didTapWrongNumberButton),
        accessibilityIdentifierSuffix: "wrongNumberButton"
    )

    private lazy var verificationCodeView: RegistrationVerificationCodeView = {
        let result = RegistrationVerificationCodeView()
        result.delegate = self
        return result
    }()

    private lazy var helpButton = button(
        title: OWSLocalizedString(
            "ONBOARDING_VERIFICATION_HELP_LINK",
            comment: "Label for a button to get help entering a verification code when registering."
        ),
        selector: #selector(didTapHelpButton),
        accessibilityIdentifierSuffix: "helpButton"
    )

    private lazy var resendSMSCodeButton = button(
        selector: #selector(didTapResendSMSCode),
        accessibilityIdentifierSuffix: "resendSMSCodeButton"
    )

    private lazy var requestVoiceCodeButton = button(
        selector: #selector(didTapSendVoiceCode),
        accessibilityIdentifierSuffix: "requestVoiceCodeButton"
    )

    private lazy var contextButton: ContextMenuButton = {
        let result = ContextMenuButton()
        result.showsContextMenuAsPrimaryAction = true
        result.autoSetDimensions(to: .square(40))
        return result
    }()

    private lazy var contextBarButton = UIBarButtonItem(
        customView: contextButton,
        accessibilityIdentifier: "registration.verificationCode.contextButton"
    )

    public override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.setHidesBackButton(true, animated: false)

        initialRender()

        // We don't need this timer in all cases but it's simpler to start it in all cases.
        nowTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            guard let self else { return }
            self.now = Date()
        }
    }

    private var isViewAppeared = false

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

        verificationCodeView.becomeFirstResponder()

        showValidationErrorUiIfNecessary()

        isViewAppeared = true
    }

    public override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        if contextButton.isShowingContextMenu {
            contextButton.dismissContextMenu(animated: animated)
        }

        isViewAppeared = false
    }

    public override func themeDidChange() {
        super.themeDidChange()
        render()
    }

    private func initialRender() {
        let scrollView = UIScrollView()
        view.addSubview(scrollView)
        scrollView.autoPinWidthToSuperview()
        scrollView.autoPinEdge(.top, to: .top, of: keyboardLayoutGuideViewSafeArea)
        scrollView.autoPinEdge(.bottom, to: .bottom, of: keyboardLayoutGuideViewSafeArea)

        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.spacing = 12
        stackView.layoutMargins = UIEdgeInsets.layoutMarginsForRegistration(
            traitCollection.horizontalSizeClass
        )
        stackView.isLayoutMarginsRelativeArrangement = true
        stackView.setContentHuggingHigh()
        scrollView.addSubview(stackView)
        stackView.autoPinWidth(toWidthOf: scrollView)
        stackView.heightAnchor.constraint(
            greaterThanOrEqualTo: scrollView.contentLayoutGuide.heightAnchor
        ).isActive = true
        stackView.heightAnchor.constraint(
            greaterThanOrEqualTo: scrollView.frameLayoutGuide.heightAnchor
        ).isActive = true

        stackView.addArrangedSubview(titleLabel)

        stackView.addArrangedSubview(explanationLabel)

        stackView.addArrangedSubview(wrongNumberButton)
        stackView.setCustomSpacing(24, after: wrongNumberButton)

        stackView.addArrangedSubview(verificationCodeView)
        stackView.setCustomSpacing(24, after: verificationCodeView)

        stackView.addArrangedSubview(helpButton)

        stackView.addArrangedSubview(UIView.vStretchingSpacer())

        let resendButtonsContainer = UIStackView(arrangedSubviews: [
            resendSMSCodeButton,
            requestVoiceCodeButton
        ])
        resendButtonsContainer.axis = .horizontal
        resendButtonsContainer.distribution = .fillEqually

        stackView.addArrangedSubview(resendButtonsContainer)

        render()
    }

    private func render() {
        switch state.exitConfiguration {
        case .noExitAllowed:
            navigationItem.leftBarButtonItem = nil
        case .exitReRegistration:
            navigationItem.leftBarButtonItem = contextBarButton
            contextButton.contextMenu = ContextMenu([
                .init(
                    title: OWSLocalizedString(
                        "EXIT_REREGISTRATION",
                        comment: "Button to exit re-registration, shown in context menu."
                    ),
                    handler: { [weak self] _ in
                        self?.presenter?.exitRegistration()
                    }
                )
            ])
        case .exitChangeNumber:
            navigationItem.leftBarButtonItem = contextBarButton
            contextButton.contextMenu = ContextMenu([
                .init(
                    title: OWSLocalizedString(
                        "EXIT_CHANGE_NUMBER",
                        comment: "Button to exit change number, shown in context menu."
                    ),
                    handler: { [weak self] _ in
                        self?.presenter?.exitRegistration()
                    }
                )
            ])
        }

        contextButton.setImage(Theme.iconImage(.buttonMore), for: .normal)
        contextButton.tintColor = Theme.accentBlueColor

        renderResendButton(
            button: resendSMSCodeButton,
            date: state.nextSMSDate,
            enabledString: OWSLocalizedString(
                "ONBOARDING_VERIFICATION_RESEND_CODE_BUTTON",
                comment: "Label for button to resend SMS verification code."
            ),
            countdownFormat: OWSLocalizedString(
                "ONBOARDING_VERIFICATION_RESEND_CODE_COUNTDOWN_FORMAT",
                comment: "Format string for button counting down time until SMS code can be resent. Embeds {{time remaining}}."
            )
        )
        renderResendButton(
            button: requestVoiceCodeButton,
            date: state.nextCallDate,
            enabledString: OWSLocalizedString(
                "ONBOARDING_VERIFICATION_CALL_ME_BUTTON",
                comment: "Label for button to perform verification with a phone call."
            ),
            countdownFormat: OWSLocalizedString(
                "ONBOARDING_VERIFICATION_CALL_ME_COUNTDOWN_FORMAT",
                comment: "Format string for button counting down time until phone call verification can be performed. Embeds {{time remaining}}."
            )
        )

        if isViewAppeared {
            showValidationErrorUiIfNecessary()
        }

        view.backgroundColor = Theme.backgroundColor
        titleLabel.textColor = .colorForRegistrationTitleLabel
        explanationLabel.textColor = .colorForRegistrationExplanationLabel
        explanationLabel.text = explanationLabelText
        wrongNumberButton.isHidden = state.canChangeE164.negated
        helpButton.isHidden = state.showHelpText.negated

        verificationCodeView.updateColors()
    }

    private lazy var retryAfterFormatter: DateFormatter = {
        let result = DateFormatter()
        result.dateFormat = "m:ss"
        result.timeZone = TimeZone(identifier: "UTC")!
        return result
    }()

    private func renderResendButton(
        button: UIButton,
        date: Date?,
        enabledString: String,
        countdownFormat: String
    ) {
        // UIButton will flash when we update the title.
        UIView.performWithoutAnimation {
            defer { button.layoutIfNeeded() }

            guard let date else {
                button.isHidden = true
                button.isEnabled = false
                return
            }

            if date <= now {
                button.isEnabled = true
                button.setTitle(enabledString, for: .normal)
            } else {
                button.isEnabled = false
                button.setTitle(
                    {
                        let timeRemaining = max(date.timeIntervalSince(now), 0)
                        let durationString = retryAfterFormatter.string(from: Date(timeIntervalSinceReferenceDate: timeRemaining))
                        return String(format: countdownFormat, durationString)
                    }(),
                    for: .normal
                )
            }
        }
    }

    private func showValidationErrorUiIfNecessary() {
        let oldError = previouslyRenderedValidationError
        let newError = state.validationError

        previouslyRenderedValidationError = newError

        guard let newError, oldError != newError else { return }
        switch newError {
        case .invalidVerificationCode(let code):
            let message = OWSLocalizedString(
                "REGISTRATION_VERIFICATION_ERROR_INVALID_VERIFICATION_CODE",
                comment: "During registration and re-registration, users may have to enter a code to verify ownership of their phone number. If they enter an invalid code, they will see this error message."
            )
            if verificationCodeView.verificationCode == code {
                verificationCodeView.clear()
            }
            OWSActionSheets.showActionSheet(title: nil, message: message)

        case .providerFailure(let isPermanent):
            let message: String
            if isPermanent {
                message = OWSLocalizedString(
                    "REGISTRATION_PROVIDER_FAILURE_MESSAGE_PERMANENT",
                    comment: "Error shown if an SMS/call service provider is permanently unable to send a verification code to the provided number."
                )
            } else {
                message = OWSLocalizedString(
                    "REGISTRATION_PROVIDER_FAILURE_MESSAGE_TRANSIENT",
                    comment: "Error shown if an SMS/call service provider is temporarily unable to send a verification code to the provided number."
                )
            }
            OWSActionSheets.showActionSheet(title: nil, message: message)

        case .genericCodeRequestError(let isNetworkError):
            let title: String?
            let message: String
            if isNetworkError {
                title = OWSLocalizedString(
                    "REGISTRATION_NETWORK_ERROR_TITLE",
                    comment: "A network error occurred during registration, and an error is shown to the user. This is the title on that error sheet."
                )
                message = OWSLocalizedString(
                    "REGISTRATION_NETWORK_ERROR_BODY",
                    comment: "A network error occurred during registration, and an error is shown to the user. This is the body on that error sheet."
                )
            } else {
                title = nil
                message = CommonStrings.somethingWentWrongTryAgainLaterError
            }
            OWSActionSheets.showActionSheet(title: title, message: message)

        case .failedInitialTransport(let failedTransport):
            let errorMessage: String
            let alternativeTransportButtonText: String
            let alternativeTransport: Registration.CodeTransport
            switch failedTransport {
            case .sms:
                errorMessage = OWSLocalizedString(
                    "REGISTRATION_SMS_CODE_FAILED_TRY_VOICE_ERROR",
                    comment: "Error message when sending a verification code via sms failed, but resending via voice call might succeed."
                )
                alternativeTransportButtonText = OWSLocalizedString(
                    "REGISTRATION_SMS_CODE_FAILED_TRY_VOICE_BUTTON",
                    comment: "Button when sending a verification code via sms failed, but resending via voice call might succeed."
                )
                alternativeTransport = .voice
            case .voice:
                errorMessage = OWSLocalizedString(
                    "REGISTRATION_VOICE_CODE_FAILED_TRY_SMS_ERROR",
                    comment: "Error message when sending a verification code via voice call failed, but resending via sms might succeed."
                )
                alternativeTransportButtonText = OWSLocalizedString(
                    "REGISTRATION_VOICE_CODE_FAILED_TRY_SMS_BUTTON",
                    comment: "Button when sending a verification code via voice call failed, but resending via sms might succeed."
                )
                alternativeTransport = .sms
            }
            let actionSheet = ActionSheetController(title: nil, message: errorMessage)
            actionSheet.addAction(.init(
                title: alternativeTransportButtonText,
                accessibilityIdentifier: nil,
                handler: { [weak self] _ in
                    switch alternativeTransport {
                    case .sms:
                        self?.presenter?.requestSMSCode()
                    case .voice:
                        self?.presenter?.requestVoiceCode()
                    }
                }
            ))
            actionSheet.addAction(.init(
                title: CommonStrings.cancelButton,
                accessibilityIdentifier: nil
            ))
            self.present(actionSheet, animated: true)
            return

        case .smsResendTimeout, .voiceResendTimeout:
            let message = OWSLocalizedString(
                "REGISTER_RATE_LIMITING_ALERT",
                comment: "Body of action sheet shown when rate-limited during registration."
            )
            OWSActionSheets.showActionSheet(title: nil, message: message)

        case .submitCodeTimeout:
            guard let nextVerificationAttemptDate = state.nextVerificationAttemptDate else {
                return
            }
            let now = Date()
            if now >= nextVerificationAttemptDate {
                return
            }
            let format = OWSLocalizedString(
                "REGISTRATION_SUBMIT_CODE_RATE_LIMIT_ALERT_FORMAT",
                comment: "Alert shown when submitting a verification code too many times. Embeds {{ duration }}, such as \"5:00\""
            )

            let formatter: DateFormatter = {
                let result = DateFormatter()
                result.dateFormat = "m:ss"
                result.timeZone = TimeZone(identifier: "UTC")!
                return result
            }()

            let timeRemaining = max(nextVerificationAttemptDate.timeIntervalSince(now), 0)
            let durationString = formatter.string(from: Date(timeIntervalSinceReferenceDate: timeRemaining))
            let message = String(format: format, durationString)
            OWSActionSheets.showActionSheet(title: nil, message: message)
        }
    }

    // MARK: Events

    @objc
    private func didTapWrongNumberButton() {
        Logger.info("")

        presenter?.returnToPhoneNumberEntry()
    }

    @objc
    private func didTapHelpButton() {
        Logger.info("")

        self.present(RegistrationVerificationHelpSheetViewController(), animated: true)
    }

    @objc
    private func didTapResendSMSCode() {
        Logger.info("")

        guard canRequestSMSCode else { return }

        presentActionSheet(.forRegistrationVerificationConfirmation(
            mode: .sms,
            e164: state.e164.stringValue,
            didConfirm: { [weak self] in self?.presenter?.requestSMSCode() },
            didRequestEdit: { [weak self] in self?.presenter?.returnToPhoneNumberEntry() }
        ))
    }

    @objc
    private func didTapSendVoiceCode() {
        Logger.info("")

        guard canRequestVoiceCode else { return }

        presentActionSheet(.forRegistrationVerificationConfirmation(
            mode: .voice,
            e164: state.e164.stringValue,
            didConfirm: { [weak self] in self?.presenter?.requestVoiceCode() },
            didRequestEdit: { [weak self] in self?.presenter?.returnToPhoneNumberEntry() }
        ))
    }
}

// MARK: - RegistrationVerificationCodeViewDelegate

extension RegistrationVerificationViewController: RegistrationVerificationCodeViewDelegate {
    func codeViewDidChange() {
        if verificationCodeView.isComplete {
            Logger.info("Submitting verification code")
            verificationCodeView.resignFirstResponder()
            // Clear any errors so we render new ones.
            previouslyRenderedValidationError = nil
            presenter?.submitVerificationCode(verificationCodeView.verificationCode)
        }
    }
}

// MARK: - RegistrationVerificationHelpSheetViewController

private class RegistrationVerificationHelpSheetViewController: InteractiveSheetViewController {

    private var intrinsicSizeObservation: NSKeyValueObservation?

    public init() {
        super.init()

        scrollView.bounces = false
        scrollView.isScrollEnabled = false

        stackView.axis = .vertical
        stackView.alignment = .fill
        stackView.spacing = 12

        stackView.addArrangedSubview(header)
        stackView.setCustomSpacing(20, after: header)
        let bulletPoints = bulletPoints
        stackView.addArrangedSubviews(bulletPoints)

        // TODO[Registration]: there should be a contact support link here.

        let insets = UIEdgeInsets(top: 20, left: 24, bottom: 80, right: 24)
        contentView.addSubview(scrollView)
        scrollView.autoPinEdgesToSuperviewEdges()
        scrollView.addSubview(stackView)
        stackView.autoPinEdgesToSuperviewEdges(with: insets)
        stackView.autoConstrainAttribute(.width, to: .width, of: contentView, withOffset: -insets.totalWidth)

        self.allowsExpansion = false
        intrinsicSizeObservation = stackView.observe(\.bounds, changeHandler: { [weak self] stackView, _ in
            self?.minimizedHeight = stackView.bounds.height + insets.totalHeight
            self?.scrollView.isScrollEnabled = (self?.maxHeight ?? 0) < stackView.bounds.height
        })
    }

    override public func viewSafeAreaInsetsDidChange() {
        super.viewSafeAreaInsetsDidChange()
        scrollView.isScrollEnabled = self.maxHeight < stackView.bounds.height
    }

    let scrollView = UIScrollView()
    let stackView = UIStackView()

    let header: UILabel = {
        let label = UILabel()
        label.textAlignment = .center
        label.font = UIFont.dynamicTypeTitle2.semibold()
        label.text = OWSLocalizedString(
            "ONBOARDING_VERIFICATION_HELP_LINK",
            comment: "Label for a button to get help entering a verification code when registering."
        )
        label.numberOfLines = 0
        label.lineBreakMode = .byWordWrapping
        return label
    }()

    let bulletPoints: [UIView] = {
        return [
            OWSLocalizedString(
                "ONBOARDING_VERIFICATION_HELP_BULLET_1",
                comment: "First bullet point for the explainer sheet for registering via verification code."
            ),
            OWSLocalizedString(
                "ONBOARDING_VERIFICATION_HELP_BULLET_2",
                comment: "Second bullet point for the explainer sheet for registering via verification code."
            ),
            OWSLocalizedString(
                "ONBOARDING_VERIFICATION_HELP_BULLET_3",
                comment: "Third bullet point for the explainer sheet for registering via verification code."
            )
        ].map { text in
            return RegistrationVerificationHelpSheetViewController.listPointView(text: text)
        }
    }()

    private static func listPointView(text: String) -> UIStackView {
        let stackView = UIStackView(frame: .zero)

        stackView.axis = .horizontal
        stackView.alignment = .center
        stackView.spacing = 8

        let label = UILabel()
        label.text = text
        label.numberOfLines = 0
        label.textColor = Theme.primaryTextColor
        label.font = .dynamicTypeBodyClamped

        let bulletPoint = UIView()
        bulletPoint.backgroundColor = UIColor(rgbHex: 0xC4C4C4)

        stackView.addArrangedSubview(.spacer(withWidth: 4))
        stackView.addArrangedSubview(bulletPoint)
        stackView.addArrangedSubview(label)

        bulletPoint.autoSetDimensions(to: .init(width: 4, height: 14))
        label.setCompressionResistanceHigh()
        return stackView
    }
}
