//
/*
 * Copyright (c) 2021 BWI GmbH
 *
 * 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 Foundation
import UIKit
import MatrixSDK
import SwiftUI


fileprivate let BWI_ServerDowntimes = "BWI_ServerDowntimes"
fileprivate let BWI_ValidAppVersions = "BWI_ValidAppVersions"
fileprivate let exampleFile = "exampleDowntime"

fileprivate let maintenanceURL = "/_matrix/cmaintenance"

@objcMembers class ServerDowntimeDefaultService : NSObject {
    var isCurrentDowntime: ServerDowntimeStatus = .none
    
    static var manuallyIgnored: Bool = false
    
    /// Find the next relevant downtime
    /// - Returns: ServerDowntime or nil, if there is no downtime in the future or the warning date is still in the future
    func nextDowntime() -> ServerDowntime? {
        guard let encodedDowntimes = UserDefaults.standard.object(forKey: BWI_ServerDowntimes) as? [Data] else {
            return nil
        }
        
        // needs to split up because compiler gets confused
        var serverDowntimes = encodedDowntimes
            .compactMap { try? JSONDecoder().decode(ServerDowntime.self, from: $0) }
         
        serverDowntimes = serverDowntimes
            .filter {
                // remove downtimes with invalid values
                return ($0.type.elementsEqual(ServerDowntimeType.adhocMessage.rawValue) ||              $0.type.elementsEqual(ServerDowntimeType.maintenance.rawValue)) &&
                    $0.startTime.iso8601Date != nil &&
                    $0.endTime.iso8601Date != nil &&
                $0.warningStartTime.iso8601Date != nil
            }
            .filter {
                // remove downtimes that are already done
                guard let downtimeEndDate = $0.endTime.iso8601Date else {
                    return false
                }
                return downtimeEndDate.timeIntervalSinceNow >= 0
            }
            .filter {
                // remove downtimes where the warning time is still in the future
                guard let warningStartDate = $0.warningStartTime.iso8601Date else {
                    return false
                }
                return warningStartDate.timeIntervalSinceNow <= 0
            }
        
        // filter Maintenance with wrong date order
        serverDowntimes = serverDowntimes
            .filter {
                guard let startDate = $0.startTime.iso8601Date, let endDate = $0.endTime.iso8601Date, let warningDate = $0.warningStartTime.iso8601Date else {
                    return false
                }
                
                return (warningDate <= startDate) && (startDate <= endDate)
            }
        
        // filter empty messages on ADMHOCMESSAGE type
        
        serverDowntimes = serverDowntimes
            .filter {
                if $0.type.elementsEqual(ServerDowntimeType.adhocMessage.rawValue) {
                    if let description = $0.description, description.count > 0 {
                        return true
                    } else {
                        return false
                    }
                }
                return true
            }
        
        serverDowntimes = serverDowntimes
            .sorted {
                // sort downtimes be startTime
                guard let downtimeStartDate1 = $0.startTime.iso8601Date, let downtimeStartDate2 = $1.startTime.iso8601Date else {
                    return false
                }
                return downtimeStartDate1 < downtimeStartDate2
            }
        
        return serverDowntimes.first
    }
    
    /// Check the state of the next downtime
    /// - Returns: ongoing, warning or none
    func nextDowntimeStatus() -> ServerDowntimeStatus {
        guard let downTime = self.nextDowntime() else {
            return .none
        }

        if let startDate = downTime.startTime.iso8601Date {
            if startDate.timeIntervalSinceNow < 0 {
                return .ongoing
            }
        }
        return .warning
    }
    
    private func loadTestMaintanceJSON(forName name: String) -> Data? {
        if let path = Bundle.main.path(forResource: name, ofType: "json") {
            do {
                let jsonString = try String(contentsOfFile: path)
                return jsonString.data(using: .utf8)
            } catch {
                print(error.localizedDescription)
            }
        }
        return nil
    }
    
    // MARK: - Handle Server Response
    
    func copyFetchResultToUserDefaults(fetchResult: [String : Any]) throws {
        if let downtimes = fetchResult["downtime"] as? [[String : Any]] {
            try copyServerDowntimesToUserDefaults(downtimes: downtimes)
        }
        
        if let versions = fetchResult["versions"] as? [String : Any] {
            try copyValidAppVersionsToUserDefaults(versions: versions)
        }
    }
    
    private func copyServerDowntimesToUserDefaults(downtimes: [[String : Any]]) throws {
        var encodedDowntimes: [Data] = []
        
        for downtimeDict in downtimes {
            if let serverDowntime = try? ServerDowntime(dict: downtimeDict) {
                let data = try JSONEncoder().encode(serverDowntime)
                encodedDowntimes.append(data)
            }
        }
        UserDefaults.standard.set(encodedDowntimes, forKey: BWI_ServerDowntimes)
    }
    
    private func copyValidAppVersionsToUserDefaults(versions: [String : Any]) throws {
        if let iosVersionsDict = versions["ios"] as? [String : Any] {
            if let validAppVersions = try? ValidAppVersions(dict: iosVersionsDict) {
                let data = try JSONEncoder().encode(validAppVersions)
                UserDefaults.standard.set(data, forKey: BWI_ValidAppVersions)
            }
        }
    }
    
    // MARK: - Mockup Fetch
    
    func fetchDowntimes(completion: @escaping () -> Void) {
        if let exampleFetch = loadTestMaintanceJSON(forName: exampleFile) {
            do {
                if let fetchResult =  try JSONSerialization.jsonObject(with: exampleFetch, options: []) as? [String: Any] {
                    try copyFetchResultToUserDefaults(fetchResult: fetchResult)
                }
            } catch {
                print(error.localizedDescription)
            }
        }
        completion()
    }

}

// MARK: -

extension ServerDowntimeDefaultService : ServerDowntimeService {

    func alert(alertType: ServerMaintenanceAlertType, completion: @escaping () -> Void) -> Alert {
        switch alertType {
        case .showInvalidAppVersionAlert:
            return Alert(title: Text(BWIL10n.bwiOutdatedVersionWarningTitle),
                         message: Text(BWIL10n.bwiOutdatedVersionWarningMessage(AppInfo.current.displayName)),
                         dismissButton: .destructive(Text(BWIL10n.bwiOutdatedVersionAppstoreButton), action: {
                            let iTunesLink = BWIBuildSettings.shared.itunesAppLink
                            UIApplication.shared.vc_open(URL(string: iTunesLink)!, completionHandler: nil)
                   }))
        case .showDowntimeTimeAlert:
            if BWIBuildSettings.shared.ignoreBlockingMaintenance && isBlocking() {
                return Alert(       title: Text(""),
                                  message: Text(ServerDowntimeDefaultService().downtimeText()),
                            primaryButton: .cancel(Text(BWIL10n.blockingDowntimeAlertIgnoreButton)) {
                                UserDefaults.standard.set(false, forKey: "ServerDownTimeBlockingKey")
                                self.setManuallyIgnored()
                                completion()
                            },
                         secondaryButton: .destructive(Text(BWIL10n.blockingDowntimeAlertDismissButton))
                        )

            } else {
                return Alert(title: Text(BWIL10n.downtimeTitle),
                             message: Text(ServerDowntimeDefaultService().downtimeText() != "" ? BWIL10n.downtimeDefaultMessage + "\n\n" + ServerDowntimeDefaultService().downtimeText() : BWIL10n.downtimeDefaultMessage),
                            dismissButton: .destructive(Text(isBlocking() ? BWIL10n.blockingDowntimeAlertDismissButton : BWIL10n.downtimeAlertDismissButton)) {
                            if self.isBlocking() {
                                return
                            } else {
                                completion()
                            }
                })
            }
        case .showServerMaintenanceInfoMessageAlert:
            if BWIBuildSettings.shared.ignoreBlockingMaintenance && isBlocking() {
                return Alert(       title: Text(""),
                                  message: Text(ServerDowntimeDefaultService().downtimeText()),
                            primaryButton: .cancel(Text(BWIL10n.blockingDowntimeAlertIgnoreButton)) {
                    completion()
                            },
                         secondaryButton: .destructive(Text(BWIL10n.blockingDowntimeAlertDismissButton))
                        )

            } else {
                return Alert(title: Text(""),
                             message: Text(ServerDowntimeDefaultService().downtimeText()),
                             dismissButton: .destructive(Text(isBlocking() ? BWIL10n.blockingDowntimeAlertDismissButton : BWIL10n.downtimeAlertDismissButton)) {
                    if self.isBlocking() {
                        return
                    } else {
                        completion()
                    }
                })
            }
        case .showServerMaintenanceDefaultAlert:
            return Alert(title: Text(BWIL10n.downtimeTitle),
                         message: Text(BWIL10n.downtimeDefaultMessage),
                         dismissButton: .destructive(Text(BWIL10n.downtimeAlertDismissButton)) {
                completion()
                   })
        }
    }
    
    func showAlert() -> Bool {
        return nextDowntimeStatus() == .ongoing
    }
    
    func alertType() -> ServerMaintenanceAlertType {
        if downtimeType() == .adhocMessage {
            return .showServerMaintenanceInfoMessageAlert
        } else {
            return .showDowntimeTimeAlert
        }
    }
    
    func fetchDowntimes(session: MXSession, completion: @escaping () -> Void) {
        session.matrixRestClient.getDowntime(completion: { (jsonResponse, error) in
            do {
                if let fetchResult = jsonResponse as? [String : Any] {
                    try self.copyFetchResultToUserDefaults(fetchResult: fetchResult)
                }
            } catch {
                print(error.localizedDescription)
            }
            completion()
        })
    }
    
    func loadDowntimes(downtimes: [[String : Any]]) throws {
        try copyServerDowntimesToUserDefaults(downtimes: downtimes)
    }
    
    func fetchDowntimesWithDirectRequest(localUrlString: String? = nil, completion: @escaping (Bool) -> Void) {
        var urlString = AppConfigService.shared.serverUrl()
        
        if let localUrlString = localUrlString {
            urlString = localUrlString
        }
        
        guard let url = URL(string: urlString + maintenanceURL) else {
            completion(false)
            return
        }
        
        let config = URLSessionConfiguration.default
        config.httpAdditionalHeaders = ["User-Agent": UserAgentService().bwiUserAgent]
        let session : URLSession = URLSession(configuration: config)
        session.configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
        
        let task = session.dataTask(with: url) { data, response, error in
            guard let response = response as? HTTPURLResponse, response.statusCode == 200 else  {
                completion(false)
                return
            }
            
            do {
                if let data = data, let fetchResult = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
                    try self.copyFetchResultToUserDefaults(fetchResult: fetchResult)
                }
            } catch  {
                print(error.localizedDescription)
            }
            
            completion(true)
        }
        task.resume()
    }

    // MARK: -
    
    @objc func isDowntimePresentable() -> Bool {
        return nextDowntimeStatus() != .none
    }
    
    @objc func downtimeText() -> String {
        guard let downTime = self.nextDowntime() else {
            return ""
        }
        switch downtimeType() {
            case .adhocMessage:
                guard let description = downTime.description else {
                    return ""
                }
                return description
            case .maintenance:
                guard let startDate = downTime.startTime.iso8601Date, let endDate = downTime.endTime.iso8601Date else {
                    return ""
                }
                
                let dayOfWeekFormatter = DateFormatter()
                dayOfWeekFormatter.dateFormat = "EEEE"
                dayOfWeekFormatter.timeZone = TimeZone.current
                dayOfWeekFormatter.calendar = Calendar.current
                
                let dateFormatter = DateFormatter()
                dateFormatter.dateFormat = "dd.MM.yyyy"
                dateFormatter.timeZone = TimeZone.current
                dateFormatter.calendar = Calendar.current
                
                let dateTimeFormatter = DateFormatter()
                dateTimeFormatter.dateFormat = "dd.MM.yyyy HH:mm"
                dateTimeFormatter.timeZone = TimeZone.current
                dateTimeFormatter.calendar = Calendar.current
                
                let timeFormatter = DateFormatter()
                timeFormatter.dateFormat = "HH:mm"
                timeFormatter.timeZone = TimeZone.current
                timeFormatter.calendar = Calendar.current
                
                let utcFormatter = DateFormatter()
                utcFormatter.dateFormat = "ZZZZZ"
                utcFormatter.timeZone = TimeZone.current
                utcFormatter.calendar = Calendar.current
                    
                if self.isSameDay() {
                    return BWIL10n.settingsDowntimeMessageSameDay(AppInfo.current.displayName,
                                                                dayOfWeekFormatter.string(from: startDate),
                                                                dateFormatter.string(from: startDate),
                                                                timeFormatter.string(from: startDate),
                                                                timeFormatter.string(from: endDate),
                                                                  utcFormatter.string(from: startDate)).appending(BWIBuildSettings.shared.showMaintenanceInfoMessageType ? "\n\n\(downTime.description ?? "")" : "")
                } else {
                    return BWIL10n.settingsDowntimeMessageDifferentDays(AppInfo.current.displayName,
                                                                        dayOfWeekFormatter.string(from: startDate),
                                                                        dateTimeFormatter.string(from: startDate),
                                                                        utcFormatter.string(from: startDate),
                                                                        dayOfWeekFormatter.string(from: endDate),
                                                                        dateTimeFormatter.string(from: endDate),
                                                                        utcFormatter.string(from: endDate)).appending(BWIBuildSettings.shared.showMaintenanceInfoMessageType ? "\n\n\(downTime.description ?? "")" : "")
                }
        }
    }
    
    @objc func isDowntimeNow() -> Bool {
        guard let downTime = self.nextDowntime(), let startDate = downTime.startTime.iso8601Date, let endDate = downTime.endTime.iso8601Date else {
            return false
        }
        return startDate <= Date() && Date() <= endDate
    }
    
    @objc func downtimeColor() -> UIColor {
        switch nextDowntimeStatus() {
        case .warning:
            return .yellow
        case .ongoing:
            if isBlocking() {
                return .red
            } else {
                return .yellow
            }
        default:
            return .clear
        }
    }
    
    @objc func downtimeTextColor() -> UIColor {
        switch nextDowntimeStatus() {
        case .warning:
            return .black
        case .ongoing:
            if isBlocking() {
                return .white
            } else {
                return .black
            }
        default:
            return .clear
        }
    }
    
    func downtimeType() -> ServerDowntimeType {
        guard let downTime = self.nextDowntime() else {
            return ServerDowntimeType.maintenance
        }
        if BWIBuildSettings.shared.showMaintenanceInfoMessageType && downTime.type.elementsEqual(ServerDowntimeType.adhocMessage.rawValue) {
            return ServerDowntimeType.adhocMessage
        } else {
            return ServerDowntimeType.maintenance
        }
    }
    
    @objc func isSameDay() -> Bool {
        guard let downTime = self.nextDowntime(), let startDate = downTime.startTime.iso8601Date, let endDate = downTime.endTime.iso8601Date else {
            return false
        }
        
        return Calendar.current.dateComponents([.day], from: startDate, to: endDate).day == 0
    }
    
    @objc func isBlocking() -> Bool {
        guard let downTime = self.nextDowntime() else {
            return false
        }
        
        return downTime.blocking ?? false
    }
    
    func setManuallyIgnored() {
        guard let downTime = self.nextDowntime(), let blocking = downTime.blocking else {
            return
        }
        
        if blocking && BWIBuildSettings.shared.ignoreBlockingMaintenance {
            ServerDowntimeDefaultService.manuallyIgnored = true
        }
    }
    
    func isManuallyIgnored() -> Bool {
        return ServerDowntimeDefaultService.manuallyIgnored
    }
}
