import Foundation


/// Configuration handles the current environment - production, beta, staging, labs
/// It has the functions that build URLs for the various APIs utilized by the app.
/// It also maintains the list of relevant domains - default domain, domains that require the CentralAuth cookies to be copied, etc.
@objc(WMFConfiguration)
public class Configuration: NSObject {
    
    public struct StagingOptions: OptionSet {
        public let rawValue: Int

        public static let appsLabsforPCS = StagingOptions(rawValue: 1 << 0)
        public static let deploymentLabsForEventLogging = StagingOptions(rawValue: 1 << 1)
        public static let betaCluster = StagingOptions(rawValue: 1 << 2) // note, this will force beta cluster for PCS (thus ignoring an appsLabsforPCS value if also set) and force deploymentLabsForEventLogging
        
        public init(rawValue: Int) {
            self.rawValue = rawValue
        }
    }
    
    public struct LocalOptions: OptionSet {
        public let rawValue: Int
        
        public static let localAnnouncements = LocalOptions(rawValue: 1 << 0)
        public static let localPCS = LocalOptions(rawValue: 1 << 1)
        
        public init(rawValue: Int) {
            self.rawValue = rawValue
        }
    }
    
    public enum Environment {
        case production
        case staging(StagingOptions)
        case local(LocalOptions)
    }
    
    public let environment: Environment
    
    @objc public static let current: Configuration = {
        #if WMF_LOCAL
        return Configuration.local(options: [.localPCS, .localAnnouncements])
        #elseif WMF_STAGING
		
		/* NOTE: .betaCluster attempts to point to the MediaWiki beta cluster for all possible endpoints.
		Change this to .appsLabsForPCS and/or .deploymentLabsForEventLogging for alternative staging environments.
		Example: Configuration.staging(options: [.appsLabsForPCS, .deploymentLabsForEventLogging])
			.appsLabsForPCS = Product Infrastructure team's labs instance for PCS endpoints
			.deploymentLabsForEventLogging = labs instance for testing event logging endpoints
			All other endpoints would point to production */
		
        return Configuration.staging(options: [.deploymentLabsForEventLogging])
        #else
        return .production
        #endif
    }()
    
    private let pageContentServiceAPIType: APIURLComponentsBuilder.RESTBase.BuilderType
    private let feedContentAPIType: APIURLComponentsBuilder.RESTBase.BuilderType
    private let announcementsAPIType: APIURLComponentsBuilder.RESTBase.BuilderType
    private let eventLoggingAPIType: APIURLComponentsBuilder.EventLogging.BuilderType
    private let mediaWikiRestAPIType = APIURLComponentsBuilder.MediaWiki.BuilderType.productionRest
    private let mediaWikiAPIType = APIURLComponentsBuilder.MediaWiki.BuilderType.production
    private let wikidataAPIType: APIURLComponentsBuilder.Wikidata.BuilderType
    private let commonsAPIType: APIURLComponentsBuilder.Commons.BuilderType
    private let metricsAPIType = APIURLComponentsBuilder.RESTBase.BuilderType.production
    
    // MARK: Configurations
    
    private static var commonProductionCentralAuthCookieTargetDomains = [
        Domain.mediaWiki.withDotPrefix,
        Domain.wikimedia.withDotPrefix,
        Domain.wiktionary.withDotPrefix,
        Domain.wikiquote.withDotPrefix,
        Domain.wikibooks.withDotPrefix,
        Domain.wikisource.withDotPrefix,
        Domain.wikinews.withDotPrefix,
        Domain.wikiversity.withDotPrefix,
        Domain.wikispecies.withDotPrefix,
        Domain.wikivoyage.withDotPrefix
    ]
    
    public static let production: Configuration = {
        
        let centralAuthCookieTargetDomains = commonProductionCentralAuthCookieTargetDomains + [Domain.wikidata.withDotPrefix, Domain.commons.withDotPrefix]
        
        return Configuration(
            environment: .production,
            defaultSiteDomain: Domain.wikipedia,
            wikipediaCookieDomain: Domain.wikipedia.withDotPrefix,
            centralAuthCookieTargetDomains: centralAuthCookieTargetDomains,
            pageContentServiceAPIType: .production,
            feedContentAPIType: .production,
            announcementsAPIType: .production,
            wikidataAPIType: .production,
            commonsAPIType: .production,
            eventLoggingAPIType: .production)
    }()
    
