import Foundation

/// Thread-safe diagnostics client using Swift actors
public actor DiagnosticsClient {
    public static let shared = DiagnosticsClient()
    
    private var configuration: DiagnosticsConfiguration?
    private var logger: DiagnosticsLogger = NoOpDiagnosticsLogger()
    private var state: DiagnosticsClientState = .idle
    private var breadcrumbs = BreadcrumbTrail(maxSize: 100)
    private var logBuffer: [DiagnosticsLogEntry] = []
    private var traceBuffer: [DiagnosticsTraceSpan] = []
    private var networkBuffer: [DiagnosticsNetworkRequest] = []
    private var pollTimer: Timer?
    private var flushTask: Task<Void, Never>?
    private var lastEtag: String?
    private var networkInterceptor: NetworkInterceptor?
    
    // MARK: - Singleton
    
    private init() {}
    
    // MARK: - Configuration
    
    public func configure(_ config: DiagnosticsConfiguration, logger: DiagnosticsLogger? = nil) {
        self.configuration = config
        self.logger = logger ?? NoOpDiagnosticsLogger()
        self.breadcrumbs = BreadcrumbTrail(maxSize: config.maxBreadcrumbs)
    }
    
    // MARK: - Lifecycle
    
    public func start() async {
        guard case .idle = state else {
            await logger.warn("[diagnostics] Already started", metadata: nil)
            return
        }
        
        guard let config = configuration else {
            await logger.error("[diagnostics] Not configured", metadata: nil)
            state = .error(DiagnosticsError.notConfigured)
            return
        }
        
        await logger.info("[diagnostics] Starting diagnostics client", metadata: nil)
        state = .polling(session: nil)
        
        // Initial poll
        await pollForSession()
        
        // Setup network capture if enabled
        if config.captureNetwork {
            setupNetworkCapture()
        }
        
        // Start flush timer (every 30 seconds)
        startFlushTimer()
        
        await breadcrumbs.add(category: "diagnostics", message: "Client started")
    }
    
    public func stop() async {
        await logger.info("[diagnostics] Stopping diagnostics client", metadata: nil)
        
        pollTimer?.invalidate()
        pollTimer = nil
        
        flushTask?.cancel()
        flushTask = nil
        
        networkInterceptor?.stop()
        networkInterceptor = nil
        
        // Final flush
        await flush()
        
        state = .idle
        await breadcrumbs.add(category: "diagnostics", message: "Client stopped")
    }
    
    // MARK: - State
    
    public func isSessionActive() -> Bool {
        if case .active = state {
            return true
        }
        return false
    }
    
    public func getCurrentSession() -> DiagnosticsSession? {
        switch state {
        case .active(let session), .polling(let session):
            return session
        default:
            return nil
        }
    }
    
    public func getState() -> DiagnosticsClientState {
        state
    }
    
    // MARK: - Logging
    
    public func log(
        level: DiagnosticsLogLevel,
        message: String,
        module: String = "unknown",
        file: String? = nil,
        line: Int? = nil,
        function: String? = nil,
        context: [String: AnyCodable] = [:],
        correlationId: String? = nil
    ) async {
        let entry = DiagnosticsLogEntry(
            level: level,
            message: message,
            timestamp: ISO8601DateFormatter().string(from: Date()),
            module: module,
            file: file,
            line: line,
            function: function,
            context: context,
            correlationId: correlationId
        )
        
        logBuffer.append(entry)
        await breadcrumbs.add(
            category: "log",
            message: "[\(level.rawValue.uppercased())] \(String(message.prefix(100)))",
            data: ["level": AnyCodable(level.rawValue)]
        )
        
        // Auto-flush on fatal
        if level == .fatal {
            Task { await flush() }
        }
    }
    
    // MARK: - Tracing
    
    public func trace<T>(
        name: String,
        operation: () async throws -> T
    ) async rethrows -> T {
        let spanId = generateId()
        let startTime = ISO8601DateFormatter().string(from: Date())
        
        await breadcrumbs.add(
            category: "trace",
            message: "Starting: \(name)",
            data: ["spanId": AnyCodable(spanId)]
        )
        
        do {
            let result = try await operation()
            let endTime = ISO8601DateFormatter().string(from: Date())
            let start = ISO8601DateFormatter().date(from: startTime) ?? Date()
            let end = ISO8601DateFormatter().date(from: endTime) ?? Date()
            let durationMs = end.timeIntervalSince(start) * 1000
            
            let span = DiagnosticsTraceSpan(
                spanId: spanId,
                name: name,
                startTime: startTime,
                endTime: endTime,
                durationMs: durationMs,
                attributes: [:],
                status: .ok
            )
            traceBuffer.append(span)
            
            await breadcrumbs.add(
                category: "trace",
                message: "Completed: \(name)",
                data: [
                    "spanId": AnyCodable(spanId),
                    "durationMs": AnyCodable(durationMs)
                ]
            )
            
            return result
        } catch {
            let endTime = ISO8601DateFormatter().string(from: Date())
            let start = ISO8601DateFormatter().date(from: startTime) ?? Date()
            let end = ISO8601DateFormatter().date(from: endTime) ?? Date()
            let durationMs = end.timeIntervalSince(start) * 1000
            
            let span = DiagnosticsTraceSpan(
                spanId: spanId,
                name: name,
                startTime: startTime,
                endTime: endTime,
                durationMs: durationMs,
                attributes: [:],
                status: .error,
                statusMessage: error.localizedDescription
            )
            traceBuffer.append(span)
            
            await breadcrumbs.add(
                category: "trace",
                message: "Failed: \(name)",
                data: [
                    "spanId": AnyCodable(spanId),
                    "error": AnyCodable(error.localizedDescription)
                ]
            )
            
            throw error
        }
    }
    
    // MARK: - Breadcrumbs
    
    public func breadcrumb(
        category: String,
        message: String,
        data: [String: AnyCodable]? = nil
    ) async {
        await breadcrumbs.add(category: category, message: message, data: data)
    }
    
    public func getBreadcrumbs() -> [DiagnosticsBreadcrumb] {
        breadcrumbs.getAll()
    }
    
    // MARK: - Private
    
    private func pollForSession() async {
        guard let config = configuration else { return }
        
        var request = URLRequest(
            url: URL(string: "\(config.serverUrl)/api/diagnostics/config?productId=\(config.productId)&installId=\(config.anonymousInstallId)")!
        )
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        
        if let etag = lastEtag {
            request.setValue(etag, forHTTPHeaderField: "If-None-Match")
        }
        
        if let getToken = config.getAuthToken {
            do {
                let token = try await getToken()
                request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
            } catch {
                await logger.error("[diagnostics] Failed to get auth token", metadata: ["error": error.localizedDescription])
            }
        }
        
        do {
            let (data, response) = try await URLSession.shared.data(for: request)
            
            guard let httpResponse = response as? HTTPURLResponse else {
                throw DiagnosticsError.invalidResponse
            }
            
            if httpResponse.statusCode == 304 {
                // No change
                return
            }
            
            guard httpResponse.statusCode == 200 else {
                throw DiagnosticsError.httpError(statusCode: httpResponse.statusCode)
            }
            
            // Store ETag
            if let etag = httpResponse.allHeaderFields["Etag"] as? String {
                lastEtag = etag
            }
            
            let decoder = JSONDecoder()
            let session = try? decoder.decode(DiagnosticsSession.self, from: data)
            
            if let session = session, session.status == .active {
                if case .active = state {
                    // Already active, just update session
                } else {
                    await logger.info("[diagnostics] Session activated", metadata: ["sessionId": session.id])
                    await breadcrumbs.add(category: "diagnostics", message: "Session activated", data: ["sessionId": AnyCodable(session.id)])
                }
                state = .active(session: session)
            } else {
                if case .active = state {
                    await logger.info("[diagnostics] Session ended", metadata: nil)
                    await breadcrumbs.add(category: "diagnostics", message: "Session ended")
                }
                state = .polling(session: nil)
            }
        } catch {
            await logger.error("[diagnostics] Failed to poll for session", metadata: ["error": error.localizedDescription])
            state = .error(error)
        }
    }
    
    private func flush() async {
        guard let session = getCurrentSession() else {
            logBuffer.removeAll()
            traceBuffer.removeAll()
            networkBuffer.removeAll()
            return
        }
        
        let batch = DiagnosticsIngestBatch(
            sessionId: session.id,
            traces: traceBuffer.isEmpty ? nil : traceBuffer,
            logs: logBuffer.isEmpty ? nil : logBuffer,
            breadcrumbs: breadcrumbs.getAll().isEmpty ? nil : breadcrumbs.getAll(),
            network: networkBuffer.isEmpty ? nil : networkBuffer
        )
        
        // Clear buffers
        logBuffer.removeAll()
        traceBuffer.removeAll()
        networkBuffer.removeAll()
        breadcrumbs.clear()
        
        guard let config = configuration else { return }
        
        var request = URLRequest(
            url: URL(string: "\(config.serverUrl)/api/diagnostics/ingest")!
        )
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        if let getToken = config.getAuthToken {
            do {
                let token = try await getToken()
                request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
            } catch {
                await logger.error("[diagnostics] Failed to get auth token for flush", metadata: ["error": error.localizedDescription])
            }
        }
        
        do {
            let encoder = JSONEncoder()
            request.httpBody = try encoder.encode(batch)
            
            let (_, response) = try await URLSession.shared.data(for: request)
            
            guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
                throw DiagnosticsError.flushFailed
            }
            
            await logger.debug("[diagnostics] Flushed batch", metadata: [
                "logs": batch.logs?.count ?? 0,
                "traces": batch.traces?.count ?? 0,
                "network": batch.network?.count ?? 0
            ])
        } catch {
            await logger.error("[diagnostics] Failed to flush batch", metadata: ["error": error.localizedDescription])
            
            // Put items back in buffers for retry
            if let logs = batch.logs { logBuffer.append(contentsOf: logs) }
            if let traces = batch.traces { traceBuffer.append(contentsOf: traces) }
            if let network = batch.network { networkBuffer.append(contentsOf: network) }
        }
    }
    
    private func startFlushTimer() {
        flushTask = Task {
            while !Task.isCancelled {
                try? await Task.sleep(nanoseconds: 30 * 1_000_000_000) // 30 seconds
                await flush()
            }
        }
    }
    
    private func setupNetworkCapture() {
        networkInterceptor = NetworkInterceptor { [weak self] request in
            Task { [weak self] in
                await self?.networkBuffer.append(request)
            }
        }
        networkInterceptor?.start()
    }
    
    private func generateId() -> String {
        "\(Int(Date().timeIntervalSince1970 * 1000))_\(UUID().uuidString.prefix(7))"
    }
}

/// Client state enum
public enum DiagnosticsClientState: Sendable {
    case idle
    case polling(session: DiagnosticsSession?)
    case active(session: DiagnosticsSession)
    case error(Error)
}

/// Diagnostics errors
public enum DiagnosticsError: Error, Sendable {
    case notConfigured
    case invalidResponse
    case httpError(statusCode: Int)
    case flushFailed
}
