//
// 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 CollectionVGrid
import Defaults
import Factory
import JellyfinAPI
import LocalAuthentication
import OrderedCollections
import SwiftUI

// TODO: authentication view during device authentication
//       - could use provided UI, but is iOS 16+
//       - could just ignore for iOS 15, or basic view
// TODO: user ordering
//       - name
//       - last signed in date

struct SelectUserView: View {

    private enum UserGridItem: Hashable {
        case user(UserState, server: ServerState)
        case addUser
    }

    @Default(.selectUserUseSplashscreen)
    private var selectUserUseSplashscreen
    @Default(.selectUserAllServersSplashscreen)
    private var selectUserAllServersSplashscreen
    @Default(.selectUserServerSelection)
    private var serverSelection
    @Default(.selectUserDisplayType)
    private var userListDisplayType

    @Environment(\.colorScheme)
    private var colorScheme

    @EnvironmentObject
    private var router: SelectUserCoordinator.Router

    @State
    private var contentSize: CGSize = .zero
    @State
    private var error: Error? = nil
    @State
    private var gridItems: OrderedSet<UserGridItem> = []
    @State
    private var gridItemSize: CGSize = .zero
    @State
    private var isEditingUsers: Bool = false
    @State
    private var isPresentingConfirmDeleteUsers = false
    @State
    private var isPresentingError: Bool = false
    @State
    private var isPresentingLocalPin: Bool = false
    @State
    private var padGridItemColumnCount: Int = 1
    @State
    private var pin: String = ""
    @State
    private var selectedUsers: Set<UserState> = []
    @State
    private var splashScreenImageSources: [ImageSource] = []

    @StateObject
    private var viewModel = SelectUserViewModel()

    private var selectedServer: ServerState? {
        if case let SelectUserServerSelection.server(id: id) = serverSelection,
           let server = viewModel.servers.keys.first(where: { server in server.id == id })
        {
            return server
        }

        return nil
    }

    private func makeGridItems(for serverSelection: SelectUserServerSelection) -> OrderedSet<UserGridItem> {
        switch serverSelection {
        case .all:
            let items = viewModel.servers
                .map { server, users in
                    users.map { (server: server, user: $0) }
                }
                .flatMap { $0 }
                .sorted(using: \.user.username)
                .reversed()
                .map { UserGridItem.user($0.user, server: $0.server) }
                .appending(.addUser)

            return OrderedSet(items)
        case let .server(id: id):
            guard let server = viewModel.servers.keys.first(where: { server in server.id == id }) else {
                assertionFailure("server with ID not found?")
                return [.addUser]
            }

            let items = viewModel.servers[server]!
                .sorted(using: \.username)
                .map { UserGridItem.user($0, server: server) }
                .appending(.addUser)

            return OrderedSet(items)
        }
    }

    // For all server selection, .all is random
    private func makeSplashScreenImageSources(
        serverSelection: SelectUserServerSelection,
        allServersSelection: SelectUserServerSelection
    ) -> [ImageSource] {
        switch (serverSelection, allServersSelection) {
        case (.all, .all):
            return viewModel
                .servers
                .keys
                .shuffled()
                .map { $0.splashScreenImageSource() }

        // need to evaluate server with id selection first
        case let (.server(id), _), let (.all, .server(id)):
            return [
                viewModel
                    .servers
                    .keys
                    .first { $0.id == id }?
                    .splashScreenImageSource() ?? .init(),
            ]
        }
    }

    private func select(user: UserState, needsPin: Bool = true) {
        Task { @MainActor in
            selectedUsers.insert(user)

            switch user.accessPolicy {
            case .requireDeviceAuthentication:
                try await performDeviceAuthentication(reason: "User \(user.username) requires device authentication")
            case .requirePin:
                if needsPin {
                    isPresentingLocalPin = true
                    return
                }
            case .none: ()
            }

            viewModel.send(.signIn(user, pin: pin))
        }
    }

