import ComposableArchitecture
import Foundation

/// Generic SSE client implementing the WHATWG Server-Sent Events specification.
/// https://html.spec.whatwg.org/multipage/server-sent-events.html
@DependencyClient
public struct SseClient {
    /// Subscribes to an SSE stream with automatic reconnection.
    public var subscribe: (
        _ config: SseConnectionConfig,
        _ authTokenProvider: @escaping () async throws -> SseAuthToken
    ) -> AsyncThrowingStream<SseEvent, Error> = { _, _ in .never }
}

extension SseClient: DependencyKey {
    public static var liveValue: SseClient {
        let TOKEN_REFRESH_BUFFER_MS: Int64 = 30000 // 30 seconds

        let urlSession: URLSession = .shared
        let lineParser: SseLineParser = .init()

        func connect(
            config: SseConnectionConfig,
            lastEventId: String?,
            authTokenProvider: @escaping () async throws -> SseAuthToken,
            onEvent: @escaping (SseEvent) -> Void
        ) async throws -> ConnectionResult {
            do {
                let authToken = try await authTokenProvider()

                let request = try createRequest(
                    config: config,
                    lastEventId: lastEventId,
                    authToken: authToken
                )

                let (asyncBytes, response) = try await urlSession.bytes(for: request)

                guard let httpResponse = response as? HTTPURLResponse else {
                    throw NSError(domain: "SseClient", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid response"])
                }

                guard (200...299).contains(httpResponse.statusCode) else {
                    throw NSError(
                        domain: "SseClient",
                        code: httpResponse.statusCode,
                        userInfo: [NSLocalizedDescriptionKey: "SSE connection failed: \(httpResponse.statusCode) \(HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode))"]
                    )
                }

                onEvent(.connected)

                // Calculate connection timeout based on token expiry
                let connectionTimeoutMs = calculateConnectionTimeout(authToken: authToken)
                if let timeout = connectionTimeoutMs, timeout <= 0 {
                    throw NSError(domain: "SseClient", code: -1, userInfo: [NSLocalizedDescriptionKey: "Token expired or expiring too soon"])
                }

                if let timeout = connectionTimeoutMs {
                    let timeoutMinutes = timeout / 60_000
                    print("📡 Connection will timeout in \(timeoutMinutes) minutes to refresh token")
                }

                // Process SSE stream with optional timeout
                do {
                    if let timeout = connectionTimeoutMs {
                        try await withTimeout(nanoseconds: UInt64(timeout * 1_000_000)) {
                            try await processSseStream(
                                asyncBytes: asyncBytes,
                                config: config,
                                onEvent: onEvent
                            )
                        }
                    } else {
                        try await processSseStream(
                            asyncBytes: asyncBytes,
                            config: config,
                            onEvent: onEvent
                        )
                    }
                } catch is TimeoutError {
                    print("📡 Connection timed out, reconnecting")
                } catch {
                    throw error
                }

                return .streamEnded
            } catch {
                if error is CancellationError {
                    print("📡 Connection cancelled")
                    throw error
                } else {
                    print("❌ Connection attempt failed: \(error)")
                    return .failure(error)
                }
            }
        }

        func createRequest(
            config: SseConnectionConfig,
            lastEventId: String?,
            authToken: SseAuthToken
        ) throws -> URLRequest {
            guard let url = URL(string: config.url) else {
                throw NSError(domain: "SseClient", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])
            }

            var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)

            var queryItems = config.queryParameters.map { URLQueryItem(name: $0.key, value: $0.value) }

            // Add lastEventId as query parameter for reconnection
            if let eventId = lastEventId {
                queryItems.append(URLQueryItem(name: "lastEventId", value: eventId))
                print("📡 Reconnecting with lastEventId: \(eventId)")
            }

            urlComponents?.queryItems = queryItems

            guard let finalURL = urlComponents?.url else {
                throw NSError(domain: "SseClient", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to build URL"])
            }

            var request = URLRequest(url: finalURL)
            request.setValue("text/event-stream", forHTTPHeaderField: "Accept")
            request.setValue("no-cache", forHTTPHeaderField: "Cache-Control")
            request.setValue(authToken.authorizationHeader, forHTTPHeaderField: "Authorization")

            return request
        }

        func calculateConnectionTimeout(authToken: SseAuthToken) -> Int64? {
            guard let expiresAtMs = authToken.expiresAtMs else {
                return nil
            }

            let currentTimeMs = Int64(Date().timeIntervalSince1970 * 1000)
            let timeUntilExpiry = expiresAtMs - currentTimeMs
            return timeUntilExpiry - TOKEN_REFRESH_BUFFER_MS
        }

        func processSseStream(
            asyncBytes: URLSession.AsyncBytes,
            config: SseConnectionConfig,
            onEvent: @escaping (SseEvent) -> Void
        ) async throws {
            var currentEventId: String? = nil
            var dataAccumulator = ""
            var lineBuffer = ""

            // Read bytes and accumulate into lines
            for try await byte in asyncBytes {
                guard !Task.isCancelled else { break }

                // TODO: (JY) Claude mentioned that this may be slow.
                // "Performance concern: Character-by-character processing can be slow for large SSE payloads. Consider processing in larger chunks."
                let character = Character(UnicodeScalar(byte))

                if character == "\n" {
                    // Process complete line
                    let line = lineBuffer.trimmingCharacters(in: .whitespacesAndNewlines)
                    lineBuffer = ""

                    let parsedLine = lineParser.parseLine(line)

                    switch parsedLine {
                    case .id(let id):
                        print("📡 Received and parsed line: id=\(id)")
                        currentEventId = id

                    case .data(let data):
                        print("📡 Received and parsed line: data=\(data.prefix(50))...")

                        // Prevent memory leak from malformed SSE streams
                        if dataAccumulator.utf8.count > config.maxEventSizeBytes {
                            print("⚠️ SSE event exceeded max size (\(dataAccumulator.utf8.count) bytes), discarding incomplete event")
                            currentEventId = nil
                            dataAccumulator = ""
                            continue
                        }

                        // Per SSE spec: multiple data lines are joined with newlines
                        if !dataAccumulator.isEmpty {
                            dataAccumulator += "\n"
                        }
                        dataAccumulator += data

                    case .endOfEvent:
                        if !dataAccumulator.isEmpty {
                            print("📡 Received end of event: \(dataAccumulator.prefix(50))...")

                            let event = SseEvent.message(data: dataAccumulator, id: currentEventId)
                            onEvent(event)

                            dataAccumulator = ""
                        }

                        // Reset for next event
                        currentEventId = nil

                    case .event, .retry, .comment, .other:
                        print("📡 Received and parsed \(parsedLine), ignoring")
                    }
                } else if character != "\r" {
                    // Accumulate characters (ignore \r for Windows line endings)
                    lineBuffer.append(character)
                }
            }

            // Process any remaining line buffer
            if !lineBuffer.isEmpty {
                let line = lineBuffer.trimmingCharacters(in: .whitespacesAndNewlines)
                if !line.isEmpty {
                    let parsedLine = lineParser.parseLine(line)
                    if case .data(let data) = parsedLine {
                        if !dataAccumulator.isEmpty {
                            dataAccumulator += "\n"
                        }
                        dataAccumulator += data
                    }
                }
            }

            // Emit final event if there's accumulated data
            if !dataAccumulator.isEmpty {
                let event = SseEvent.message(data: dataAccumulator, id: currentEventId)
                onEvent(event)
            }
        }

        func calculateRetryDelay(attempt: Int, config: SseConnectionConfig) -> Int64 {
            let cappedAttempt = min(attempt, config.maxRetryAttemptForBackoff)
            let exponentialDelay = Double(config.initialRetryDelayMs) * pow(config.retryBackoffMultiplier, Double(cappedAttempt))
            return min(Int64(exponentialDelay), config.maxRetryDelayMs)
        }

        return Self(
            subscribe: { config, authTokenProvider in
                AsyncThrowingStream { continuation in
                    Task {
                        var retryAttempt = 0
                        var lastEventId: String? = nil

                        while !Task.isCancelled {
                            do {
                                let connectionResult = try await connect(
                                    config: config,
                                    lastEventId: lastEventId,
                                    authTokenProvider: authTokenProvider,
                                    onEvent: { event in
                                        if case .message(let data, let id) = event {
                                            if let id = id {
                                                lastEventId = id
                                            }
                                        }
                                        continuation.yield(event)
                                    }
                                )

                                switch connectionResult {
                                case .streamEnded:
                                    continuation.yield(.disconnected)
                                    retryAttempt = 0

                                case .failure(let error):
                                    continuation.yield(.error(error))

                                    if retryAttempt >= config.maxRetries {
                                        print("⚠️ Max retry attempts (\(config.maxRetries)) exceeded, stopping reconnection")
                                        continuation.finish(throwing: error)
                                        break
                                    }

                                    let delayMs = calculateRetryDelay(
                                        attempt: retryAttempt,
                                        config: config
                                    )
                                    try await Task.sleep(nanoseconds: UInt64(delayMs * 1_000_000))

                                    retryAttempt += 1
                                }
                            } catch {
                                continuation.finish(throwing: error)
                                break
                            }
                        }
                    }
                }
            }
        )
    }
}

// Helper for timeout
private func withTimeout<T>(
    nanoseconds: UInt64,
    operation: @escaping () async throws -> T
) async throws -> T {
    try await withThrowingTaskGroup(of: T.self) { group in
        group.addTask {
            try await operation()
        }

        group.addTask {
            try await Task.sleep(nanoseconds: nanoseconds)
            throw TimeoutError()
        }

        guard let result = try await group.next() else {
            throw NSError(domain: "SseClient", code: -1, userInfo: [NSLocalizedDescriptionKey: "No result from operation"])
        }
        group.cancelAll()
        return result
    }
}

private struct TimeoutError: Error {
    var localizedDescription: String {
        "Connection timeout"
    }
}

/// Connection result for SSE connection attempts
private enum ConnectionResult {
    case streamEnded
    case failure(Error)
}