    private static func staging(options: StagingOptions) -> Configuration {
        
        let defaultSiteDomain = options.contains(.betaCluster) ? Domain.wikipediaBetaLabs : Domain.wikipedia
        let wikipediaCookieDomain = options.contains(.betaCluster) ? Domain.wikipediaBetaLabs.withDotPrefix : Domain.wikipedia.withDotPrefix
        let wikidataCookieDomain = options.contains(.betaCluster) ? Domain.wikidataBetaLabs.withDotPrefix : Domain.wikidata.withDotPrefix
        let commonsCookieDomain = options.contains(.betaCluster) ? Domain.commonsBetaLabs.withDotPrefix : Domain.commons.withDotPrefix
        
        let centralAuthCookieTargetDomains = commonProductionCentralAuthCookieTargetDomains + [wikidataCookieDomain, commonsCookieDomain]
        
        let pcsApiType: APIURLComponentsBuilder.RESTBase.BuilderType = options.contains(.appsLabsforPCS) && !options.contains(.betaCluster) ? .stagingAppsLabsPCS : .production
        let wikidataApiType: APIURLComponentsBuilder.Wikidata.BuilderType = options.contains(.betaCluster) ? .betaLabs : .production
        let commonsApiType: APIURLComponentsBuilder.Commons.BuilderType = options.contains(.betaCluster) ? .betaLabs : .production
        let eventLoggingApiType: APIURLComponentsBuilder.EventLogging
            .BuilderType = options.contains(.deploymentLabsForEventLogging) || options.contains(.betaCluster) ? .staging : .production
        
        return Configuration(
            environment: .staging(options),
            defaultSiteDomain: defaultSiteDomain,
            wikipediaCookieDomain: wikipediaCookieDomain,
            centralAuthCookieTargetDomains: centralAuthCookieTargetDomains,
            pageContentServiceAPIType: pcsApiType,
            feedContentAPIType: .production,
            announcementsAPIType: .production,
            wikidataAPIType: wikidataApiType,
            commonsAPIType: commonsApiType,
            eventLoggingAPIType: eventLoggingApiType
        )
    }
    
    private static func local(options: LocalOptions) -> Configuration {
        
        let pcsApiType: APIURLComponentsBuilder.RESTBase.BuilderType = options.contains(.localPCS) ? .localPCS : .production
        let announcementsApiType: APIURLComponentsBuilder.RESTBase.BuilderType = options.contains(.localAnnouncements) ? .localAnnouncements : .production
        
        let centralAuthCookieTargetDomains = commonProductionCentralAuthCookieTargetDomains + [Domain.wikidata.withDotPrefix, Domain.commons.withDotPrefix]
        
        return Configuration(
            environment: .local(options),
            defaultSiteDomain: Domain.wikipedia,
            wikipediaCookieDomain: Domain.wikipedia.withDotPrefix,
            centralAuthCookieTargetDomains: centralAuthCookieTargetDomains,
            pageContentServiceAPIType: pcsApiType,
            feedContentAPIType: .production,
            announcementsAPIType: announcementsApiType,
            wikidataAPIType: .production,
            commonsAPIType: .production,
            eventLoggingAPIType: .production)
    }
    
    // MARK: Constants
    
    struct Scheme {
        static let http = "http"
        static let https = "https"
    }
    
