// OpenAPITransport.swift
//
// Generated by openapi-generator
// https://openapi-generator.tech

import Foundation
import Combine

// MARK: - OpenAPITransport

public protocol OpenAPITransport: AnyObject {
    var baseURL: URL? { get }

    func send(request: URLRequest) -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError>

    func cancelAll()
}

public struct OpenAPITransportResponse {
    public let data: Data
    public let statusCode: Int

    public init(data: Data, statusCode: Int) {
        self.data = data
        self.statusCode = statusCode
    }
}

public struct OpenAPITransportError: Error, CustomStringConvertible, LocalizedError {
    public let statusCode: Int
    public let description: String
    public let errorDescription: String?
    /// It might be source network error
    public let nestedError: Error?
    /// Data may contain additional reason info (like json payload)
    public let data: Data

    public init(
        statusCode: Int,
        description: String? = nil,
        errorDescription: String? = nil,
        nestedError: Error? = nil,
        data: Data = Data()
    ) {
        self.statusCode = statusCode
        self.errorDescription = errorDescription
        self.nestedError = nestedError
        self.data = data
        if let description = description {
            self.description = description
        } else {
            var summary = "OpenAPITransportError with status \(statusCode)"
            if let nestedError = nestedError {
                summary.append(contentsOf: ", \(nestedError.localizedDescription)")
            }
            self.description = summary
        }
    }
}

// MARK: - Policy

/// Policy to define whether response is successful or requires authentication
public protocol ResponsePolicy {
    func defineState(for request: URLRequest, output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<ResponseState, Never>
}

public enum ResponseState {
    /// Return success to client
    case success
    /// Return error to client
    case failure
    /// Repeat request
    case retry
}

// MARK: - Interceptor

/// Define how to customize URL request before network call
public protocol Interceptor {
    /// Customize request before performing. Add headers or encrypt body for example.
    func intercept(request: URLRequest) -> AnyPublisher<URLRequest, OpenAPITransportError>

    /// Customize response before handling. Decrypt body for example.
    func intercept(output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<URLSession.DataTaskPublisher.Output, OpenAPITransportError>
}

// MARK: - Transport delegate

public protocol OpenAPITransportDelegate: AnyObject {
    func willStart(request: URLRequest)

    func didFinish(request: URLRequest, response: HTTPURLResponse?, data: Data)

    func didFinish(request: URLRequest, error: Error)
}

// MARK: - Implementation

open class URLSessionOpenAPITransport: OpenAPITransport {
    public struct Config {
        public var baseURL: URL?
        public var session: URLSession
        public var processor: Interceptor
        public var policy: ResponsePolicy
        public weak var delegate: OpenAPITransportDelegate?

        public init(
            baseURL: URL? = nil,
            session: URLSession = .shared,
            processor: Interceptor = DefaultInterceptor(),
            policy: ResponsePolicy = DefaultResponsePolicy(),
            delegate: OpenAPITransportDelegate? = nil
        ) {
            self.baseURL = baseURL
            self.session = session
            self.processor = processor
            self.policy = policy
            self.delegate = delegate
        }
    }

    private var cancellable = Set<AnyCancellable>()
    public var config: Config
    public var baseURL: URL? { config.baseURL }

    public init(config: Config = .init()) {
        self.config = config
    }

    open func send(request: URLRequest) -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError> {
        config.processor
            // Add custom headers or refresh token if needed
            .intercept(request: request)
            .flatMap { request -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError> in
                self.config.delegate?.willStart(request: request)
                // Perform network call
                return self.config.session.dataTaskPublisher(for: request)
                    .mapError {
                        self.config.delegate?.didFinish(request: request, error: $0)
                        return OpenAPITransportError(statusCode: $0.code.rawValue, description: "Network call finished fails")
                    }
                    .flatMap { output in
                        self.config.processor.intercept(output: output)
                    }
                    .flatMap { output -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError> in
                        let response = output.response as? HTTPURLResponse
                        self.config.delegate?.didFinish(request: request, response: response, data: output.data)
                        return self.config.policy.defineState(for: request, output: output)
                            .setFailureType(to: OpenAPITransportError.self)
                            .flatMap { state -> AnyPublisher<OpenAPITransportResponse, OpenAPITransportError> in
                                switch state {
                                case .success:
                                    let transportResponse = OpenAPITransportResponse(data: output.data, statusCode: 200)
                                    return Result.success(transportResponse).publisher.eraseToAnyPublisher()
                                case .retry:
                                    return Fail(error: OpenAPITransportError.retryError).eraseToAnyPublisher()
                                case .failure:
                                    let code = response?.statusCode ?? OpenAPITransportError.noResponseCode
                                    let transportError = OpenAPITransportError(statusCode: code, data: output.data)
                                    return Fail(error: transportError).eraseToAnyPublisher()
                                }
                            }.eraseToAnyPublisher()
                    }
                    .eraseToAnyPublisher()
            }
            .retry(times: 2) { error -> Bool in
                return error.statusCode == OpenAPITransportError.retryError.statusCode
            }.eraseToAnyPublisher()
    }

