import Foundation
import OpenClawProtocol
import UniformTypeIdentifiers

actor MacNodeBrowserProxy {
    static let shared = MacNodeBrowserProxy()

    struct Endpoint {
        let baseURL: URL
        let token: String?
        let password: String?
    }

    private struct RequestParams: Decodable {
        let method: String?
        let path: String?
        let query: [String: OpenClawProtocol.AnyCodable]?
        let body: OpenClawProtocol.AnyCodable?
        let timeoutMs: Int?
        let profile: String?
    }

    private struct ProxyFilePayload {
        let path: String
        let base64: String
        let mimeType: String?

        func asJSON() -> [String: Any] {
            var json: [String: Any] = [
                "path": self.path,
                "base64": self.base64,
            ]
            if let mimeType = self.mimeType {
                json["mimeType"] = mimeType
            }
            return json
        }
    }

    private static let maxProxyFileBytes = 10 * 1024 * 1024
    private let endpointProvider: @Sendable () -> Endpoint
    private let performRequest: @Sendable (URLRequest) async throws -> (Data, URLResponse)

    init(
        session: URLSession = .shared,
        endpointProvider: (@Sendable () -> Endpoint)? = nil,
        performRequest: (@Sendable (URLRequest) async throws -> (Data, URLResponse))? = nil)
    {
        self.endpointProvider = endpointProvider ?? MacNodeBrowserProxy.defaultEndpoint
        self.performRequest = performRequest ?? { request in
            try await session.data(for: request)
        }
    }

    func request(paramsJSON: String?) async throws -> String {
        let params = try Self.decodeRequestParams(from: paramsJSON)
        let request = try Self.makeRequest(params: params, endpoint: self.endpointProvider())
        let (data, response) = try await self.performRequest(request)
        let http = try Self.requireHTTPResponse(response)
        guard (200..<300).contains(http.statusCode) else {
            throw NSError(domain: "MacNodeBrowserProxy", code: http.statusCode, userInfo: [
                NSLocalizedDescriptionKey: Self.httpErrorMessage(statusCode: http.statusCode, data: data),
            ])
        }

        let result = try JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed])
        let files = try Self.loadProxyFiles(from: result)
        var payload: [String: Any] = ["result": result]
        if !files.isEmpty {
            payload["files"] = files.map { $0.asJSON() }
        }
        let payloadData = try JSONSerialization.data(withJSONObject: payload)
        guard let payloadJSON = String(data: payloadData, encoding: .utf8) else {
            throw NSError(domain: "MacNodeBrowserProxy", code: 2, userInfo: [
                NSLocalizedDescriptionKey: "browser proxy returned invalid UTF-8",
            ])
        }
        return payloadJSON
    }

    private static func defaultEndpoint() -> Endpoint {
        let config = GatewayEndpointStore.localConfig()
        let controlPort = GatewayEnvironment.gatewayPort() + 2
        let baseURL = URL(string: "http://127.0.0.1:\(controlPort)")!
        return Endpoint(baseURL: baseURL, token: config.token, password: config.password)
    }

    private static func decodeRequestParams(from raw: String?) throws -> RequestParams {
        guard let raw else {
            throw NSError(domain: "MacNodeBrowserProxy", code: 3, userInfo: [
                NSLocalizedDescriptionKey: "INVALID_REQUEST: paramsJSON required",
            ])
        }
        return try JSONDecoder().decode(RequestParams.self, from: Data(raw.utf8))
    }

    private static func makeRequest(params: RequestParams, endpoint: Endpoint) throws -> URLRequest {
        let method = (params.method ?? "GET").trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
        let path = (params.path ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
        guard !path.isEmpty else {
            throw NSError(domain: "MacNodeBrowserProxy", code: 1, userInfo: [
                NSLocalizedDescriptionKey: "INVALID_REQUEST: path required",
            ])
        }

        let normalizedPath = path.hasPrefix("/") ? path : "/\(path)"
        guard var components = URLComponents(
            url: endpoint.baseURL.appendingPathComponent(String(normalizedPath.dropFirst())),
            resolvingAgainstBaseURL: false)
        else {
            throw NSError(domain: "MacNodeBrowserProxy", code: 4, userInfo: [
                NSLocalizedDescriptionKey: "INVALID_REQUEST: invalid browser proxy URL",
            ])
        }

        var queryItems: [URLQueryItem] = []
        if let query = params.query {
            for key in query.keys.sorted() {
                let value = query[key]?.value
                guard value != nil, !(value is NSNull) else { continue }
                queryItems.append(URLQueryItem(name: key, value: Self.stringValue(for: value)))
            }
        }
        let profile = params.profile?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        if !profile.isEmpty, !queryItems.contains(where: { $0.name == "profile" }) {
            queryItems.append(URLQueryItem(name: "profile", value: profile))
        }
        if !queryItems.isEmpty {
            components.queryItems = queryItems
        }
        guard let url = components.url else {
            throw NSError(domain: "MacNodeBrowserProxy", code: 5, userInfo: [
                NSLocalizedDescriptionKey: "INVALID_REQUEST: invalid browser proxy URL",
            ])
        }

        var request = URLRequest(url: url)
        request.httpMethod = method
        request.timeoutInterval = params.timeoutMs.map { TimeInterval(max($0, 1)) / 1000 } ?? 5
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        if let token = endpoint.token?.trimmingCharacters(in: .whitespacesAndNewlines), !token.isEmpty {
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        } else if let password = endpoint.password?.trimmingCharacters(in: .whitespacesAndNewlines),
                  !password.isEmpty
        {
            request.setValue(password, forHTTPHeaderField: "x-openclaw-password")
        }

        if method != "GET", let body = params.body {
            request.httpBody = try JSONSerialization.data(
                withJSONObject: body.foundationValue,
                options: [.fragmentsAllowed])
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        }

        return request
    }

    private static func requireHTTPResponse(_ response: URLResponse) throws -> HTTPURLResponse {
        guard let http = response as? HTTPURLResponse else {
            throw NSError(domain: "MacNodeBrowserProxy", code: 6, userInfo: [
                NSLocalizedDescriptionKey: "browser proxy returned a non-HTTP response",
            ])
        }
        return http
    }

    private static func httpErrorMessage(statusCode: Int, data: Data) -> String {
        if let object = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) as? [String: Any],
           let error = object["error"] as? String,
           !error.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
        {
            return error
        }
        if let text = String(data: data, encoding: .utf8)?
            .trimmingCharacters(in: .whitespacesAndNewlines),
            !text.isEmpty
        {
            return text
        }
        return "HTTP \(statusCode)"
    }

    private static func stringValue(for value: Any?) -> String? {
        guard let value else { return nil }
        if let string = value as? String { return string }
        if let bool = value as? Bool { return bool ? "true" : "false" }
        if let number = value as? NSNumber { return number.stringValue }
        return String(describing: value)
    }

    private static func loadProxyFiles(from result: Any) throws -> [ProxyFilePayload] {
        let paths = self.collectProxyPaths(from: result)
        return try paths.map(self.loadProxyFile)
    }

    private static func collectProxyPaths(from payload: Any) -> [String] {
        guard let object = payload as? [String: Any] else { return [] }

        var paths = Set<String>()
        if let path = object["path"] as? String, !path.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            paths.insert(path.trimmingCharacters(in: .whitespacesAndNewlines))
        }
        if let imagePath = object["imagePath"] as? String,
           !imagePath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
        {
            paths.insert(imagePath.trimmingCharacters(in: .whitespacesAndNewlines))
        }
        if let download = object["download"] as? [String: Any],
           let path = download["path"] as? String,
           !path.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
        {
            paths.insert(path.trimmingCharacters(in: .whitespacesAndNewlines))
        }
        return paths.sorted()
    }

    private static func loadProxyFile(path: String) throws -> ProxyFilePayload {
        let url = URL(fileURLWithPath: path)
        let values = try url.resourceValues(forKeys: [.isRegularFileKey, .fileSizeKey])
        guard values.isRegularFile == true else {
            throw NSError(domain: "MacNodeBrowserProxy", code: 7, userInfo: [
                NSLocalizedDescriptionKey: "browser proxy file not found: \(path)",
            ])
        }
        if let fileSize = values.fileSize, fileSize > Self.maxProxyFileBytes {
            throw NSError(domain: "MacNodeBrowserProxy", code: 8, userInfo: [
                NSLocalizedDescriptionKey: "browser proxy file exceeds 10MB: \(path)",
            ])
        }

        let data = try Data(contentsOf: url)
        let mimeType = UTType(filenameExtension: url.pathExtension)?.preferredMIMEType
        return ProxyFilePayload(path: path, base64: data.base64EncodedString(), mimeType: mimeType)
    }
}
