//
//  V2raySubscription.swift
//  V2rayU
//
//  Created by yanue on 2019/5/15.
//  Copyright © 2019 yanue. All rights reserved.
//

import Cocoa
import Yams

// ----- v2ray subscribe manager -----
class V2raySubscription: NSObject {
    static var shared = V2raySubscription()
    static let lock = NSLock()
    
    // Initialization
    override init() {
        super.init()
        print("V2raySubscription init")
        V2raySubscription.loadConfig()
    }

    // v2ray subscribe list
    static private var v2raySubList: [V2raySubItem] = []

    // (init) load v2ray subscribe list from UserDefaults
    static func loadConfig() {
        self.lock.lock()
        defer {
            self.lock.unlock()
        }
        // static reset
        self.v2raySubList = []

        // load name list from UserDefaults
        let list = UserDefaults.getArray(forKey: .v2raySubList)
//        print("loadConfig", list)

        if list == nil {
            return
        }
        // load each V2raySubItem
        for item in list! {
            guard let v2ray = V2raySubItem.load(name: item) else {
                // delete from UserDefaults
                V2raySubItem.remove(name: item)
                continue
            }
            // append
            self.v2raySubList.append(v2ray)
        }
    }

    // get list from v2ray subscribe list
    static func list() -> [V2raySubItem] {
        return self.v2raySubList
    }

    // get count from v2ray subscribe list
    static func count() -> Int {
        return self.v2raySubList.count
    }

    static func edit(rowIndex: Int, remark: String) {
        if !self.v2raySubList.indices.contains(rowIndex) {
            NSLog("index out of range", rowIndex)
            return
        }

        // update list
        self.v2raySubList[rowIndex].remark = remark

        // save
        let v2ray = self.v2raySubList[rowIndex]
        v2ray.remark = remark
        v2ray.store()
    }

    static func edit(rowIndex: Int, url: String) {
        if !self.v2raySubList.indices.contains(rowIndex) {
            NSLog("index out of range", rowIndex)
            return
        }

        // update list
        self.v2raySubList[rowIndex].url = url

        // save
        let v2ray = self.v2raySubList[rowIndex]
        v2ray.url = url
        v2ray.store()
    }

    // move item to new index
    static func move(oldIndex: Int, newIndex: Int) {
        if !V2raySubscription.v2raySubList.indices.contains(oldIndex) {
            NSLog("index out of range", oldIndex)
            return
        }
        if !V2raySubscription.v2raySubList.indices.contains(newIndex) {
            NSLog("index out of range", newIndex)
            return
        }

        let o = self.v2raySubList[oldIndex]
        self.v2raySubList.remove(at: oldIndex)
        self.v2raySubList.insert(o, at: newIndex)

        // update subscribe list UserDefaults
        self.saveItemList()
    }

    // add v2ray subscribe (by scan qrcode)
    static func add(remark: String, url: String) {
        if self.v2raySubList.count > 50 {
//            NSLog("over max len")
//            return
        }

        // name is : subscribe. + uuid
        let name = "subscribe." + UUID().uuidString

        let v2ray = V2raySubItem(name: name, remark: remark, url: url)
        // save to v2ray UserDefaults
        v2ray.store()

        // just add to mem
        self.v2raySubList.append(v2ray)

        // update subscribe list UserDefaults
        self.saveItemList()
    }

    // remove v2ray subscribe (tmp and UserDefaults and config json file)
    static func remove(idx: Int) {
        if !V2raySubscription.v2raySubList.indices.contains(idx) {
            NSLog("index out of range", idx)
            return
        }

        let v2ray = V2raySubscription.v2raySubList[idx]

        // delete from tmp
        self.v2raySubList.remove(at: idx)

        // delete from v2ray UserDefaults
        V2raySubItem.remove(name: v2ray.name)

        // update subscribe list UserDefaults
        self.saveItemList()
    }

    // update subscribe list UserDefaults
    static private func saveItemList() {
        var v2raySubList: Array<String> = []
        for item in V2raySubscription.list() {
            v2raySubList.append(item.name)
        }

        UserDefaults.setArray(forKey: .v2raySubList, value: v2raySubList)
    }

    // load json file data
    static func loadSubItem(idx: Int) -> V2raySubItem? {
        if !V2raySubscription.v2raySubList.indices.contains(idx) {
            NSLog("index out of range", idx)
            return nil
        }

        return self.v2raySubList[idx]
    }
}

