//
//  DataManager.swift
//  DocumentRepositoryMobile
//
//  Created by Tobias on 23.10.18.
//  Copyright © 2018 Tobias. All rights reserved.
//

import Foundation
import Future
import os.log

public protocol HttpResponseFilter
{
    func handleResponse(request: Request, data: Data?, responseHandler: ResponseHandler?, sender: Any?, promise: Promise<Data?, ResponseError>)
}

public protocol HttpSuccessFilter
{
    func handleResponse(data: Data?, response: HTTPURLResponse, responseHandler: ResponseHandler?, sender: Any?, promise: Promise<Data?, ResponseError>)
}

public protocol DataManagerDelegate
{
    func manipulateRequest(request: Request) -> Request
}

public class DataManager: NSObject, URLSessionDelegate
{
    public enum DataManagerError: Error
    {
        case authError
    }

    public struct Host: Equatable
    {
        public let `protocol`: String
        public let host: String
        public let port: Int
        public let context: String
        
        public init(`protocol`: String, host: String, port: Int, context: String) {
            self.protocol = `protocol`
            self.host = host
            self.port = port
            self.context = context
        }
    }

    private let timeout: TimeInterval
    public private(set) var host: Host

    private lazy var urlSession: URLSession = {
        URLSession(configuration: .default, delegate: self, delegateQueue: OperationQueue.main)
    }()

    private var responseOkFilter: HttpSuccessFilter
    private var responseFilters: [Int: HttpResponseFilter] = [:]

    public var delegate: DataManagerDelegate?
    
    public init(host: Host, responseOkFilter: HttpSuccessFilter, responseFilters: [Int: HttpResponseFilter] = [:], timeout: TimeInterval = 10.0) {
        self.host = host
        self.responseOkFilter = responseOkFilter
        self.responseFilters = responseFilters
        self.timeout = timeout
        super.init()
    }

    public func url(username: String = "", password: String = "", path: String) -> String {
        var url: String = host.protocol

        if !username.isEmpty || !password.isEmpty {
            url += username + ":" + password + "@"
        }

        url += "\(host.host):\(host.port)\(host.context)"
        url += path

        return url
    }

    @discardableResult
    public func request(request r: Request,
                 responseHandler: ResponseHandler? = nil,
                 sender: Any? = nil,
                 promise: Promise<Data?, ResponseError> = Promise<Data?, ResponseError>())
                    -> Future<Data?, ResponseError> {
        if #available(iOS 14.0, *) {
            os_log("Request: \(r)")
        } else {
            print("Request: \(r)")
        }

        let request = delegate?.manipulateRequest(request: r) ?? r
        
        if var req: URLRequest = createUrlRequest(request: request) {
            if let authentication = request.authentication {
                if let headerKey = authentication.headerKey, let headerValue = authentication.headerValue {
                    req.setValue(headerValue, forHTTPHeaderField: headerKey)
                }
            }

            let task = urlSession.dataTask(with: req) { data, response, error in
                if let error: NSError = error as NSError? {
                    promise.fail(error: self.determineResponseError(error))
                    return
                }

                guard let response: HTTPURLResponse = response as? HTTPURLResponse else {
                    promise.fail(error: ResponseError.networkError(error: .unknown()))
                    return
                }

                if response.is2xx {
                    self.responseOkFilter.handleResponse(data: data, response: response, responseHandler: responseHandler, sender: sender, promise: promise)
                } else if let filter = self.responseFilters[response.statusCode] {
                    filter.handleResponse(request: request, data: data, responseHandler: responseHandler, sender: sender, promise: promise)
                } else {
                    promise.fail(error: ResponseError.getErrorForStatusCode(code: response.statusCode))
                }
            }
            task.resume()
        }
        return promise.future
    }

    private func createUrlRequest(request: Request) -> URLRequest? {
        if let url: URL = request.constructUrl() {
            var req: URLRequest = URLRequest(url: url)
            req.httpMethod = request.method.rawValue

            if let payload = request.payload {
                req.httpBody = payload
                req.setValue(request.contentType, forHTTPHeaderField: "Content-Type")
                req.setValue("application/json", forHTTPHeaderField: "Accept")
            }

            req.timeoutInterval = self.timeout
            return req
        }
        return nil
    }

    fileprivate func determineResponseError(_ error: NSError) -> ResponseError {
        if error.code == NSURLErrorNotConnectedToInternet {
            return ResponseError.networkError(error: .noInternet)
        } else if error.code == NSURLErrorTimedOut {
            return ResponseError.networkError(error: .timeout)
        } else {
            return ResponseError.networkError(error: .unknown(error: error))
        }
    }

    public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
    }
}