    public struct Domain {
        public static let wikipedia = "wikipedia.org"
        public static let wikipediaBetaLabs = "wikipedia.beta.wmflabs.org"
        public static let wikidata = "wikidata.org"
        public static let wikidataBetaLabs = "wikidata.beta.wmflabs.org"
        public static let commons = "commons.wikimedia.org"
        public static let commonsBetaLabs = "commons.wikimedia.beta.wmflabs.org"
        public static let mediaWiki = "www.mediawiki.org"
        public static let wikispecies = "species.wikimedia.org"
        public static let appsLabs = "mobileapps.wmflabs.org" // Product Infrastructure team's labs instance
        public static let localhost = "localhost"
        public static let englishWikipedia = "en.wikipedia.org"
        public static let testWikipedia = "test.wikipedia.org"
        public static let wikimedia = "wikimedia.org"
        public static let metaWiki = "meta.wikimedia.org"
        public static let wikimediafoundation = "wikimediafoundation.org"
        public static let uploads = "upload.wikimedia.org"
        public static let wikibooks = "wikibooks.org"
        public static let wiktionary = "wiktionary.org"
        public static let wikiquote = "wikiquote.org"
        public static let wikisource = "wikisource.org"
        public static let wikinews = "wikinews.org"
        public static let wikiversity = "wikiversity.org"
        public static let wikivoyage = "wikivoyage.org"
    }
    
    struct Path {
        static let wikiResourceComponent = ["wiki"]
        static let restBaseAPIComponents = ["api", "rest_v1"]
        static let mediaWikiAPIComponents = ["w", "api.php"]
        static let mediaWikiRestAPIComponents = ["w", "rest.php"]
        static let expandedWikiResourceComponents = ["w", "index.php"]
    }
    
    // MARK: State
    
    @objc public let defaultSiteDomain: String
    public let defaultSiteURL: URL
    
    public let wikipediaCookieDomain: String
    public let centralAuthCookieSourceDomain: String // copy cookies from
    public let centralAuthCookieTargetDomains: [String] // copy cookies to
    
    // Wikipedia Domains
    public let wikipediaDomains: [String]
    
    // Domains that can fall back to in-app web view
    public let inAppWebViewRoutingDomains: [String]

    @objc public lazy var router: Router = {
        return Router(configuration: self)
    }()

    required init(environment: Environment, defaultSiteDomain: String,
                  wikipediaCookieDomain: String,
                  centralAuthCookieTargetDomains: [String] = [],
                  pageContentServiceAPIType: APIURLComponentsBuilder.RESTBase.BuilderType,
                  feedContentAPIType: APIURLComponentsBuilder.RESTBase.BuilderType,
                  announcementsAPIType: APIURLComponentsBuilder.RESTBase.BuilderType,
                  wikidataAPIType: APIURLComponentsBuilder.Wikidata.BuilderType,
                  commonsAPIType: APIURLComponentsBuilder.Commons.BuilderType,
                  eventLoggingAPIType: APIURLComponentsBuilder.EventLogging.BuilderType) {
        self.environment = environment
        self.defaultSiteDomain = defaultSiteDomain
        var components = URLComponents()
        components.scheme = "https"
        components.host = defaultSiteDomain
        self.defaultSiteURL = components.url!
        self.wikipediaCookieDomain = wikipediaCookieDomain
        self.centralAuthCookieSourceDomain = self.wikipediaCookieDomain
        self.centralAuthCookieTargetDomains = centralAuthCookieTargetDomains
        
        self.wikipediaDomains = [Domain.wikipedia, Domain.wikipediaBetaLabs, Domain.appsLabs]
        self.inAppWebViewRoutingDomains = wikipediaDomains + [Domain.mediaWiki, Domain.wikidata, Domain.wikimedia, Domain.wikimediafoundation]
        self.pageContentServiceAPIType = pageContentServiceAPIType
        self.feedContentAPIType = feedContentAPIType
        self.announcementsAPIType = announcementsAPIType
        self.wikidataAPIType = wikidataAPIType
        self.commonsAPIType = commonsAPIType
        self.eventLoggingAPIType = eventLoggingAPIType
    }
    
    // MARK: Page Content Service
    
    public func pageContentServiceBuilder(withWikiHost wikiHost: String? = nil) -> APIURLComponentsBuilder {
        let builder = pageContentServiceAPIType.builder(withWikiHost: wikiHost)
        return builder
    }
    