    open func cancelAll() {
        cancellable.removeAll()
    }
}

public final class DefaultInterceptor: Interceptor {
    public init() {}

    public func intercept(request: URLRequest) -> AnyPublisher<URLRequest, OpenAPITransportError> {
        Just(request)
            .setFailureType(to: OpenAPITransportError.self)
            .eraseToAnyPublisher()
    }

    public func intercept(output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<URLSession.DataTaskPublisher.Output, OpenAPITransportError> {
        Just(output)
            .setFailureType(to: OpenAPITransportError.self)
            .eraseToAnyPublisher()
    }
}

public final class DefaultResponsePolicy: ResponsePolicy {
    public init() {}

    public func defineState(for request: URLRequest, output: URLSession.DataTaskPublisher.Output) -> AnyPublisher<ResponseState, Never> {
        let state: ResponseState
        switch (output.response as? HTTPURLResponse)?.statusCode {
        case .some(200...299): state = .success
        default: state = .failure
        }
        return Just(state).eraseToAnyPublisher()
    }
}

/// Custom transport errors. It begins with 6.. not to conflict with HTTP codes
public extension OpenAPITransportError {
    static let incorrectAuthenticationCode = 600
    static func incorrectAuthenticationError(_ nestedError: Error? = nil) -> OpenAPITransportError {
        OpenAPITransportError(
            statusCode: OpenAPITransportError.incorrectAuthenticationCode,
            description: "Impossible to add authentication headers to request",
            errorDescription: NSLocalizedString(
                "Impossible to add authentication headers to request",
                comment: "Incorrect authentication"
            ),
            nestedError: nestedError
        )
    }

    static let failedAuthenticationRefreshCode = 601
    static func failedAuthenticationRefreshError(_ nestedError: Error? = nil) -> OpenAPITransportError {
        OpenAPITransportError(
            statusCode: OpenAPITransportError.failedAuthenticationRefreshCode,
            description: "Error while refreshing authentication",
            errorDescription: NSLocalizedString(
                "Error while refreshing authentication",
                comment: "Failed authentication refresh"
            ),
            nestedError: nestedError
        )
    }

    static let noResponseCode = 603
    static func noResponseError(_ nestedError: Error? = nil) -> OpenAPITransportError {
        OpenAPITransportError(
            statusCode: OpenAPITransportError.noResponseCode,
            description: "There is no HTTP URL response",
            errorDescription: NSLocalizedString(
                "There is no HTTP URL response",
                comment: "No response"
            ),
            nestedError: nestedError
        )
    }

    static let badURLCode = 604
    static func badURLError(_ nestedError: Error? = nil) -> OpenAPITransportError {
        OpenAPITransportError(
            statusCode: OpenAPITransportError.badURLCode,
            description: "Request URL cannot be created with given parameters",
            errorDescription: NSLocalizedString(
                "Request URL cannot be created with given parameters",
                comment: "Bad URL"
            ),
            nestedError: nestedError
        )
    }

    static let invalidResponseMappingCode = 605
    static func invalidResponseMappingError(data: Data) -> OpenAPITransportError {
        OpenAPITransportError(
            statusCode: OpenAPITransportError.invalidResponseMappingCode,
            description: "Response data cannot be expected object scheme",
            errorDescription: NSLocalizedString(
                "Response data cannot be expected object scheme",
                comment: "Invalid response mapping"
            ),
            data: data
        )
    }

    static let retryErrorCode = 606
    static let retryError = OpenAPITransportError(statusCode: OpenAPITransportError.retryErrorCode)
}

// MARK: - Private

private extension Publishers {
    struct RetryIf<P: Publisher>: Publisher {
        typealias Output = P.Output
        typealias Failure = P.Failure

        let publisher: P
        let times: Int
        let condition: (P.Failure) -> Bool

        func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
            guard times > 0 else { return publisher.receive(subscriber: subscriber) }

            publisher.catch { (error: P.Failure) -> AnyPublisher<Output, Failure> in
                if condition(error)  {
                    return RetryIf(publisher: publisher, times: times - 1, condition: condition).eraseToAnyPublisher()
                } else {
                    return Fail(error: error).eraseToAnyPublisher()
                }
            }.receive(subscriber: subscriber)
        }
    }
}

private extension Publisher {
    func retry(times: Int, if condition: @escaping (Failure) -> Bool) -> Publishers.RetryIf<Self> {
        Publishers.RetryIf(publisher: self, times: times, condition: condition)
    }
}