// ----- v2ray subscribe item -----
class V2raySubItem: NSObject, NSCoding {
    var name: String
    var remark: String
    var isValid: Bool
    var url: String

    // init
    required init(name: String, remark: String, url: String, isValid: Bool = true) {
        self.name = name
        self.remark = remark
        self.isValid = isValid
        self.url = url
    }

    // decode
    required init(coder decoder: NSCoder) {
        self.name = decoder.decodeObject(forKey: "Name") as? String ?? ""
        self.remark = decoder.decodeObject(forKey: "Remark") as? String ?? ""
        self.isValid = decoder.decodeBool(forKey: "IsValid")
        self.url = decoder.decodeObject(forKey: "Url") as? String ?? ""
    }

    // object encode
    func encode(with coder: NSCoder) {
        coder.encode(name, forKey: "Name")
        coder.encode(remark, forKey: "Remark")
        coder.encode(isValid, forKey: "IsValid")
        coder.encode(url, forKey: "Url")
    }

    // store into UserDefaults
    func store() {
        let modelData = NSKeyedArchiver.archivedData(withRootObject: self)
        UserDefaults.standard.set(modelData, forKey: self.name)
    }

    // static load from UserDefaults
    static func load(name: String) -> V2raySubItem? {
        guard let myModelData = UserDefaults.standard.data(forKey: name) else {
            return nil
        }
        do {
            let result = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(myModelData)
            return result as? V2raySubItem
        } catch {
            print("load userDefault error:", error)
            return nil
        }
    }

    // remove from UserDefaults
    static func remove(name: String) {
        UserDefaults.standard.removeObject(forKey: name)
    }
}

// ----- v2ray subscribe  updater -----
let NOTIFY_UPDATE_SubSync = Notification.Name(rawValue: "NOTIFY_UPDATE_SubSync")

class V2raySubSync: NSObject {
    var V2raySubSyncing = false
    let maxConcurrentTasks = 1 // work pool

    static var shared = V2raySubSync()
    // Initialization
    override init() {
        super.init()
        NSLog("V2raySubSync init")
    }
    
    // sync from Subscription list
    public func sync() {
        if V2raySubSyncing {
            NSLog("V2raySubSync Syncing ...")
            return
        }
        self.V2raySubSyncing = true
        NSLog("V2raySubSync start")
        
        let list = V2raySubscription.list()

        if list.count == 0 {
            self.logTip(title: "fail: ", uri: "", informativeText: " please add Subscription Url")
        }
        // sync queue with DispatchGroup
        Task {
            do {
                try await self.syncTaskGroup(items: list)
            } catch let error {
                NSLog("pingTaskGroup error: \(error)")
            }
        }
    }
    
    func syncTaskGroup(items: [V2raySubItem]) async throws {
        let taskChunks = stride(from: 0, to: items.count, by: maxConcurrentTasks).map {
            Array(items[$0..<min($0 + maxConcurrentTasks, items.count)])
        }
        NSLog("syncTaskGroup-start: taskChunks=\(taskChunks.count)")
        for (i, chunk) in taskChunks.enumerated() {
            NSLog("syncTaskGroup-start-\(i): count=\(chunk.count)")
            try await withThrowingTaskGroup(of: Void.self) { group in
                for item in chunk {
                    group.addTask {
                        do {
                            try await self.dlFromUrl(url: item.url, subscribe: item.name)
                        } catch {
                            NSLog("dlFromUrl error: \(error)")
                        }
                        return
                    }
                }
                // 等待当前批次所有任务完成
                try await group.waitForAll()
            }
            NSLog("syncTaskGroup-end-\(i)")
        }
        NSLog("syncTaskGroup-end")
        self.refreshMenu()
    }
    
    func refreshMenu()  {
        NSLog("V2raySubSync refreshMenu")
        self.V2raySubSyncing = false
        usleep(useconds_t(1 * second))
        do {
            // refresh server
            menuController.showServers()
            // sleep 2
            sleep(2)
            // do ping
            ping.pingAll()
        }
    }
    
