//
//  OpenWeatherStar.swift
//  Version: 0.1.1
//
//  Created by Mikhail Zhigulin in 7531.
//
//  Copyright © 7531 Mikhail Zhigulin of Novosibirsk.
//  All rights reserved.
//
//
//  MIT License
//
//  Copyright © 7531 Mikhail Zhigulin of Novosibirsk
//
//  The year starts from the creation of the world according to a Slavic calendar.
//  September, the 1st of Slavic year.
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in all
//  copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//  SOFTWARE.
//
// swiftlint:disable file_length closure_parameter_position
//

import Foundation

public class OpenWeatherFreeClient: FreeNetworkClient {

    public func call(with respect: OpenWeatherDetails) throws {
        guard let requestURL = URL(string: respect.urlString) else {
            throw NetworkClientError.invalidUrl
        }

        requestData(url: requestURL)
    }
}

public enum NetworkClientError: Error, Equatable {
    case invalidUrl
    case failedRequest(String)
    case statusCode404
    case failedResponse(String)
}

public enum Result<Value, Error: Swift.Error> {
    case success(Value)
    case failure(Error)
}

public class FreeNetworkClient {

    private(set) var dataTask: URLSessionDataTask?
    private(set) var session: URLSession

    public var onDataGiven: (Result<Data, NetworkClientError>) -> Void = { result in
        switch result {
        case .success(let weatherData):
            print("""
            — Default closure invoked! \(#function): \(result)
              DATA: BEGIN
              \(String(decoding: weatherData, as: UTF8.self))
              DATA: END
            """)
        case .failure(let error):
            switch error {
            case .failedRequest(let message):
                log.message(message, .error)
            default:
                break
            }
        }
    }

    public var data: Data { return networkData }
    private(set) var networkData: Data = Data() {
        didSet {
            onDataGiven(.success(networkData))
        }
    }

    public init(_ session: URLSession = URLSession.shared) {
        self.session = session
    }

    internal func requestData(url: URL) {

        log.message("[\(type(of: self))].\(#function)")

        dataTask = session.dataTask(with: URLRequest(url: url)) {
            [self] (requestedData: Data?, response: URLResponse?, error: Error?) -> Void in

            // Answer

            var answerData: Data?
            var answerError: NetworkClientError?

            // Check Status

            if let error = error {
                answerError = .failedResponse(error.localizedDescription)
                // WRONG: https://apiiiii.openweathermap.org/...
            } else {
                if let statusCode = (response as? HTTPURLResponse)?.statusCode {
                    if statusCode == 404 {
                        answerError = .statusCode404
                        // WRONG: https://api.openweathermap.org/data/999/...
                    } else if statusCode != 200 {
                        answerError = .failedResponse(
                            HTTPURLResponse.localizedString(forStatusCode: statusCode))
                        // WRONG: https://api.openweathermap.org/...&appid=wrong_api_key
                    }
                } else {
                    answerError = .failedResponse("No Status Code")
                }
            }

            // Data

            answerData = requestedData ?? Data()

            // Communicate Changes

            if let error = answerError {
                self.onDataGiven(.failure(error))
            } else if let data = answerData {
                self.networkData = data
            }

            self.dataTask = nil
        }

        dataTask?.resume()
    }
}

public let weatherSchemeBase = "https://api.openweathermap.org/data/2.5/"
public let weatherSchemeAttributes = "%@?lat=%@&lon=%@&appid=%@"

public enum OpenWeatherURLFormat: String {
    case currentWeather = "weather" // Default.
    case forecast = "forecast"
}

public enum Units: String {
    case standard // Default.
    case metric
    case imperial
}

public enum Mode: String {
    case json // Default.
    case xml
    case html
}

public struct Lang: RawRepresentable {
    public var rawValue: String
    public static let byDefault = Lang(rawValue: "")

    public init(rawValue: String) {
        self.rawValue = rawValue
    }
}

extension Lang {
    public static let en = Lang(rawValue: "en")
    public static let ru = Lang(rawValue: "ru")
}

public struct OpenWeatherDetails {

    public let appid: String
    public let format: OpenWeatherURLFormat

    public let lat: String
    public let lon: String

    public let units: Units
    public let lang: Lang
    public let mode: Mode

    // A number of timestamps, which will be returned in the API response.
    public var cnt: Int = -1

    public init(appid: String, format: OpenWeatherURLFormat = .currentWeather,
                lat: String = "55.66", lon: String = "85.62", units: Units = .standard,
                lang: Lang = Lang.byDefault, mode: Mode = Mode.json) {

        self.appid = appid
        self.format = format
        self.lat = lat
        self.lon = lon
        self.units = units
        self.lang = lang
        self.mode = mode
    }

    public var urlString: String {

        let args: [String] = [format.rawValue, lat, lon, appid]
        var attributes = String(format: weatherSchemeAttributes, arguments: args)

        if !lang.rawValue.isEmpty {
            attributes.append("&lang=\(lang.rawValue)")
        }

        if format == .forecast && cnt != -1 {
            attributes.append("&cnt=\(cnt)")
        }

        if mode != .json {
            attributes.append("&mode=\(mode.rawValue)")
        }

        if units != .standard {
            attributes.append("&units=\(units.rawValue)")
        }

        return weatherSchemeBase + attributes
    }
}