//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Combine
import MatrixRustSDK
import UIKit

class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
    private let window: UIWindow
    
    private let stateMachine: AppCoordinatorStateMachine
    
    private let mainNavigationController: UINavigationController
    private let splashViewController: UIViewController
    
    private let navigationRouter: NavigationRouter
    
    private let userSessionStore: UserSessionStoreProtocol
    
    private var userSession: UserSessionProtocol!
    
    private let memberDetailProviderManager: MemberDetailProviderManager

    private let bugReportService: BugReportServiceProtocol
    private let screenshotDetector: ScreenshotDetector
    private let backgroundTaskService: BackgroundTaskServiceProtocol

    private var indicatorPresenter: UserIndicatorTypePresenterProtocol
    private var loadingIndicator: UserIndicator?
    private var statusIndicator: UserIndicator?
    
    var childCoordinators: [Coordinator] = []
    
    init() {
        stateMachine = AppCoordinatorStateMachine()

        do {
            bugReportService = try BugReportService(withBaseUrlString: BuildSettings.bugReportServiceBaseUrlString,
                                                    sentryEndpoint: BuildSettings.bugReportSentryEndpoint)
        } catch {
            fatalError(error.localizedDescription)
        }

        splashViewController = SplashViewController()
        mainNavigationController = ElementNavigationController(rootViewController: splashViewController)
        window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = mainNavigationController
        window.tintColor = .element.accent
        
        navigationRouter = NavigationRouter(navigationController: mainNavigationController)
        
        memberDetailProviderManager = MemberDetailProviderManager()
        
        indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: mainNavigationController)
        
        guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
            fatalError("Should have a valid bundle identifier at this point")
        }

        backgroundTaskService = UIKitBackgroundTaskService(withApplication: UIApplication.shared)
        
        userSessionStore = UserSessionStore(bundleIdentifier: bundleIdentifier,
                                            backgroundTaskService: backgroundTaskService)

        screenshotDetector = ScreenshotDetector()
        screenshotDetector.callback = processScreenshotDetection
        
        setupStateMachine()
        
        setupLogging()
        
        // Benchmark.trackingEnabled = true
    }
    
    func start() {
        window.makeKeyAndVisible()
        stateMachine.processEvent(userSessionStore.hasSessions ? .startWithExistingSession : .startWithAuthentication)
    }
    
    // MARK: - AuthenticationCoordinatorDelegate
    
    func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didLoginWithSession userSession: UserSessionProtocol) {
        self.userSession = userSession
        remove(childCoordinator: authenticationCoordinator)
        stateMachine.processEvent(.succeededSigningIn)
    }
    
    // MARK: - Private
    
    private func setupLogging() {
        let loggerConfiguration = MXLogConfiguration()
        
        #if DEBUG
        // This exposes the full Rust side tracing subscriber filter for more flexibility.
        // We can filter by level, crate and even file. See more details here:
        // https://docs.rs/tracing-subscriber/0.2.7/tracing_subscriber/filter/struct.EnvFilter.html#examples
        setupTracing(configuration: "info,hyper=warn,sled=warn,matrix_sdk_sled=warn")
        
        loggerConfiguration.logLevel = .debug
        #else
        setupTracing(configuration: "info,hyper=warn,sled=warn,matrix_sdk_sled=warn")
        loggerConfiguration.logLevel = .info
        #endif
        
        // Avoid redirecting NSLogs to files if we are attached to a debugger.
        if isatty(STDERR_FILENO) == 0 {
            loggerConfiguration.redirectLogsToFiles = true
        }
      
        MXLog.configure(loggerConfiguration)
    }
    
    // swiftlint:disable cyclomatic_complexity
    private func setupStateMachine() {
        stateMachine.addTransitionHandler { [weak self] context in
            guard let self = self else { return }
            
            switch (context.fromState, context.event, context.toState) {
            case (.initial, .startWithAuthentication, .signedOut):
                self.startAuthentication()
            case (.signedOut, .succeededSigningIn, .homeScreen):
                self.presentHomeScreen()
                
            case (.initial, .startWithExistingSession, .restoringSession):
                self.showLoadingIndicator()
                self.restoreUserSession()
            case (.restoringSession, .failedRestoringSession, .signedOut):
                self.hideLoadingIndicator()
                self.showLoginErrorToast()
            case (.restoringSession, .succeededRestoringSession, .homeScreen):
                self.hideLoadingIndicator()
                self.presentHomeScreen()
            
            case(_, _, .roomScreen(let roomId)):
                self.presentRoomWithIdentifier(roomId)
            case(.roomScreen, .dismissedRoomScreen, .homeScreen):
                self.tearDownDismissedRoomScreen()
            case (_, .attemptSignOut, .signingOut):
                self.userSessionStore.logout(userSession: self.userSession)
                self.stateMachine.processEvent(.succeededSigningOut)
            case (.signingOut, .succeededSigningOut, .signedOut):
                self.tearDownUserSession()
            case (.signingOut, .failedSigningOut, _):
                self.showLogoutErrorToast()
            
            case (.homeScreen, .showSettingsScreen, .settingsScreen):
                self.presentSettingsScreen()
            case (.settingsScreen, .dismissedSettingsScreen, .homeScreen):
                self.tearDownDismissedSettingsScreen()
                
            case (.homeScreen, .showSessionVerificationScreen, .sessionVerificationScreen):
                self.presentSessionVerification()
            case (.sessionVerificationScreen, .dismissedSessionVerificationScreen, .homeScreen):
                self.tearDownDismissedSessionVerificationScreen()
                
            default:
                fatalError("Unknown transition: \(context)")
            }
        }
        
        stateMachine.addErrorHandler { context in
            fatalError("Failed transition with context: \(context)")
        }
    }

    // swiftlint:enable cyclomatic_complexity function_body_length
    
    private func restoreUserSession() {
        Task {
            switch await userSessionStore.restoreUserSession() {
            case .success(let userSession):
                self.userSession = userSession
                stateMachine.processEvent(.succeededRestoringSession)
            case .failure:
                MXLog.error("Failed to restore an existing session.")
                stateMachine.processEvent(.failedRestoringSession)
            }
        }
    }
    
    private func startAuthentication() {
        let authenticationService = AuthenticationServiceProxy(userSessionStore: userSessionStore)
        let coordinator = AuthenticationCoordinator(authenticationService: authenticationService,
                                                    navigationRouter: navigationRouter)
        coordinator.delegate = self
        
        add(childCoordinator: coordinator)
        coordinator.start()
    }
    
    private func tearDownUserSession() {
        if let presentedCoordinator = childCoordinators.first {
            remove(childCoordinator: presentedCoordinator)
        }
        
        userSession = nil
        
        mainNavigationController.setViewControllers([splashViewController], animated: false)
        
        startAuthentication()
    }
    
    private func presentHomeScreen() {
        let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
                                                         attributedStringBuilder: AttributedStringBuilder(),
                                                         memberDetailProviderManager: memberDetailProviderManager)
        let coordinator = HomeScreenCoordinator(parameters: parameters)
        
        coordinator.callback = { [weak self] action in
            guard let self = self else { return }
            
            switch action {
            case .presentRoom(let roomIdentifier):
                self.stateMachine.processEvent(.showRoomScreen(roomId: roomIdentifier))
            case .presentSettings:
                self.stateMachine.processEvent(.showSettingsScreen)
            case .verifySession:
                self.stateMachine.processEvent(.showSessionVerificationScreen)
            }
        }
        
        add(childCoordinator: coordinator)
        navigationRouter.setRootModule(coordinator)

        if bugReportService.crashedLastRun {
            showCrashPopup()
        }
    }
    
    // MARK: Rooms

    private func presentRoomWithIdentifier(_ roomIdentifier: String) {
        guard let roomProxy = userSession.clientProxy.rooms.first(where: { $0.id == roomIdentifier }) else {
            MXLog.error("Invalid room identifier: \(roomIdentifier)")
            return
        }
        let userId = userSession.clientProxy.userIdentifier
        
        let memberDetailProvider = memberDetailProviderManager.memberDetailProviderForRoomProxy(roomProxy)
        
        let timelineItemFactory = RoomTimelineItemFactory(mediaProvider: userSession.mediaProvider,
                                                          memberDetailProvider: memberDetailProvider,
                                                          attributedStringBuilder: AttributedStringBuilder())
        
        let timelineController = RoomTimelineController(userId: userId,
                                                        timelineProvider: RoomTimelineProvider(roomProxy: roomProxy),
                                                        timelineItemFactory: timelineItemFactory,
                                                        mediaProvider: userSession.mediaProvider,
                                                        memberDetailProvider: memberDetailProvider)
        
        let parameters = RoomScreenCoordinatorParameters(timelineController: timelineController,
                                                         roomName: roomProxy.displayName ?? roomProxy.name,
                                                         roomAvatar: userSession.mediaProvider.imageFromURLString(roomProxy.avatarURL),
                                                         roomEncryptionBadge: roomProxy.encryptionBadgeImage)
        let coordinator = RoomScreenCoordinator(parameters: parameters)
        
        add(childCoordinator: coordinator)
        navigationRouter.push(coordinator) { [weak self] in
            guard let self = self else { return }
            self.stateMachine.processEvent(.dismissedRoomScreen)
        }
    }
    
    private func tearDownDismissedRoomScreen() {
        guard let coordinator = childCoordinators.last as? RoomScreenCoordinator else {
            fatalError("Invalid coordinator hierarchy: \(childCoordinators)")
        }
        
        remove(childCoordinator: coordinator)
    }
    
    // MARK: Settings
    
    private func presentSettingsScreen() {
        let parameters = SettingsCoordinatorParameters(navigationRouter: navigationRouter,
                                                       userSession: userSession,
                                                       bugReportService: bugReportService)
        let coordinator = SettingsCoordinator(parameters: parameters)
        coordinator.callback = { [weak self] action in
            guard let self = self else { return }
            switch action {
            case .logout:
                self.stateMachine.processEvent(.attemptSignOut)
            }
        }

        add(childCoordinator: coordinator)
        coordinator.start()
        navigationRouter.push(coordinator) { [weak self] in
            guard let self = self else { return }

            self.stateMachine.processEvent(.dismissedSettingsScreen)
        }
    }
        
    private func tearDownDismissedSettingsScreen() {
        guard let coordinator = childCoordinators.last as? SettingsCoordinator else {
            fatalError("Invalid coordinator hierarchy: \(childCoordinators)")
        }

        remove(childCoordinator: coordinator)
    }
    
    private func showCrashPopup() {
        let alert = UIAlertController(title: nil,
                                      message: ElementL10n.sendBugReportAppCrashed,
                                      preferredStyle: .alert)

        alert.addAction(UIAlertAction(title: ElementL10n.no, style: .cancel))
        alert.addAction(UIAlertAction(title: ElementL10n.yes, style: .default) { [weak self] _ in
            self?.presentBugReportScreen()
        })

        navigationRouter.present(alert, animated: true)
    }

    private func processScreenshotDetection(image: UIImage?, error: Error?) {
        MXLog.debug("Detected screenshot: \(String(describing: image)), error: \(String(describing: error))")

        let alert = UIAlertController(title: ElementL10n.screenshotDetectedTitle,
                                      message: ElementL10n.screenshotDetectedMessage,
                                      preferredStyle: .alert)

        alert.addAction(UIAlertAction(title: ElementL10n.no, style: .cancel))
        alert.addAction(UIAlertAction(title: ElementL10n.yes, style: .default) { [weak self] _ in
            self?.presentBugReportScreen(for: image)
        })

        navigationRouter.present(alert, animated: true)
    }

    private func presentBugReportScreen(for image: UIImage? = nil) {
        let parameters = BugReportCoordinatorParameters(bugReportService: bugReportService,
                                                        screenshot: image)
        let coordinator = BugReportCoordinator(parameters: parameters)
        coordinator.completion = { [weak self, weak coordinator] in
            guard let self = self, let coordinator = coordinator else { return }
            self.navigationRouter.dismissModule(animated: true)
            self.remove(childCoordinator: coordinator)
        }

        add(childCoordinator: coordinator)
        coordinator.start()
        let navController = ElementNavigationController(rootViewController: coordinator.toPresentable())
        navController.navigationBar.topItem?.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel,
                                                                                 target: self,
                                                                                 action: #selector(dismissBugReportScreen))
        navController.isModalInPresentation = true
        navigationRouter.present(navController, animated: true)
    }

    @objc
    private func dismissBugReportScreen() {
        MXLog.debug("dismissBugReportScreen")

        guard let bugReportCoordinator = childCoordinators.first(where: { $0 is BugReportCoordinator }) else {
            return
        }

        navigationRouter.dismissModule()
        remove(childCoordinator: bugReportCoordinator)
    }
    
    // MARK: Session verification
        
    private func presentSessionVerification() {
        Task {
            guard let sessionVerificationController = userSession.sessionVerificationController else {
                fatalError("The sessionVerificationController should aways be valid at this point")
            }
            
            let parameters = SessionVerificationCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController)
            
            let coordinator = SessionVerificationCoordinator(parameters: parameters)
            
            coordinator.callback = { [weak self] in
                self?.navigationRouter.dismissModule()
                self?.stateMachine.processEvent(.dismissedSessionVerificationScreen)
            }
            
            add(childCoordinator: coordinator)
            navigationRouter.present(coordinator)

            coordinator.start()
        }
    }
    
    private func tearDownDismissedSessionVerificationScreen() {
        guard let coordinator = childCoordinators.last as? SessionVerificationCoordinator else {
            fatalError("Invalid coordinator hierarchy: \(childCoordinators)")
        }

        remove(childCoordinator: coordinator)
    }
    
    // MARK: Toasts and loading indicators
    
    private func showLoadingIndicator() {
        loadingIndicator = indicatorPresenter.present(.loading(label: ElementL10n.loading, isInteractionBlocking: true))
    }
    
    private func hideLoadingIndicator() {
        loadingIndicator = nil
    }
    
    private func showLoginErrorToast() {
        statusIndicator = indicatorPresenter.present(.error(label: "Failed logging in"))
    }
    
    private func showLogoutErrorToast() {
        statusIndicator = indicatorPresenter.present(.error(label: "Failed logging out"))
    }
}