    public func dlFromUrl(url: String, subscribe: String) async throws {
        logTip(title: "loading from : ", uri: "", informativeText: url + "\n\n")

        guard let reqUrl = URL(string: url) else {
            logTip(title: "loading from : ", uri: "", informativeText: "url is not valid: " + url + "\n\n")
            return
        }
        
        // url request with proxy
        let session = URLSession(configuration: getProxyUrlSessionConfigure())
        do {
            let (data, _) = try await session.data(for: URLRequest(url: reqUrl))
            if let outputStr = String(data: data, encoding: String.Encoding.utf8) {
                self.handle(base64Str: outputStr, subscribe: subscribe, url: url)
            } else {
                self.logTip(title: "loading fail: ", uri: url, informativeText: "data is nil")
            }
        } catch let error {
            // failed to write file – bad permissions, bad filename, missing permissions, or more likely it can't be converted to the encoding
            NSLog("save json file fail: \(error)")
        }
    }

    func handle(base64Str: String, subscribe: String, url: String) {
        guard let strTmp = base64Str.trimmingCharacters(in: .whitespacesAndNewlines).base64Decoded() else {
            self.logTip(title: "parse fail : ", uri: "", informativeText: base64Str)
            return
        }

        self.logTip(title: "handle url: ", uri: "", informativeText: url + "\n\n")

        if self.importByYaml(strTmp: strTmp, subscribe: subscribe) {
            return
        }
        self.importByNormal(strTmp: strTmp, subscribe: subscribe)
    }
    
    func getOld(subscribe: String) -> [String] {
        // reload all
        V2rayServer.loadConfig()
        // get old
        var oldList: [String] = []
        for (_, item) in V2rayServer.list().enumerated() {
            if item.subscribe == subscribe {
                oldList.append(item.name)
            }
        }
        return oldList
    }

    func importByYaml(strTmp: String, subscribe: String) -> Bool {
        // parse clash yaml
        do {
            let oldList = getOld(subscribe: subscribe)
            var exists: Dictionary = [String: Bool]()
            
            let decoder = YAMLDecoder()
            let decoded = try decoder.decode(Clash.self, from: strTmp)
            for item in decoded.proxies {
                if let importUri = importByClash(clash: item) {
                    importUri.remark = item.name
                    if let v2rayOld = self.saveImport(importUri: importUri, subscribe: subscribe) {
                        exists[v2rayOld.name] = true
                    }
                }
            }
                
            logTip(title: "need remove?: ", informativeText: "old=\(oldList.count) - new=\(exists.count)")

            // remove not exist
            for name in oldList {
                if !(exists[name] ?? false) {
                    // delete from v2ray UserDefaults
                    V2rayItem.remove(name: name)
                    logTip(title: "remove: ", informativeText: name)
                }
            }
            
            return true
        } catch {
            NSLog("parseYaml \(error)")
        }
        
        return false
    }
    
    func importByNormal(strTmp: String, subscribe: String)  {
        let oldList = getOld(subscribe: subscribe)
        var exists: Dictionary = [String: Bool]()
        
        let list = strTmp.trimmingCharacters(in: .newlines).components(separatedBy: CharacterSet.newlines)
        var count = 0
        for uri in list {
            // import every server
            if (uri.count > 0) {
                let filterUri =  uri.trimmingCharacters(in: .whitespacesAndNewlines)
                if let importUri = ImportUri.importUri(uri: filterUri,checkExist: false) {
                    if let v2rayOld = self.saveImport(importUri: importUri, subscribe: subscribe) {
                        exists[v2rayOld.name] = true
                    }
                }
            }
        }
        
        logTip(title: "need remove?: ", informativeText: "old=\(oldList.count) - new=\(exists.count)")
        
        // remove not exist
        for name in oldList {
            if !(exists[name] ?? false) {
                // delete from v2ray UserDefaults
                V2rayItem.remove(name: name)
                logTip(title: "remove: ", informativeText: name)
            }
        }
        
    }