    /// The Page Content Service includes mobile-html and the associated endpoints. It can be run locally with this repository: https://gerrit.wikimedia.org/r/admin/projects/mediawiki/services/mobileapps
    /// On production, it is run through RESTBase at  https://en.wikipedia.org/api/rest_v1/ (works for all language wikis)
    @objc(pageContentServiceAPIURLForURL:appendingPathComponents:)
    public func pageContentServiceAPIURLForURL(_ url: URL? = nil, appending pathComponents: [String] = [""]) -> URL? {
        let builder = pageContentServiceAPIType.builder(withWikiHost: url?.host)
        let components = builder.components(byAppending: pathComponents)
        return components.wmf_URLWithLanguageVariantCode(url?.wmf_languageVariantCode)
    }
    
    /// Returns the default request headers for Page Content Service API requests
    public func pageContentServiceHeaders(for url: URL) -> [String: String] {
        
        // If the language supports variants, only send a single code with variant for that language.
        // This is a workaround for an issue with server-side Accept-Language header handling and
        // can be removed when https://phabricator.wikimedia.org/T256491 is fixed.
        // NOTE: In general it does not seem that most sites process multi-language Accept-Language headers.
        // For variants, sending a single Accept-Language header is sufficient and seems the least error-prone.
        if let languageVariantCode = url.wmf_languageVariantCode {
            return ["Accept-Language": languageVariantCode]
        } else {
            return [:]
        }
    }
    
    // MARK: Metrics
    
    /// The metrics API lives only on wikimedia.org: https://wikimedia.org/api/rest_v1/
    @objc(metricsAPIURLComponentsAppendingPathComponents:)
    public func metricsAPIURLComponents(appending pathComponents: [String] = [""]) -> URLComponents {
        let builder = metricsAPIType.builder(withWikiHost: Domain.wikimedia)
        return builder.components(byAppending: ["metrics"] + pathComponents)
    }
    
    // MARK: Wikifeeds (Feed Content and Announcements)
    
    /// Feed content is located in the wikifeeds repository. It can be run locally with: https://gerrit.wikimedia.org/r/admin/projects/mediawiki/services/wikifeeds
    /// On production, it is run through RESTBase at  https://en.wikipedia.org/api/rest_v1/ (works for all language wikis)
    @objc(feedContentAPIURLForURL:appendingPathComponents:)
    public func feedContentAPIURLForURL(_ url: URL?, appending pathComponents: [String] = [""]) -> URL? {
        let builder = feedContentAPIType.builder(withWikiHost: url?.host)
        let components = builder.components(byAppending: pathComponents)
        return components.wmf_URLWithLanguageVariantCode(url?.wmf_languageVariantCode)
    }
    
    /// Announcements are located in the wikifeeds repository. It can be run locally with: https://gerrit.wikimedia.org/r/admin/projects/mediawiki/services/wikifeeds
    /// On production, it is run through RESTBase at  https://en.wikipedia.org/api/rest_v1/ (works for all language wikis)
    @objc(announcementsAPIURLForURL:appendingPathComponents:)
    public func announcementsAPIURLForURL(_ url: URL?, appending pathComponents: [String] = [""]) -> URL? {
        let builder = announcementsAPIType.builder(withWikiHost: url?.host)
        let components = builder.components(byAppending: pathComponents)
        return components.wmf_URLWithLanguageVariantCode(url?.wmf_languageVariantCode)
    }
    
    // MARK: Event Logging
    
    @objc(eventLoggingAPIURLWithPayload:)
    public func eventLoggingAPIURL(with payload: NSObject) -> URL? {
        let builder = eventLoggingAPIType.builder()
        let components = try? builder.components(byAssigningPayloadToPercentEncodedQuery: payload)
        return components?.url
    }
    
    // MARK: MediaWiki Rest
    
    public func mediaWikiRestAPIURLForURL(_ url: URL? = nil, appending pathComponents: [String] = [""], queryParameters: [String: Any]? = nil) -> URL? {
        let builder = mediaWikiRestAPIType.builder(withWikiHost: url?.host)
        let components = builder.components(byAppending: pathComponents, queryParameters: queryParameters)
        return components.wmf_URLWithLanguageVariantCode(url?.wmf_languageVariantCode)
    }
    
