import Foundation

public enum StreamCinemaClientError: Error, LocalizedError {
    case missingAuthToken
    case badTokenResponse
    case invalidPath(String)

    public var errorDescription: String? {
        switch self {
        case .missingAuthToken:
            return "Missing Stream Cinema auth token"
        case .badTokenResponse:
            return "Token response did not include token"
        case .invalidPath(let value):
            return "Invalid path: \(value)"
        }
    }
}

public actor StreamCinemaClient {
    public struct Settings: Sendable {
        public var dubbed = false
        public var dubbedWithSubtitles = false
        public var showGenre = false
        public var showOldMenu = true
        public var excludeHDR = false
        public var excludeDolbyVision = true

        public init() {}
    }

    public struct Configuration: Sendable {
        public let baseURL: URL
        public let apiVersion: String
        public let uuid: String
        public let languageCode: String
        public let skinName: String
        public let userAgent: String
        public let settings: Settings

        public init(
            baseURL: URL = URL(string: "https://stream-cinema.online/kodi")!,
            apiVersion: String = "2.0",
            uuid: String = UUID().uuidString,
            languageCode: String = Locale.current.language.languageCode?.identifier.uppercased() ?? "CS",
            skinName: String = "native-macos",
            userAgent: String = "StreamCinemaNative/0.1 (macOS)",
            settings: Settings = .init()
        ) {
            self.baseURL = baseURL
            self.apiVersion = apiVersion
            self.uuid = uuid
            self.languageCode = languageCode
            self.skinName = skinName
            self.userAgent = userAgent
            self.settings = settings
        }
    }

    private let configuration: Configuration
    private let httpClient: HTTPClient
    private let decoder = JSONDecoder()
    private var authToken: String?
    private let alwaysArrayParams: Set<String> = ["co", "ca", "ge", "mu"]

    public init(
        configuration: Configuration = .init(),
        httpClient: HTTPClient = .init()
    ) {
        self.configuration = configuration
        self.httpClient = httpClient
    }

    public func setAuthToken(_ token: String) {
        authToken = token
    }

    @discardableResult
    public func fetchAuthToken(kraSessionToken: String) async throws -> String {
        let response = try await getJSONObject(
            path: "/auth/token",
            params: ["krt": .string(kraSessionToken)],
            requiresAuth: false,
            method: .post
        )
        guard let token = response["token"]?.stringValue, !token.isEmpty else {
            throw StreamCinemaClientError.badTokenResponse
        }
        authToken = token
        return token
    }

    public func get(path: String, params: [String: QueryValue] = [:]) async throws -> StreamCinemaResponse {
        let data = try await requestData(path: path, method: .get, params: params, requiresAuth: true)
        return try decoder.decode(StreamCinemaResponse.self, from: data)
    }

    public func getJSONObject(
        path: String,
        params: [String: QueryValue] = [:],
        requiresAuth: Bool = true,
        method: HTTPMethod = .get
    ) async throws -> JSONObject {
        let data = try await requestData(path: path, method: method, params: params, requiresAuth: requiresAuth)
        return try decoder.decode(JSONObject.self, from: data)
    }

    private func requestData(
        path: String,
        method: HTTPMethod,
        params: [String: QueryValue],
        requiresAuth: Bool
    ) async throws -> Data {
        let url = try buildURL(path: path, params: params)
        var headers = [
            "User-Agent": configuration.userAgent,
            "X-Uuid": configuration.uuid
        ]
        if requiresAuth {
            guard let authToken, !authToken.isEmpty else {
                throw StreamCinemaClientError.missingAuthToken
            }
            headers["X-AUTH-TOKEN"] = authToken
        }

        return try await httpClient.send(
            url: url,
            method: method,
            headers: headers
        )
    }

    private func buildURL(path: String, params: [String: QueryValue]) throws -> URL {
        let normalizedPath = path.hasPrefix("/") ? String(path.dropFirst()) : path
        guard var components = URLComponents(
            url: configuration.baseURL.appending(path: normalizedPath),
            resolvingAgainstBaseURL: false
        ) else {
            throw StreamCinemaClientError.invalidPath(path)
        }

        var queryMap: [String: [String]] = [:]
        for item in components.queryItems ?? [] {
            let key = item.name.hasSuffix("[]") ? String(item.name.dropLast(2)) : item.name
            queryMap[key, default: []].append(item.value ?? "")
        }

        for (key, value) in defaultParams() {
            queryMap[key] = value
        }
        for (key, value) in params {
            queryMap[key] = value.values
        }

        components.queryItems = queryMap
            .keys
            .sorted()
            .flatMap { key -> [URLQueryItem] in
                guard let values = queryMap[key] else { return [] }
                if values.count > 1 || alwaysArrayParams.contains(key) {
                    return values.map { URLQueryItem(name: "\(key)[]", value: $0) }
                }
                return [URLQueryItem(name: key, value: values.first ?? "")]
            }

        guard let url = components.url else {
            throw StreamCinemaClientError.invalidPath(path)
        }
        return url
    }

    private func defaultParams() -> [String: [String]] {
        var params: [String: [String]] = [
            "ver": [configuration.apiVersion],
            "uid": [configuration.uuid],
            "skin": [configuration.skinName],
            "lang": [configuration.languageCode],
            "HDR": [configuration.settings.excludeHDR ? "0" : "1"],
            "DV": [configuration.settings.excludeDolbyVision ? "0" : "1"]
        ]

        if configuration.settings.dubbed {
            params["dub"] = ["1"]
        }
        if configuration.settings.dubbedWithSubtitles && !configuration.settings.dubbed {
            params["dub"] = ["1"]
            params["tit"] = ["1"]
        }
        if configuration.settings.showGenre {
            params["gen"] = ["1"]
        }
        if configuration.settings.showOldMenu {
            params["old"] = ["1"]
        }
        return params
    }
}
