//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//

import Defaults
import Factory
import LocalAuthentication
import Stinsen
import SwiftUI

// TODO: ignore device authentication `canceled by user` NSError
// TODO: fix duplicate user
//       - could be good to replace access token
//       - check against current user policy

struct UserSignInView: View {

    @Default(.accentColor)
    private var accentColor

    @EnvironmentObject
    private var router: UserSignInCoordinator.Router

    @FocusState
    private var focusedTextField: Int?

    @State
    private var duplicateUser: UserState? = nil
    @State
    private var error: Error? = nil
    @State
    private var isPresentingDuplicateUser: Bool = false
    @State
    private var isPresentingError: Bool = false
    @State
    private var isPresentingLocalPin: Bool = false
    @State
    private var onPinCompletion: (() -> Void)? = nil
    @State
    private var password: String = ""
    @State
    private var pin: String = ""
    @State
    private var pinHint: String = ""
    @State
    private var accessPolicy: UserAccessPolicy = .none
    @State
    private var username: String = ""

    @StateObject
    private var viewModel: UserSignInViewModel

    init(server: ServerState) {
        self._viewModel = StateObject(wrappedValue: UserSignInViewModel(server: server))
    }

    private func handleSignIn(_ event: UserSignInViewModel.Event) {
        switch event {
        case let .duplicateUser(duplicateUser):
            UIDevice.impact(.medium)

            self.duplicateUser = duplicateUser
            isPresentingDuplicateUser = true
        case let .error(eventError):
            UIDevice.feedback(.error)

            error = eventError
            isPresentingError = true
        case let .signedIn(user):
            UIDevice.feedback(.success)

            Defaults[.lastSignedInUserID] = .signedIn(userID: user.id)
            Container.shared.currentUserSession.reset()
            Notifications[.didSignIn].post()
        }
    }

    // TODO: don't have multiple ways to handle device authentication vs required pin

    private func openQuickConnect(needsPin: Bool = true) {
        Task {
            switch accessPolicy {
            case .none: ()
            case .requireDeviceAuthentication:
                try await performDeviceAuthentication(
                    reason: "Require device authentication to sign in to the Quick Connect user on this device"
                )
            case .requirePin:
                if needsPin {
                    onPinCompletion = {
                        router.route(to: \.quickConnect, viewModel.quickConnect)
                    }
                    isPresentingLocalPin = true
                    return
                }
            }

            router.route(to: \.quickConnect, viewModel.quickConnect)
        }
    }

    private func signInUserPassword(needsPin: Bool = true) {
        Task {
            switch accessPolicy {
            case .none: ()
            case .requireDeviceAuthentication:
                try await performDeviceAuthentication(reason: "Require device authentication to sign in to \(username) on this device")
            case .requirePin:
                if needsPin {
                    onPinCompletion = {
                        viewModel.send(.signIn(username: username, password: password, policy: accessPolicy))
                    }
                    isPresentingLocalPin = true
                    return
                }
            }

            viewModel.send(.signIn(username: username, password: password, policy: accessPolicy))
        }
    }

    private func signInUplicate(user: UserState, needsPin: Bool = true, replace: Bool) {
        Task {
            switch user.accessPolicy {
            case .none: ()
            case .requireDeviceAuthentication:
                try await performDeviceAuthentication(reason: "User \(user.username) requires device authentication")
            case .requirePin:
                onPinCompletion = {
                    viewModel.send(.signInDuplicate(user, replace: replace))
                }
                isPresentingLocalPin = true
                return
            }

            viewModel.send(.signInDuplicate(user, replace: replace))
        }
    }

    private func performPinAuthentication() async throws {
        isPresentingLocalPin = true

        guard pin.count > 4, pin.count < 30 else {
            throw JellyfinAPIError("Pin auth failed")
        }
    }

    // error logging/presentation is handled within here, just
    // use try+thrown error in local Task for early return
    private func performDeviceAuthentication(reason: String) async throws {
        let context = LAContext()
        var policyError: NSError?

        guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &policyError) else {
            viewModel.logger.critical("\(policyError!.localizedDescription)")

            await MainActor.run {
                self
                    .error =
                    JellyfinAPIError(
                        "Unable to perform device authentication. You may need to enable Face ID in the Settings app for Swiftfin."
                    )
                self.isPresentingError = true
            }

            throw JellyfinAPIError("Device auth failed")
        }

