import Foundation

struct APIConfiguration: Sendable {
    var baseURL: URL
    var authToken: String?
}

struct AnyEncodable: Encodable {
    private let encodeClosure: (Encoder) throws -> Void

    init<T: Encodable>(_ value: T) {
        encodeClosure = value.encode(to:)
    }

    func encode(to encoder: Encoder) throws {
        try encodeClosure(encoder)
    }
}

actor SybilAPIClient: SybilAPIClienting {
    private let configuration: APIConfiguration
    private let session: URLSession

    @MainActor
    private static let iso8601FormatterWithFractional: ISO8601DateFormatter = {
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        return formatter
    }()
    
    @MainActor
    private static let iso8601Formatter: ISO8601DateFormatter = {
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime]
        return formatter
    }()

    init(configuration: APIConfiguration, session: URLSession = .shared) {
        self.configuration = configuration
        self.session = session
    }

    func verifySession() async throws -> AuthSession {
        try await request("/v1/auth/session", method: "GET", responseType: AuthSession.self)
    }

    func listWorkspaceItems() async throws -> [WorkspaceItem] {
        let response = try await request("/v1/workspace-items", method: "GET", responseType: WorkspaceListResponse.self)
        return response.items
    }

    func listChats() async throws -> [ChatSummary] {
        let response = try await request("/v1/chats", method: "GET", responseType: ChatListResponse.self)
        return response.chats
    }

    func createChat(
        title: String? = nil,
        provider: Provider? = nil,
        model: String? = nil,
        messages: [CompletionRequestMessage]? = nil
    ) async throws -> ChatSummary {
        let response = try await request(
            "/v1/chats",
            method: "POST",
            body: AnyEncodable(ChatCreateBody(title: title, provider: provider, model: model, messages: messages)),
            responseType: ChatCreateResponse.self
        )
        return response.chat
    }

    func getChat(chatID: String) async throws -> ChatDetail {
        let response = try await request("/v1/chats/\(chatID)", method: "GET", responseType: ChatDetailResponse.self)
        return response.chat
    }

    func forkChat(chatID: String, messageID: String? = nil) async throws -> ChatSummary {
        let response = try await request(
            "/v1/chats/\(chatID)/fork",
            method: "POST",
            body: AnyEncodable(ChatForkBody(messageId: messageID)),
            responseType: ChatCreateResponse.self
        )
        return response.chat
    }

    func updateChatTitle(chatID: String, title: String) async throws -> ChatSummary {
        let response = try await request(
            "/v1/chats/\(chatID)",
            method: "PATCH",
            body: AnyEncodable(ChatTitleUpdateBody(title: title)),
            responseType: ChatCreateResponse.self
        )
        return response.chat
    }

    func updateChatStar(chatID: String, starred: Bool) async throws -> ChatSummary {
        let response = try await request(
            "/v1/chats/\(chatID)/star",
            method: "PATCH",
            body: AnyEncodable(StarUpdateBody(starred: starred)),
            responseType: ChatCreateResponse.self
        )
        return response.chat
    }

    func deleteChat(chatID: String) async throws {
        _ = try await request("/v1/chats/\(chatID)", method: "DELETE", responseType: DeleteResponse.self)
    }

    func suggestChatTitle(chatID: String, content: String) async throws -> ChatSummary {
        let response = try await request(
            "/v1/chats/title/suggest",
            method: "POST",
            body: AnyEncodable(SuggestTitleBody(chatId: chatID, content: content)),
            responseType: ChatCreateResponse.self
        )
        return response.chat
    }

    func listSearches() async throws -> [SearchSummary] {
        let response = try await request("/v1/searches", method: "GET", responseType: SearchListResponse.self)
        return response.searches
    }

    func createSearch(title: String? = nil, query: String? = nil) async throws -> SearchSummary {
        let response = try await request(
            "/v1/searches",
            method: "POST",
            body: AnyEncodable(SearchCreateBody(title: title, query: query)),
            responseType: SearchCreateResponse.self
        )
        return response.search
    }

    func getSearch(searchID: String) async throws -> SearchDetail {
        let response = try await request("/v1/searches/\(searchID)", method: "GET", responseType: SearchDetailResponse.self)
        return response.search
    }

    func createChatFromSearch(searchID: String, title: String? = nil) async throws -> ChatSummary {
        let response = try await request(
            "/v1/searches/\(searchID)/chat",
            method: "POST",
            body: AnyEncodable(SearchChatCreateBody(title: title)),
            responseType: ChatCreateResponse.self
        )
        return response.chat
    }

    func updateSearchStar(searchID: String, starred: Bool) async throws -> SearchSummary {
        let response = try await request(
            "/v1/searches/\(searchID)/star",
            method: "PATCH",
            body: AnyEncodable(StarUpdateBody(starred: starred)),
            responseType: SearchCreateResponse.self
        )
        return response.search
    }

    func deleteSearch(searchID: String) async throws {
        _ = try await request("/v1/searches/\(searchID)", method: "DELETE", responseType: DeleteResponse.self)
    }

    func listModels() async throws -> ModelCatalogResponse {
        try await request("/v1/models", method: "GET", responseType: ModelCatalogResponse.self)
    }

    func getActiveRuns() async throws -> ActiveRunsResponse {
        try await request("/v1/active-runs", method: "GET", responseType: ActiveRunsResponse.self)
    }

    func runCompletionStream(
        body: CompletionStreamRequest,
        onEvent: @escaping @Sendable (CompletionStreamEvent) async -> Void
    ) async throws {
        let request = try makeRequest(
            path: "/v1/chat-completions/stream",
            method: "POST",
            body: AnyEncodable(body),
            acceptsSSE: true
        )

        SybilLog.info(
            SybilLog.network,
            "Starting chat stream POST \(request.url?.absoluteString ?? "<unknown>")"
        )

        try await stream(request: request) { eventName, dataText in
            try await Self.handleCompletionStreamEvent(eventName: eventName, dataText: dataText, onEvent: onEvent)
        }

        SybilLog.info(SybilLog.network, "Chat stream completed")
    }

    func runQuickQuestionStream(
        body: QuickQuestionStreamRequest,
        onEvent: @escaping @Sendable (CompletionStreamEvent) async -> Void
    ) async throws {
        let request = try makeRequest(
            path: "/v1/quick-questions/stream",
            method: "POST",
            body: AnyEncodable(body),
            acceptsSSE: true
        )

        SybilLog.info(
            SybilLog.network,
            "Starting quick question stream POST \(request.url?.absoluteString ?? "<unknown>")"
        )

        try await stream(request: request) { eventName, dataText in
            try await Self.handleCompletionStreamEvent(eventName: eventName, dataText: dataText, onEvent: onEvent)
        }

        SybilLog.info(SybilLog.network, "Quick question stream completed")
    }

    func attachCompletionStream(
        chatID: String,
        onEvent: @escaping @Sendable (CompletionStreamEvent) async -> Void
    ) async throws {
        let request = try makeRequest(
            path: "/v1/chats/\(chatID)/stream/attach",
            method: "POST",
            body: nil,
            acceptsSSE: true
        )

        SybilLog.info(
            SybilLog.network,
            "Attaching chat stream POST \(request.url?.absoluteString ?? "<unknown>")"
        )

        try await stream(request: request) { eventName, dataText in
            try await Self.handleCompletionStreamEvent(eventName: eventName, dataText: dataText, onEvent: onEvent)
        }

        SybilLog.info(SybilLog.network, "Attached chat stream completed")
    }

    func runSearchStream(
        searchID: String,
        body: SearchRunRequest,
        onEvent: @escaping @Sendable (SearchStreamEvent) async -> Void
    ) async throws {
        let request = try makeRequest(
            path: "/v1/searches/\(searchID)/run/stream",
            method: "POST",
            body: AnyEncodable(body),
            acceptsSSE: true
        )

        SybilLog.info(
            SybilLog.network,
            "Starting search stream POST \(request.url?.absoluteString ?? "<unknown>")"
        )

        try await stream(request: request) { eventName, dataText in
            try await Self.handleSearchStreamEvent(eventName: eventName, dataText: dataText, onEvent: onEvent)
        }

        SybilLog.info(SybilLog.network, "Search stream completed")
    }

    func attachSearchStream(
        searchID: String,
        onEvent: @escaping @Sendable (SearchStreamEvent) async -> Void
    ) async throws {
        let request = try makeRequest(
            path: "/v1/searches/\(searchID)/run/stream/attach",
            method: "POST",
            body: nil,
            acceptsSSE: true
        )

        SybilLog.info(
            SybilLog.network,
            "Attaching search stream POST \(request.url?.absoluteString ?? "<unknown>")"
        )

        try await stream(request: request) { eventName, dataText in
            try await Self.handleSearchStreamEvent(eventName: eventName, dataText: dataText, onEvent: onEvent)
        }

        SybilLog.info(SybilLog.network, "Attached search stream completed")
    }

    private func request<Response: Decodable>(
        _ path: String,
        method: String,
        body: AnyEncodable? = nil,
        responseType: Response.Type
    ) async throws -> Response {
        let request = try makeRequest(path: path, method: method, body: body, acceptsSSE: false)

        SybilLog.debug(
            SybilLog.network,
            "HTTP request \(method) \(request.url?.absoluteString ?? "<unknown>")"
        )

        let data: Data
        let response: URLResponse

        do {
            (data, response) = try await session.data(for: request)
        } catch {
            if let urlError = error as? URLError, urlError.code == .cancelled {
                SybilLog.debug(
                    SybilLog.network,
                    "HTTP request cancelled \(method) \(request.url?.absoluteString ?? "<unknown>")"
                )
                throw CancellationError()
            }
            let wrapped = Self.wrapTransportError(error, method: method, url: request.url)
            SybilLog.error(SybilLog.network, "HTTP transport failure", error: wrapped)
            throw wrapped
        }

        try validate(response: response, data: data)

        do {
            let decoded = try Self.decodeJSON(Response.self, from: data)
            if let httpResponse = response as? HTTPURLResponse {
                SybilLog.debug(
                    SybilLog.network,
                    "HTTP response \(httpResponse.statusCode) for \(method) \(request.url?.path(percentEncoded: false) ?? path)"
                )
            }
            return decoded
        } catch let decodingError as DecodingError {
            let details = SybilLog.describe(decodingError)
            let snippet = Self.responseSnippet(data)
            let message = "Failed to decode response for \(method) \(request.url?.absoluteString ?? path): \(details). Body: \(snippet)"
            SybilLog.error(SybilLog.network, message)
            throw APIError.decodingError(message: message)
        } catch {
            SybilLog.error(SybilLog.network, "Unexpected decoding failure", error: error)
            throw error
        }
    }

    private func makeRequest(
        path: String,
        method: String,
        body: AnyEncodable?,
        acceptsSSE: Bool
    ) throws -> URLRequest {
        let url = try buildURL(path: path)
        var request = URLRequest(url: url)
        request.httpMethod = method
        request.timeoutInterval = 120

        if acceptsSSE {
            request.setValue("text/event-stream", forHTTPHeaderField: "Accept")
        } else {
            request.setValue("application/json", forHTTPHeaderField: "Accept")
        }

        if let token = configuration.authToken?.trimmingCharacters(in: .whitespacesAndNewlines), !token.isEmpty {
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }

        if let body {
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.httpBody = try Self.encodeJSON(body)
        }

        return request
    }

    private func buildURL(path: String) throws -> URL {
        guard var components = URLComponents(url: configuration.baseURL, resolvingAgainstBaseURL: false) else {
            throw APIError.invalidBaseURL
        }

        let trimmedPath = path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
        var basePath = components.path
        if basePath.hasSuffix("/") {
            basePath.removeLast()
        }
        components.path = "\(basePath)/\(trimmedPath)"

        guard let url = components.url else {
            throw APIError.invalidBaseURL
        }

        return url
    }

    private func validate(response: URLResponse, data: Data) throws {
        guard let httpResponse = response as? HTTPURLResponse else {
            throw APIError.invalidResponse
        }

        guard (200 ... 299).contains(httpResponse.statusCode) else {
            let message = Self.parseMessage(from: data) ?? "\(httpResponse.statusCode) \(HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode))"
            SybilLog.warning(
                SybilLog.network,
                "HTTP non-success status \(httpResponse.statusCode): \(message)"
            )
            throw APIError.httpError(statusCode: httpResponse.statusCode, message: message)
        }
    }

    private static func parseMessage(from data: Data) -> String? {
        guard !data.isEmpty else { return nil }
        if let decoded = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
           let message = decoded["message"] as? String,
           !message.isEmpty {
            return message
        }
        return nil
    }

    private func stream(
        request: URLRequest,
        onEvent: @escaping @Sendable (_ eventName: String, _ dataText: String) async throws -> Void
    ) async throws {
        let bytes: URLSession.AsyncBytes
        let response: URLResponse

        do {
            (bytes, response) = try await session.bytes(for: request)
        } catch {
            if let urlError = error as? URLError, urlError.code == .cancelled {
                SybilLog.debug(
                    SybilLog.network,
                    "SSE request cancelled \(request.httpMethod ?? "GET") \(request.url?.absoluteString ?? "<unknown>")"
                )
                throw CancellationError()
            }
            let wrapped = Self.wrapTransportError(error, method: request.httpMethod ?? "GET", url: request.url)
            SybilLog.error(SybilLog.network, "SSE transport failure", error: wrapped)
            throw wrapped
        }

        try validate(response: response, data: Data())

        var eventName = "message"
        var dataLines: [String] = []
        var lineBytes: [UInt8] = []

        for try await byte in bytes {
            if Task.isCancelled {
                SybilLog.warning(SybilLog.network, "SSE task cancelled")
                throw CancellationError()
            }

            if byte == 0x0A { // \n
                var bytesForLine = lineBytes
                if bytesForLine.last == 0x0D { // \r
                    bytesForLine.removeLast()
                }
                let line = String(decoding: bytesForLine, as: UTF8.self)
                lineBytes.removeAll(keepingCapacity: true)

                if line.isEmpty {
                    if let emitted = Self.flushSSEEvent(eventName: &eventName, dataLines: &dataLines) {
                        SybilLog.debug(SybilLog.network, "SSE event \(emitted.name) payload chars=\(emitted.payload.count)")
                        try await onEvent(emitted.name, emitted.payload)
                    }
                    continue
                }

                if line.hasPrefix(":") {
                    continue
                }

                if line.hasPrefix("event:") {
                    if let emitted = Self.flushSSEEvent(eventName: &eventName, dataLines: &dataLines) {
                        SybilLog.debug(SybilLog.network, "SSE event \(emitted.name) payload chars=\(emitted.payload.count)")
                        try await onEvent(emitted.name, emitted.payload)
                    }

                    eventName = line.dropFirst("event:".count).trimmingCharacters(in: .whitespaces)
                    continue
                }

                if line.hasPrefix("data:") {
                    let payload = line.dropFirst("data:".count)
                    dataLines.append(Self.trimLeadingWhitespace(payload))
                    continue
                }

                SybilLog.debug(SybilLog.network, "Ignoring SSE line '\(String(line.prefix(120)))'")
            } else {
                lineBytes.append(byte)
            }
        }

        if !lineBytes.isEmpty {
            var bytesForLine = lineBytes
            if bytesForLine.last == 0x0D { // \r
                bytesForLine.removeLast()
            }
            let line = String(decoding: bytesForLine, as: UTF8.self)

            if line.isEmpty {
                if let emitted = Self.flushSSEEvent(eventName: &eventName, dataLines: &dataLines) {
                    SybilLog.debug(SybilLog.network, "SSE event \(emitted.name) payload chars=\(emitted.payload.count)")
                    try await onEvent(emitted.name, emitted.payload)
                }
            } else if line.hasPrefix("event:") {
                if let emitted = Self.flushSSEEvent(eventName: &eventName, dataLines: &dataLines) {
                    SybilLog.debug(SybilLog.network, "SSE event \(emitted.name) payload chars=\(emitted.payload.count)")
                    try await onEvent(emitted.name, emitted.payload)
                }
                eventName = line.dropFirst("event:".count).trimmingCharacters(in: .whitespaces)
            } else if line.hasPrefix("data:") {
                let payload = line.dropFirst("data:".count)
                dataLines.append(Self.trimLeadingWhitespace(payload))
            } else if !line.hasPrefix(":") {
                SybilLog.debug(SybilLog.network, "Ignoring SSE line '\(String(line.prefix(120)))'")
            }
        }

        if let emitted = Self.flushSSEEvent(eventName: &eventName, dataLines: &dataLines) {
            SybilLog.debug(SybilLog.network, "SSE event \(emitted.name) payload chars=\(emitted.payload.count)")
            try await onEvent(emitted.name, emitted.payload)
        }
    }

    private static func decodeJSON<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .custom { decoder in
            let container = try decoder.singleValueContainer()
            let string = try container.decode(String.self)
            if let value = Self.iso8601FormatterWithFractional.date(from: string) {
                return value
            }
            if let value = Self.iso8601Formatter.date(from: string) {
                return value
            }
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "Expected ISO-8601 date")
        }
        return try decoder.decode(T.self, from: data)
    }

    private static func decodeEvent<T: Decodable>(_ dataText: String, as type: T.Type, eventName: String) throws -> T {
        guard let data = dataText.data(using: .utf8) else {
            let message = "Failed to decode SSE event '\(eventName)': payload is not UTF-8"
            SybilLog.error(SybilLog.network, message)
            throw APIError.decodingError(message: message)
        }

        do {
            return try Self.decodeJSON(type, from: data)
        } catch let decodingError as DecodingError {
            let details = SybilLog.describe(decodingError)
            let snippet = dataText.replacingOccurrences(of: "\n", with: " ").prefix(400)
            let message = "Failed to decode SSE event '\(eventName)': \(details). Payload: \(snippet)"
            SybilLog.error(SybilLog.network, message)
            throw APIError.decodingError(message: message)
        }
    }

    private static func decodeLastJSONLine<T: Decodable>(_ dataText: String, as type: T.Type) -> T? {
        let lines = dataText
            .split(whereSeparator: \.isNewline)
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty }

        guard let last = lines.last, let data = last.data(using: .utf8) else {
            return nil
        }

        return try? Self.decodeJSON(type, from: data)
    }

    private static func handleCompletionStreamEvent(
        eventName: String,
        dataText: String,
        onEvent: @escaping @Sendable (CompletionStreamEvent) async -> Void
    ) async throws {
        switch eventName {
        case "meta":
            let payload: CompletionStreamMeta = try Self.decodeEvent(dataText, as: CompletionStreamMeta.self, eventName: eventName)
            await onEvent(.meta(payload))
        case "tool_call":
            let payload: CompletionStreamToolCall = try Self.decodeEvent(dataText, as: CompletionStreamToolCall.self, eventName: eventName)
            await onEvent(.toolCall(payload))
        case "delta":
            let payload: CompletionStreamDelta = try Self.decodeEvent(dataText, as: CompletionStreamDelta.self, eventName: eventName)
            await onEvent(.delta(payload))
        case "done":
            do {
                let payload: CompletionStreamDone = try Self.decodeEvent(dataText, as: CompletionStreamDone.self, eventName: eventName)
                await onEvent(.done(payload))
            } catch {
                if let recovered = Self.decodeLastJSONLine(dataText, as: CompletionStreamDone.self) {
                    SybilLog.warning(
                        SybilLog.network,
                        "Recovered chat stream done payload from concatenated SSE data"
                    )
                    await onEvent(.done(recovered))
                } else {
                    throw error
                }
            }
        case "error":
            let payload: StreamErrorPayload = try Self.decodeEvent(dataText, as: StreamErrorPayload.self, eventName: eventName)
            await onEvent(.error(payload))
        default:
            SybilLog.warning(SybilLog.network, "Ignoring unknown chat stream event '\(eventName)'")
            await onEvent(.ignored)
        }
    }

    private static func handleSearchStreamEvent(
        eventName: String,
        dataText: String,
        onEvent: @escaping @Sendable (SearchStreamEvent) async -> Void
    ) async throws {
        switch eventName {
        case "search_results":
            let payload: SearchResultsPayload = try Self.decodeEvent(dataText, as: SearchResultsPayload.self, eventName: eventName)
            await onEvent(.searchResults(payload))
        case "search_error":
            let payload: SearchErrorPayload = try Self.decodeEvent(dataText, as: SearchErrorPayload.self, eventName: eventName)
            await onEvent(.searchError(payload))
        case "answer":
            let payload: SearchAnswerPayload = try Self.decodeEvent(dataText, as: SearchAnswerPayload.self, eventName: eventName)
            await onEvent(.answer(payload))
        case "answer_error":
            let payload: SearchErrorPayload = try Self.decodeEvent(dataText, as: SearchErrorPayload.self, eventName: eventName)
            await onEvent(.answerError(payload))
        case "done":
            let payload: SearchDonePayload = try Self.decodeEvent(dataText, as: SearchDonePayload.self, eventName: eventName)
            await onEvent(.done(payload))
        case "error":
            let payload: StreamErrorPayload = try Self.decodeEvent(dataText, as: StreamErrorPayload.self, eventName: eventName)
            await onEvent(.error(payload))
        default:
            SybilLog.warning(SybilLog.network, "Ignoring unknown search stream event '\(eventName)'")
            await onEvent(.ignored)
        }
    }

    private static func flushSSEEvent(
        eventName: inout String,
        dataLines: inout [String]
    ) -> (name: String, payload: String)? {
        guard !dataLines.isEmpty else {
            eventName = "message"
            return nil
        }

        let name = eventName
        let payload = dataLines.joined(separator: "\n")
        dataLines.removeAll(keepingCapacity: true)
        eventName = "message"
        return (name, payload)
    }

    private static func trimLeadingWhitespace(_ text: Substring) -> String {
        var index = text.startIndex
        while index < text.endIndex, text[index].isWhitespace {
            index = text.index(after: index)
        }
        return String(text[index...])
    }

    private static func encodeJSON<T: Encodable>(_ value: T) throws -> Data {
        let encoder = JSONEncoder()
        return try encoder.encode(value)
    }

    private static func wrapTransportError(_ error: Error, method: String, url: URL?) -> APIError {
        if let urlError = error as? URLError {
            return APIError.networkError(
                message: "Network error \(urlError.code.rawValue) while requesting \(method) \(url?.absoluteString ?? "<unknown>"): \(urlError.localizedDescription)"
            )
        }

        return APIError.networkError(
            message: "Network request failed for \(method) \(url?.absoluteString ?? "<unknown>"): \(error.localizedDescription)"
        )
    }

    private static func responseSnippet(_ data: Data) -> String {
        guard !data.isEmpty else { return "<empty>" }
        if let string = String(data: data, encoding: .utf8) {
            let normalized = string.replacingOccurrences(of: "\n", with: " ")
            return String(normalized.prefix(500))
        }
        return "<non-utf8 body, \(data.count) bytes>"
    }
}

struct CompletionStreamRequest: Codable, Sendable {
    var chatId: String?
    var persist: Bool? = nil
    var provider: Provider
    var model: String
    var messages: [CompletionRequestMessage]
    var userLocation: String? = nil
}

struct QuickQuestionStreamRequest: Codable, Sendable {
    var provider: Provider
    var model: String
    var question: String
}

private struct ChatCreateBody: Encodable {
    var title: String?
    var provider: Provider?
    var model: String?
    var messages: [CompletionRequestMessage]?
}

private struct ChatTitleUpdateBody: Encodable {
    var title: String
}

private struct ChatForkBody: Encodable {
    var messageId: String?
}

private struct StarUpdateBody: Encodable {
    var starred: Bool
}

private struct SearchCreateBody: Encodable {
    var title: String?
    var query: String?
}

private struct SearchChatCreateBody: Encodable {
    var title: String?
}