    func saveImport(importUri: ImportUri, subscribe: String) -> V2rayItem? {
        if importUri.isValid {
            var newUri = importUri.uri
            // clash has no uri
            if newUri.count == 0 {
                // old share uri
                let v2ray = V2rayItem(name: "tmp", remark: importUri.remark, isValid: importUri.isValid, json: importUri.json, url: "", subscribe: subscribe)
                let share = ShareUri()
                share.qrcode(item: v2ray)
                newUri = share.uri
            }

            print("\(importUri.remark) - \(newUri)")

            if let v2rayOld = V2rayServer.existItem(url: newUri) {
                v2rayOld.json = importUri.json
                v2rayOld.isValid = importUri.isValid
                v2rayOld.remark = importUri.remark
                v2rayOld.store()
                logTip(title: "success update: ", informativeText: importUri.remark)
                return v2rayOld
            } else {
                // add server
                V2rayServer.add(remark: importUri.remark, json: importUri.json, isValid: true, url: newUri, subscribe: subscribe)
                logTip(title: "success add: ", informativeText: importUri.remark)
            }
        } else {
            logTip(title: "fail: ", informativeText: importUri.error)
        }
        return nil
    }

    func logTip(title: String = "", uri: String = "", informativeText: String = "") {
        NotificationCenter.default.post(name: NOTIFY_UPDATE_SubSync, object: title + informativeText + "\n")
        print("SubSync", title + informativeText)
        if uri != "" {
            NotificationCenter.default.post(name: NOTIFY_UPDATE_SubSync, object: "url: " + uri + "\n\n\n")
        }
    }
}

// MARK: - clash

struct Clash: Codable {
    var port, socksPort, redirPort, mixedPort: Int?
    var allowLAN: Bool?
    var mode: String
    var logLevel: String?
    var externalController: String?
    var proxies: [clashProxy]
    var rules: [String]?
}

// MARK: - Proxy
struct clashProxy: Codable {
    var type: String
    var name: String
    var server: String
    var port: Int
    var username: String? // socks5 | http
    var password: String?
    var sni: String?
    var skipCERTVerify: Bool?
    var cipher: String? // ss | ssr
    var uuid: String? // vmess | vless
    var alterId: Int? // vmess | vless
    var tls: Bool? // tls
    var fp: String?
    var `protocol`: String? // ssr
    var obfs: String? // ssr
    var udp: Bool? // socks5
    var network: String? // ws | h2
    var servername: String? // priority over wss host, REALITY servername,SNI
    var clientFingerprint: String? // vless
    var fingerprint: String? // vmess
    var security: String? // vmess
    var flow: String? // vless
    var wsOpts: clashWsOpts? // vmess
    var httpOpts: clashHttpOpts? // vmess
    var h2Opts: clashH2Opts? // vmess
    var grpcOpts: grpcOpts? // vmess
    var realityOpts: realityOpts? // vless
}

struct clashWsOpts: Codable {
    var path: String?
}

struct clashHttpOpts: Codable {
    var path: [String]?
}

struct clashH2Opts: Codable {
    var path: String?
    var host: [String]?
}

struct grpcOpts: Codable {
    var grpcServiceName: String?
}

struct realityOpts: Codable {
    var publicKey: String?
    var shortId: String?
}

/**
 - {"type":"ss","name":"v2rayse_test_1","server":"198.57.27.218","port":5004,"cipher":"aes-256-gcm","password":"g5MeD6Ft3CWlJId"}
 - {"type":"ssr","name":"v2rayse_test_3","server":"20.239.49.44","port":59814,"protocol":"origin","cipher":"dummy","obfs":"plain","password":"3df57276-03ef-45cf-bdd4-4edb6dfaa0ef"}
 - {"type":"vmess","name":"v2rayse_test_2","ws-opts":{"path":"/"},"server":"154.23.190.162","port":443,"uuid":"b9984674-f771-4e67-a198-","alterId":"0","cipher":"auto","network":"ws"}
 - {"type":"vless","name":"test","server":"1.2.3.4","port":7777,"uuid":"abc-def-ghi-fge-zsx","skip-cert-verify":true,"network":"tcp","tls":true,"udp":true}
 - {"type":"trojan","name":"v2rayse_test_4","server":"ca-trojan.bonds.id","port":443,"password":"bc7593fe-0604-4fbe--b4ab-11eb-b65e-1239d0255272","udp":true,"skip-cert-verify":true}
 - {"type":"http","name":"http_proxy","server":"124.15.12.24","port":251,"username":"username","password":"password","udp":true}
 - {"type":"socks5","name":"socks5_proxy","server":"124.15.12.24","port":2312,"udp":true}
 - {"type":"socks5","name":"telegram_proxy","server":"1.2.3.4","port":123,"username":"username","password":"password","udp":true}
 */