        do {
            try await context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason)
        } catch {
            viewModel.logger.critical("\(error.localizedDescription)")

            await MainActor.run {
                self.error = JellyfinAPIError("Unable to perform device authentication")
                self.isPresentingError = true
            }

            throw JellyfinAPIError("Device auth failed")
        }
    }

    @ViewBuilder
    private var signInSection: some View {
        Section {
            TextField(L10n.username, text: $username)
                .autocorrectionDisabled()
                .textInputAutocapitalization(.never)
                .focused($focusedTextField, equals: 0)
                .onSubmit {
                    focusedTextField = 1
                }

            UnmaskSecureField(L10n.password, text: $password) {
                focusedTextField = nil

                signInUserPassword()
            }
            .autocorrectionDisabled()
            .textInputAutocapitalization(.never)
            .focused($focusedTextField, equals: 1)
        } header: {
            Text(L10n.signInToServer(viewModel.server.name))
        } footer: {
            switch accessPolicy {
            case .requireDeviceAuthentication:
                Label("This user will require device authentication.", systemImage: "exclamationmark.circle.fill")
                    .labelStyle(.sectionFooterWithImage(imageStyle: .orange))
            case .requirePin:
                Label("This user will require a pin.", systemImage: "exclamationmark.circle.fill")
                    .labelStyle(.sectionFooterWithImage(imageStyle: .orange))
            case .none:
                EmptyView()
            }
        }

        if case .signingIn = viewModel.state {
            ListRowButton(L10n.cancel) {
                viewModel.send(.cancel)
            }
            .foregroundStyle(.red, .red.opacity(0.2))
        } else {
            ListRowButton(L10n.signIn) {
                focusedTextField = nil

                signInUserPassword()
            }
            .disabled(username.isEmpty)
            .foregroundStyle(
                accentColor.overlayColor,
                accentColor
            )
            .opacity(username.isEmpty ? 0.5 : 1)
        }

        if viewModel.isQuickConnectEnabled {
            Section {
                ListRowButton(L10n.quickConnect) {
                    openQuickConnect()
                }
                .disabled(viewModel.state == .signingIn)
                .foregroundStyle(
                    accentColor.overlayColor,
                    accentColor
                )
            }
        }

        if let disclaimer = viewModel.serverDisclaimer {
            Section("Disclaimer") {
                Text(disclaimer)
                    .font(.callout)
            }
        }
    }

    @ViewBuilder
    private var publicUsersSection: some View {
        Section(L10n.publicUsers) {
            if viewModel.publicUsers.isEmpty {
                L10n.noPublicUsers.text
                    .font(.callout)
                    .foregroundColor(.secondary)
                    .frame(maxWidth: .infinity)
            } else {
                ForEach(viewModel.publicUsers, id: \.id) { user in
                    PublicUserRow(
                        user: user,
                        client: viewModel.server.client
                    ) {
                        username = user.name ?? ""
                        password = ""
                        focusedTextField = 1
                    }
                }
            }
        }
    }

    var body: some View {
        List {
            signInSection

            publicUsersSection
        }
        .animation(.linear, value: viewModel.isQuickConnectEnabled)
        .interactiveDismissDisabled(viewModel.state == .signingIn)
        .navigationTitle(L10n.signIn)
        .navigationBarTitleDisplayMode(.inline)
        .navigationBarCloseButton(disabled: viewModel.state == .signingIn) {
            router.dismissCoordinator()
        }
        .onChange(of: isPresentingLocalPin) { newValue in
            if newValue {
                pin = ""
            } else {
                onPinCompletion = nil
            }
        }
        .onChange(of: pin) { newValue in
            StoredValues[.Temp.userLocalPin] = newValue
        }
        .onChange(of: pinHint) { newValue in
            StoredValues[.Temp.userLocalPinHint] = newValue
        }
        .onChange(of: accessPolicy) { newValue in
            // necessary for Quick Connect sign in, but could
            // just use for general sign in
            StoredValues[.Temp.userAccessPolicy] = newValue
        }
        .onReceive(viewModel.events) { event in
            handleSignIn(event)
        }
        .onFirstAppear {
            focusedTextField = 0
            viewModel.send(.getPublicData)
        }
        .topBarTrailing {
            if viewModel.state == .signingIn || viewModel.backgroundStates.contains(.gettingPublicData) {
                ProgressView()
            }

            Button("Security", systemImage: "gearshape.fill") {
                let parameters = UserSignInCoordinator.SecurityParameters(
                    pinHint: $pinHint,
                    accessPolicy: $accessPolicy
                )
                router.route(to: \.security, parameters)
            }
        }
        .alert(
            Text("Duplicate User"),
            isPresented: $isPresentingDuplicateUser,
            presenting: duplicateUser
        ) { _ in

            // TODO: uncomment when duplicate user fixed
//            Button(L10n.signIn) {
//                signInUplicate(user: user, replace: false)
//            }

//            Button("Replace") {
//                signInUplicate(user: user, replace: true)
//            }

            Button(L10n.dismiss, role: .cancel)
        } message: { duplicateUser in
            Text("\(duplicateUser.username) is already saved")
        }
        .alert(
            L10n.error.text,
            isPresented: $isPresentingError,
            presenting: error
        ) { _ in
            Button(L10n.dismiss, role: .cancel)
        } message: { error in
            Text(error.localizedDescription)
        }
        .alert(
            "Set Pin",
            isPresented: $isPresentingLocalPin,
            presenting: onPinCompletion
        ) { completion in

            TextField("Pin", text: $pin)
                .keyboardType(.numberPad)

            // bug in SwiftUI: having .disabled will dismiss
            // alert but not call the closure (for length)
            Button("Sign In") {
                completion()
            }

            Button(L10n.cancel, role: .cancel) {}
        } message: { _ in
            Text("Set pin for new user.")
        }
    }
}
