import SwiftUI
import CryptoKit

struct ContentView: View {
    
    @AppStorage("server")
    var serverPath: String = "https://christophhagen.de/sesame/"
    
    @AppStorage("localIP")
    var localAddress: String = "192.168.178.104/"

    @AppStorage("counter")
    var nextMessageCounter: Int = 0

    @AppStorage("compensate")
    var isCompensatingDaylightTime: Bool = false
    
    @AppStorage("local")
    private var useLocalConnection = false

    @State
    var keyManager = KeyManagement()

    let history = HistoryManager()
    
    @State
    var state: ClientState = .noKeyAvailable

    @State
    private var timer: Timer?

    @State
    private var hasActiveRequest = false

    @State
    private var responseTime: Date? = nil

    @State
    private var showSettingsSheet = false

    @State
    private var showHistorySheet = false
    
    @State
    private var didShowKeySheetOnce = false
    
    let server = Client()

    var compensationTime: UInt32 {
        isCompensatingDaylightTime ? 3600 : 0
    }

    var isPerformingRequests: Bool {
        hasActiveRequest ||
        state == .waitingForResponse
    }

    var buttonBackground: Color {
        state.allowsAction ?
            .white.opacity(0.2) :
            .black.opacity(0.2)
    }

    let buttonBorderWidth: CGFloat = 3

    var buttonColor: Color {
        state.allowsAction ? .white : .gray
    }

    private let buttonWidth: CGFloat = 250

    private let smallButtonHeight: CGFloat = 50

    private let smallButtonWidth: CGFloat = 120

    private let smallButtonBorderWidth: CGFloat = 1

    var body: some View {
        GeometryReader { geo in
            VStack(spacing: 20) {
                HStack {
                    Button("History", action: { showHistorySheet = true })
                        .frame(width: smallButtonWidth,
                               height: smallButtonHeight)
                        .background(.white.opacity(0.2))
                        .cornerRadius(smallButtonHeight / 2)
                        .overlay(RoundedRectangle(cornerRadius: smallButtonHeight / 2).stroke(lineWidth: smallButtonBorderWidth).foregroundColor(.white))
                        .foregroundColor(.white)
                        .font(.title2)
                        .padding()
                    Spacer()
                    Button("Settings", action: { showSettingsSheet = true })
                        .frame(width: smallButtonWidth,
                               height: smallButtonHeight)
                        .background(.white.opacity(0.2))
                        .cornerRadius(smallButtonHeight / 2)
                        .overlay(RoundedRectangle(cornerRadius: smallButtonHeight / 2).stroke(lineWidth: smallButtonBorderWidth).foregroundColor(.white))
                        .foregroundColor(.white)
                        .font(.title2)
                        .padding()
                }
                Spacer()
                if state.requiresDescription {
                    Text(state.description)
                        .padding()
                }
                Button(state.actionText, action: mainButtonPressed)
                    .frame(width: buttonWidth,
                           height: buttonWidth)
                    .background(buttonBackground)
                    .cornerRadius(buttonWidth / 2)
                    .overlay(RoundedRectangle(cornerRadius: buttonWidth / 2)
                        .stroke(lineWidth: buttonBorderWidth).foregroundColor(buttonColor))
                    .foregroundColor(buttonColor)
                    .font(.title)
                    .disabled(!state.allowsAction)
                    .padding(.bottom, (geo.size.width-buttonWidth) / 2)
            }
            .background(state.color)
            .onAppear {
                if keyManager.hasAllKeys {
                    state = .requestingStatus
                }
                startRegularStatusUpdates()
            }
            .onDisappear {
                endRegularStatusUpdates()
            }
            .frame(width: geo.size.width, height: geo.size.height)
            .animation(.easeInOut, value: state.color)
            .sheet(isPresented: $showSettingsSheet) {
                SettingsView(
                    keyManager: $keyManager,
                    serverAddress: $serverPath,
                    localAddress: $localAddress,
                    isCompensatingDaylightTime: $isCompensatingDaylightTime,
                    useLocalConnection: $useLocalConnection)
            }
            .sheet(isPresented: $showHistorySheet) {
                HistoryView(manager: history)
            }
        }
        .preferredColorScheme(.dark)
    }
    
