import Foundation
import GRDB
import SessionSnodeKit
import SessionUtilitiesKit

final class IP2Country {
    static var isInitialized = false
    
    var countryNamesCache: Atomic<[String: String]> = Atomic([:])
    
    // MARK: - Tables
    /// This table has two columns: the "network" column and the "registered_country_geoname_id" column. The network column contains
    /// the **lower** bound of an IP range and the "registered_country_geoname_id" column contains the ID of the country corresponding
    /// to that range. We look up an IP by finding the first index in the network column where the value is greater than the IP we're looking
    /// up (converted to an integer). The IP we're looking up must then be in the range **before** that range.
    private lazy var ipv4Table: [String: [Int]] = {
        let url = Bundle.main.url(forResource: "GeoLite2-Country-Blocks-IPv4", withExtension: nil)!
        let data = try! Data(contentsOf: url)
        return try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [String: [Int]]
    }()
    
    private lazy var countryNamesTable: [String: [String]] = {
        let url = Bundle.main.url(forResource: "GeoLite2-Country-Locations-English", withExtension: nil)!
        let data = try! Data(contentsOf: url)
        return try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [String: [String]]
    }()

    // MARK: Lifecycle
    static let shared = IP2Country()

    private init() {
        NotificationCenter.default.addObserver(self, selector: #selector(populateCacheIfNeededAsync), name: .pathsBuilt, object: nil)
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }
    
    // MARK: - Implementation
    
    @discardableResult private func cacheCountry(for ip: String, inCache cache: inout [String: String]) -> String {
        if let result: String = cache[ip] { return result }
        
        let ipAsInt: Int = IPv4.toInt(ip)
        
        guard
            let ipv4TableIndex = ipv4Table["network"]?.firstIndex(where: { $0 > ipAsInt }).map({ $0 - 1 }),
            let countryID: Int = ipv4Table["registered_country_geoname_id"]?[ipv4TableIndex],
            let countryNamesTableIndex = countryNamesTable["geoname_id"]?.firstIndex(of: String(countryID)),
            let result: String = countryNamesTable["country_name"]?[countryNamesTableIndex]
        else {
            return "Unknown Country" // Relies on the array being sorted
        }
        
        cache[ip] = result
        return result
    }

    @objc func populateCacheIfNeededAsync() {
        DispatchQueue.global(qos: .utility).async { [weak self] in
            self?.populateCacheIfNeeded()
        }
    }

    @discardableResult func populateCacheIfNeeded() -> Bool {
        guard let pathToDisplay: [Snode] = OnionRequestAPI.paths.first else { return false }
        
        countryNamesCache.mutate { [weak self] cache in
            pathToDisplay.forEach { snode in
                self?.cacheCountry(for: snode.ip, inCache: &cache) // Preload if needed
            }
        }
        
        DispatchQueue.main.async {
            IP2Country.isInitialized = true
            NotificationCenter.default.post(name: .onionRequestPathCountriesLoaded, object: nil)
        }
        SNLog("Finished preloading onion request path countries.")
        return true
    }
}