    // 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")
        }
    }

    // MARK: advancedMenu

    @ViewBuilder
    private var advancedMenu: some View {
        Menu(L10n.advanced, systemImage: "gearshape.fill") {

            Section {
                if gridItems.count > 1 {
                    Button("Edit Users", systemImage: "person.crop.circle") {
                        isEditingUsers.toggle()
                    }
                }
            }

            if !viewModel.servers.isEmpty {
                Picker(selection: $userListDisplayType) {
                    ForEach(LibraryDisplayType.allCases, id: \.hashValue) {
                        Label($0.displayTitle, systemImage: $0.systemImage)
                            .tag($0)
                    }
                } label: {
                    Text("Layout")
                    Text(userListDisplayType.displayTitle)
                    Image(systemName: userListDisplayType.systemImage)
                }
                .pickerStyle(.menu)
            }

            Section {
                Button(L10n.advanced, systemImage: "gearshape.fill") {
                    router.route(to: \.advancedSettings)
                }
            }
        }
    }

    // MARK: grid

    private func padGridItemOffset(index: Int) -> CGFloat {
        let lastRowIndices = (gridItems.count - gridItems.count % padGridItemColumnCount ..< gridItems.count)

        guard lastRowIndices.contains(index) else { return 0 }

        let lastRowMissing = padGridItemColumnCount - gridItems.count % padGridItemColumnCount
        return CGFloat(lastRowMissing) * (gridItemSize.width + EdgeInsets.edgePadding) / 2
    }

    @ViewBuilder
    private var padGridContentView: some View {
        let columns = [GridItem(.adaptive(minimum: 150, maximum: 300), spacing: EdgeInsets.edgePadding)]

        LazyVGrid(columns: columns, spacing: EdgeInsets.edgePadding) {
            ForEach(Array(gridItems.enumerated().map(\.offset)), id: \.hashValue) { index in
                let item = gridItems[index]

                gridItemView(for: item)
                    .trackingSize($gridItemSize)
                    .offset(x: padGridItemOffset(index: index))
            }
        }
        .edgePadding()
        .scrollIfLargerThanContainer(padding: 100)
        .onChange(of: gridItemSize) { newValue in
            let columns = Int(contentSize.width / (newValue.width + EdgeInsets.edgePadding))

            padGridItemColumnCount = columns
        }
    }

    @ViewBuilder
    private var phoneGridContentView: some View {
        let columns = [GridItem(.flexible(), spacing: EdgeInsets.edgePadding), GridItem(.flexible())]

        LazyVGrid(columns: columns, spacing: EdgeInsets.edgePadding) {
            ForEach(gridItems, id: \.hashValue) { item in
                gridItemView(for: item)
                    .if(gridItems.count % 2 == 1 && item == gridItems.last) { view in
                        view.trackingSize($gridItemSize)
                            .offset(x: (gridItemSize.width + EdgeInsets.edgePadding) / 2)
                    }
            }
        }
        .edgePadding()
        .scrollIfLargerThanContainer(padding: 100)
    }

    @ViewBuilder
    private func gridItemView(for item: UserGridItem) -> some View {
        switch item {
        case let .user(user, server):
            UserGridButton(
                user: user,
                server: server,
                showServer: serverSelection == .all
            ) {
                if isEditingUsers {
                    selectedUsers.toggle(value: user)
                } else {
                    select(user: user)
                }
            } onDelete: {
                selectedUsers.insert(user)
                isPresentingConfirmDeleteUsers = true
            }
            .environment(\.isEditing, isEditingUsers)
            .environment(\.isSelected, selectedUsers.contains(user))
        case .addUser:
            AddUserButton(
                serverSelection: $serverSelection,
                servers: viewModel.servers.keys
            ) { server in
                UIDevice.impact(.light)
                router.route(to: \.userSignIn, server)
            }
            .environment(\.isEnabled, !isEditingUsers)
        }
    }

    // MARK: list

    @ViewBuilder
    private var listContentView: some View {
        ScrollView {
            LazyVStack {
                ForEach(gridItems, id: \.hashValue) { item in
                    listItemView(for: item)
                }
            }
        }
    }

    @ViewBuilder
    private func listItemView(for item: UserGridItem) -> some View {
        switch item {
        case let .user(user, server):
            UserRow(
                user: user,
                server: server,
                showServer: serverSelection == .all
            ) {
                if isEditingUsers {
                    selectedUsers.toggle(value: user)
                } else {
                    select(user: user)
                }
            } onDelete: {
                selectedUsers.insert(user)
                isPresentingConfirmDeleteUsers = true
            }
            .environment(\.isEditing, isEditingUsers)
            .environment(\.isSelected, selectedUsers.contains(user))
        case .addUser:
            AddUserRow(
                serverSelection: $serverSelection,
                servers: viewModel.servers.keys
            ) { server in
                UIDevice.impact(.light)
                router.route(to: \.userSignIn, server)
            }
            .environment(\.isEnabled, !isEditingUsers)
        }
    }

    @ViewBuilder
    private var deleteUsersButton: some View {
        Button {
            isPresentingConfirmDeleteUsers = true
        } label: {
            ZStack {
                Color.red

                Text("Delete")
                    .font(.body.weight(.semibold))
                    .foregroundStyle(selectedUsers.isNotEmpty ? .primary : .secondary)

                if selectedUsers.isEmpty {
                    Color.black
                        .opacity(0.5)
                }
            }
            .clipShape(RoundedRectangle(cornerRadius: 10))
            .frame(height: 50)
            .frame(maxWidth: 400)
        }
        .disabled(selectedUsers.isEmpty)
        .buttonStyle(.plain)
    }

    // MARK: userView

    @ViewBuilder
    private var userView: some View {
        VStack(spacing: 0) {
            ZStack {
                Color.clear
                    .onSizeChanged { size, _ in
                        contentSize = size
                    }

                switch userListDisplayType {
                case .grid:
                    if UIDevice.isPhone {
                        phoneGridContentView
                    } else {
                        padGridContentView
                    }
                case .list:
                    listContentView
                }
            }
            .frame(maxHeight: .infinity)
            .mask {
                VStack(spacing: 0) {
                    Color.white

                    LinearGradient(
                        stops: [
                            .init(color: .white, location: 0),
                            .init(color: .clear, location: 1),
                        ],
                        startPoint: .top,
                        endPoint: .bottom
                    )
                    .frame(height: 30)
                }
            }

            if !isEditingUsers {
                ServerSelectionMenu(
                    selection: $serverSelection,
                    viewModel: viewModel
                )
                .edgePadding([.bottom, .horizontal])
            }

            if isEditingUsers {
                deleteUsersButton
                    .edgePadding([.bottom, .horizontal])
            }
        }
        .background {
            if selectUserUseSplashscreen, splashScreenImageSources.isNotEmpty {
                ZStack {
                    Color.clear

                    ImageView(splashScreenImageSources)
                        .pipeline(.Swiftfin.branding)
                        .aspectRatio(contentMode: .fill)
                        .id(splashScreenImageSources)
                        .transition(.opacity)
                        .animation(.linear, value: splashScreenImageSources)

                    Color.black
                        .opacity(0.9)
                }
                .ignoresSafeArea()
            }
        }
    }

    // MARK: emptyView

    @ViewBuilder
    private var emptyView: some View {
        VStack(spacing: 10) {
            L10n.connectToJellyfinServerStart.text
                .frame(minWidth: 50, maxWidth: 240)
                .multilineTextAlignment(.center)

            PrimaryButton(title: L10n.connect)
                .onSelect {
                    router.route(to: \.connectToServer)
                }
                .frame(maxWidth: 300)
        }
    }

    // MARK: body

    var body: some View {
        WrappedView {
            if viewModel.servers.isEmpty {
                emptyView
            } else {
                userView
            }
        }
        .ignoresSafeArea(.keyboard, edges: .bottom)
        .navigationTitle("Users")
        .navigationBarTitleDisplayMode(.inline)
        .toolbar {
            ToolbarItem(placement: .principal) {
                Image(uiImage: .jellyfinBlobBlue)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 30)
            }
        }
        .topBarTrailing {
            if isEditingUsers {
                Button {
                    isEditingUsers = false
                } label: {
                    L10n.cancel.text
                        .font(.headline)
                        .padding(.vertical, 5)
                        .padding(.horizontal, 10)
                        .background {
                            if colorScheme == .light {
                                Color.secondarySystemFill
                            } else {
                                Color.tertiarySystemBackground
                            }
                        }
                        .clipShape(RoundedRectangle(cornerRadius: 10))
                }
                .buttonStyle(.plain)
            } else {
                advancedMenu
            }
        }
        .onAppear {
            viewModel.send(.getServers)

            splashScreenImageSources = makeSplashScreenImageSources(
                serverSelection: serverSelection,
                allServersSelection: selectUserAllServersSplashscreen
            )
        }
        .onChange(of: isEditingUsers) { newValue in
            guard !newValue else { return }
            selectedUsers.removeAll()
        }
        .onChange(of: isPresentingConfirmDeleteUsers) { newValue in
            guard !newValue else { return }
            isEditingUsers = false
            selectedUsers.removeAll()
        }
        .onChange(of: isPresentingLocalPin) { newValue in
            if newValue {
                pin = ""
            } else {
                selectedUsers.removeAll()
            }
        }
        .onChange(of: selectUserAllServersSplashscreen) { newValue in
            splashScreenImageSources = makeSplashScreenImageSources(
                serverSelection: serverSelection,
                allServersSelection: newValue
            )
        }
        .onChange(of: serverSelection) { newValue in
            gridItems = makeGridItems(for: newValue)

            splashScreenImageSources = makeSplashScreenImageSources(
                serverSelection: newValue,
                allServersSelection: selectUserAllServersSplashscreen
            )
        }
        .onChange(of: viewModel.servers) { _ in
            gridItems = makeGridItems(for: serverSelection)
        }
        .onReceive(viewModel.events) { event in
            switch event {
            case let .error(eventError):
                UIDevice.feedback(.error)

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

                Defaults[.lastSignedInUserID] = .signedIn(userID: user.id)
                Container.shared.currentUserSession.reset()
                Notifications[.didSignIn].post()
            }
        }
        .onNotification(.didConnectToServer) { notification in
            if let server = notification.object as? ServerState {
                viewModel.send(.getServers)
                serverSelection = .server(id: server.id)
            }
        }
        .onNotification(.didChangeCurrentServerURL) { notification in
            if let server = notification.object as? ServerState {
                viewModel.send(.getServers)
                serverSelection = .server(id: server.id)
            }
        }
        .onNotification(.didDeleteServer) { notification in
            viewModel.send(.getServers)

            if let server = notification.object as? ServerState {
                if case let SelectUserServerSelection.server(id: id) = serverSelection, server.id == id {
                    if viewModel.servers.keys.count == 1, let first = viewModel.servers.keys.first {
                        serverSelection = .server(id: first.id)
                    } else {
                        serverSelection = .all
                    }
                }

                // change splash screen selection if necessary
                selectUserAllServersSplashscreen = serverSelection
            }
        }
        .alert(
            Text("Delete User"),
            isPresented: $isPresentingConfirmDeleteUsers,
            presenting: selectedUsers
        ) { selectedUsers in
            Button("Delete", role: .destructive) {
                viewModel.send(.deleteUsers(Array(selectedUsers)))
            }
        } message: { selectedUsers in
            if selectedUsers.count == 1, let first = selectedUsers.first {
                Text("Are you sure you want to delete \(first.username)?")
            } else {
                Text("Are you sure you want to delete \(selectedUsers.count) users?")
            }
        }
        .alert(
            L10n.error.text,
            isPresented: $isPresentingError,
            presenting: error
        ) { _ in
            Button(L10n.dismiss, role: .destructive)
        } message: { error in
            Text(error.localizedDescription)
        }
        .alert("Sign in", isPresented: $isPresentingLocalPin) {

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

            // bug in SwiftUI: having .disabled will dismiss
            // alert but not call the closure (for length)
            Button("Sign In") {
                guard let user = selectedUsers.first else {
                    assertionFailure("User not selected")
                    return
                }

                select(user: user, needsPin: false)
            }

            Button("Cancel", role: .cancel) {}
        } message: {
            if let user = selectedUsers.first, user.pinHint.isNotEmpty {
                Text(user.pinHint)
            } else {
                let username = selectedUsers.first?.username ?? .emptyDash

                Text("Enter pin for \(username)")
            }
        }
    }
}