    // MARK: MediaWiki
    
    @objc(mediaWikiAPIURLForURL:withQueryParameters:)
    public func mediaWikiAPIURLForURL(_ url: URL?, with queryParameters: [String: Any]? = nil) -> URL? {
        let components = mediaWikiAPIURLForHost(url?.host, with: queryParameters)
        return components.wmf_URLWithLanguageVariantCode(url?.wmf_languageVariantCode)
    }
    
    public func mediaWikiAPIURLForHost(_ host: String? = nil, with queryParameters: [String: Any]? = nil) -> URLComponents {
        let builder = mediaWikiAPIType.builder(withWikiHost: host)
        guard let queryParameters = queryParameters else {
            return builder.components()
        }
        return builder.components(queryParameters: queryParameters)
    }
    
    public func mediaWikiAPIURLForLanguageCode(_ languageCode: String, siteDomain: String? = nil, queryParameters: [String: Any]?) -> URLComponents {
        let domain = siteDomain ?? defaultSiteDomain
        let host = "\(languageCode).\(domain)"
        return mediaWikiAPIURLForHost(host, with: queryParameters)
    }
    
    // MARK: Wikidata
    
    public func wikidataAPIURLComponents(with queryParameters: [String: Any]?) -> URLComponents {
        let builder = wikidataAPIType.builder()
        return builder.components(queryParameters: queryParameters)
    }
    
    // MARK: Commons

    @objc(commonsAPIURLComponentsWithQueryParameters:)
    public func commonsAPIURLComponents(with queryParameters: [String: Any]?) -> URLComponents {
        let builder = commonsAPIType.builder()
        return builder.components(queryParameters: queryParameters)
    }
    
    // MARK: Article URLs
    
    func articleURLComponentsBuilder(for host: String) -> APIURLComponentsBuilder {
        var components = URLComponents()
        components.host = host
        components.scheme = Scheme.https
        return APIURLComponentsBuilder(hostComponents: components, basePathComponents: Path.wikiResourceComponent)
    }
    
    func expandedArticleURLComponentsBuilder(for host: String) -> APIURLComponentsBuilder {
        var components = URLComponents()
        components.host = host
        components.scheme = Scheme.https
        return APIURLComponentsBuilder(hostComponents: components, basePathComponents: Path.expandedWikiResourceComponents)
    }
    
    public func articleURLForHost(_ host: String, languageVariantCode: String?, appending pathComponents: [String]) -> URL? {
        let builder = articleURLComponentsBuilder(for: host)
        let components = builder.components(byAppending: pathComponents)
        return components.wmf_URLWithLanguageVariantCode(languageVariantCode)
    }
    
    // Uses format https://en.wikipedia.org/w/index.php?title=Main_Page
    // As opposed to https://en.wikipedia.org/wiki/Main_Page
    public func expandedArticleURLForHost(_ host: String, languageVariantCode: String?, queryParameters: [String: Any]?) -> URL? {
        let builder = expandedArticleURLComponentsBuilder(for: host)
        let components = builder.components(byAppending: [], queryParameters: queryParameters)
        return components.wmf_URLWithLanguageVariantCode(languageVariantCode)
    }
    
    // MARK: Routing Helpers
    
    public func isWikipediaHost(_ host: String?) -> Bool {
        guard let host = host else {
            return false
        }
        for domain in wikipediaDomains {
            if host.isDomainOrSubDomainOf(domain) {
                return true
            }
        }
        
        return false
    }
    
    /// Indicates if a url should fall back to an in-app web view or not
    /// Please inspect url namespace first and confirm url cannot display natively before using this method.
    /// - Parameter host: url host that you are trying to route
    /// - Returns: true = host should fall back to app web view, route to in-app web view. false = host should fall back to external Safari web browser (business logic for parental controls).
    public func hostCanRouteToInAppWebView(_ host: String?) -> Bool {
        guard let host = host else {
            return false
        }
        for domain in inAppWebViewRoutingDomains {
            if host.isDomainOrSubDomainOf(domain) {
                return true
            }
        }
        return false
    }
}