    func mainButtonPressed() {
        guard let key = keyManager.get(.remoteKey),
              let token = keyManager.get(.authToken)?.data else {
            return
        }

        let count = UInt32(nextMessageCounter)
        let sentTime = Date()
        // Add time to compensate that the device is using daylight savings time
        let content = Message.Content(
            time: sentTime.timestamp + compensationTime,
            id: count)
        let message = content.authenticate(using: key)
        let historyItem = HistoryItem(sent: message.content, date: sentTime, local: useLocalConnection)
        state = .waitingForResponse
        print("Sending message \(count)")
        Task {
            let (newState, responseMessage) = await send(message, authToken: token)
            let receivedTime = Date.now
            responseTime = receivedTime
            state = newState
            let finishedItem = historyItem.didReceive(response: newState, date: receivedTime, message: responseMessage?.content)
            guard let key = keyManager.get(.deviceKey) else {
                save(historyItem: finishedItem.notAuthenticated())
                return
            }
            guard let responseMessage else {
                save(historyItem: finishedItem)
                return
            }
            guard responseMessage.isValid(using: key) else {
                save(historyItem: finishedItem.invalidated())
                return
            }
            
            nextMessageCounter = Int(responseMessage.content.id)
            save(historyItem: finishedItem)
        }
    }
    
    private func send(_ message: Message, authToken: Data) async -> (state: ClientState, response: Message?) {
        if useLocalConnection {
            return await server.sendMessageOverLocalNetwork(message, server: localAddress)
        } else {
            return await server.send(message, server: serverPath, authToken: authToken)
        }
    }

    private func save(historyItem: HistoryItem) {
        do {
            try history.save(item: historyItem)
        } catch {
            print("Failed to save item: \(error)")
        }
    }


    private func startRegularStatusUpdates() {
        guard timer == nil else {
            return
        }
        DispatchQueue.main.async {
            timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: checkDeviceStatus)
            timer!.fire()
        }
    }

    private func endRegularStatusUpdates() {
        timer?.invalidate()
        timer = nil
    }
    
    func checkDeviceStatus(_ timer: Timer) {
        guard !useLocalConnection else {
            return
        }
        guard let authToken = keyManager.get(.authToken) else {
            if !didShowKeySheetOnce {
                didShowKeySheetOnce = true
                //showSettingsSheet = true
            }
            return
        }
        guard !hasActiveRequest else {
            return
        }
        hasActiveRequest = true
        Task {
            let newState = await server.deviceStatus(authToken: authToken.data, server: serverPath)
            hasActiveRequest = false
            switch state {
            case .noKeyAvailable:
                return
            case .requestingStatus, .deviceNotAvailable, .ready:
                state = newState
            case .waitingForResponse:
                return
            case .messageRejected, .openSesame, .internalError, .responseRejected:
                guard let time = responseTime else {
                    state = newState
                    return
                }
                responseTime = nil
                // Wait at least 5 seconds after these states have been reached before changing the
                // interface to allow sufficient time to see the result
                let elapsed = Date.now.timeIntervalSince(time)
                guard elapsed < 5 else {
                    state = newState
                    return
                }
                let secondsToWait = Int(elapsed.rounded(.up))
                DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(secondsToWait)) {
                    state = newState
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .previewDevice("iPhone 8")
    }
}

extension Date {

    var timestamp: UInt32 {
        UInt32(timeIntervalSince1970.rounded())
    }

    init(timestamp: UInt32) {
        self.init(timeIntervalSince1970: TimeInterval(timestamp))
    }
}
