package com.bytelyst.platform.diagnostics

import android.content.Context
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit

/**
 * Client state
 */
sealed class DiagnosticsClientState {
    object Idle : DiagnosticsClientState()
    data class Polling(val session: DiagnosticsSession?) : DiagnosticsClientState()
    data class Active(val session: DiagnosticsSession) : DiagnosticsClientState()
    data class Error(val exception: Throwable) : DiagnosticsClientState()
}

/**
 * Diagnostics client configuration
 */
data class DiagnosticsConfiguration(
    val productId: String,
    val userId: String? = null,
    val anonymousInstallId: String,
    val platform: String,
    val channel: String,
    val osFamily: String,
    val appVersion: String,
    val buildNumber: String,
    val releaseChannel: String,
    val serverUrl: String,
    val pollIntervalMs: Long = 5000,
    val maxBreadcrumbs: Int = 100,
    val captureConsole: Boolean = true,
    val captureErrors: Boolean = true,
    val captureNetwork: Boolean = true,
    val getAuthToken: (suspend () -> String)? = null
)

/**
 * Logger interface
 */
interface DiagnosticsLogger {
    fun debug(message: String, metadata: Map<String, Any>? = null)
    fun info(message: String, metadata: Map<String, Any>? = null)
    fun warn(message: String, metadata: Map<String, Any>? = null)
    fun error(message: String, metadata: Map<String, Any>? = null)
}

/**
 * No-op logger
 */
class NoOpDiagnosticsLogger : DiagnosticsLogger {
    override fun debug(message: String, metadata: Map<String, Any>?) {}
    override fun info(message: String, metadata: Map<String, Any>?) {}
    override fun warn(message: String, metadata: Map<String, Any>?) {}
    override fun error(message: String, metadata: Map<String, Any>?) {}
}

/**
 * Android Log-based logger
 */
class AndroidDiagnosticsLogger(private val tag: String = "ByteLystDiagnostics") : DiagnosticsLogger {
    override fun debug(message: String, metadata: Map<String, Any>?) {
        android.util.Log.d(tag, message)
    }
    override fun info(message: String, metadata: Map<String, Any>?) {
        android.util.Log.i(tag, message)
    }
    override fun warn(message: String, metadata: Map<String, Any>?) {
        android.util.Log.w(tag, message)
    }
    override fun error(message: String, metadata: Map<String, Any>?) {
        android.util.Log.e(tag, message)
    }
}

/**
 * Main diagnostics client
 */
class DiagnosticsClient private constructor(
    private val context: Context,
    private val config: DiagnosticsConfiguration,
    private val logger: DiagnosticsLogger
) {
    companion object {
        @Volatile
        private var instance: DiagnosticsClient? = null

        fun getInstance(
            context: Context,
            config: DiagnosticsConfiguration,
            logger: DiagnosticsLogger = NoOpDiagnosticsLogger()
        ): DiagnosticsClient {
            return instance ?: synchronized(this) {
                instance ?: DiagnosticsClient(context.applicationContext, config, logger).also {
                    instance = it
                }
            }
        }

        fun reset() {
            instance?.stop()
            instance = null
        }
    }

    private val _state = MutableStateFlow<DiagnosticsClientState>(DiagnosticsClientState.Idle)
    val state: StateFlow<DiagnosticsClientState> = _state.asStateFlow()

    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    private val breadcrumbs = BreadcrumbTrail(maxSize = config.maxBreadcrumbs)
    private val logBuffer = mutableListOf<DiagnosticsLogEntry>()
    private val traceBuffer = mutableListOf<DiagnosticsTraceSpan>()
    private val networkBuffer = mutableListOf<DiagnosticsNetworkRequest>()

    private var pollJob: Job? = null
    private var flushJob: Job? = null
    private var networkInterceptor: NetworkInterceptor? = null
    private var lastEtag: String? = null

    private val httpClient = OkHttpClient.Builder()
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build()

    private val json = Json { ignoreUnknownKeys = true }
    private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
        timeZone = TimeZone.getTimeZone("UTC")
    }

    /**
     * Start polling for active debug sessions
     */
    fun start() {
        if (_state.value != DiagnosticsClientState.Idle) {
            logger.warn("[diagnostics] Already started")
            return
        }

        logger.info("[diagnostics] Starting diagnostics client")
        _state.value = DiagnosticsClientState.Polling(null)

        // Initial poll
        scope.launch {
            pollForSession()
        }

        // Start polling timer
        pollJob = scope.launch {
            while (isActive) {
                delay(config.pollIntervalMs)
                pollForSession()
            }
        }

        // Start auto-flush timer (every 30 seconds)
        flushJob = scope.launch {
            while (isActive) {
                delay(30000)
                flush()
            }
        }

        // Setup network capture if enabled
        if (config.captureNetwork) {
            setupNetworkCapture()
        }

        breadcrumbs.add(category = "diagnostics", message = "Client started")
    }

    /**
     * Stop polling and cleanup
     */
    fun stop() {
        logger.info("[diagnostics] Stopping diagnostics client")

        pollJob?.cancel()
        pollJob = null

        flushJob?.cancel()
        flushJob = null

        networkInterceptor?.stop()
        networkInterceptor = null

        // Final flush
        scope.launch {
            flush()
        }

        _state.value = DiagnosticsClientState.Idle
        breadcrumbs.add(category = "diagnostics", message = "Client stopped")
    }

    /**
     * Check if a debug session is currently active
     */
    fun isSessionActive(): Boolean {
        return _state.value is DiagnosticsClientState.Active
    }

    /**
     * Get current session if active
     */
    fun getCurrentSession(): DiagnosticsSession? {
        return when (val current = _state.value) {
            is DiagnosticsClientState.Active -> current.session
            is DiagnosticsClientState.Polling -> current.session
            else -> null
        }
    }

    /**
     * Record a log entry
     */
    fun log(
        level: DiagnosticsLogLevel,
        message: String,
        module: String = "unknown",
        file: String? = null,
        line: Int? = null,
        function: String? = null,
        context: Map<String, String> = emptyMap(),
        correlationId: String? = null
    ) {
        val entry = DiagnosticsLogEntry(
            level = level,
            message = message,
            timestamp = dateFormat.format(Date()),
            module = module,
            file = file,
            line = line,
            function = function,
            context = context,
            correlationId = correlationId
        )

        synchronized(logBuffer) {
            logBuffer.add(entry)
        }

        breadcrumbs.add(
            category = "log",
            message = "[${level.name}] ${message.take(100)}",
            data = mapOf("level" to level.name)
        )

        // Auto-flush on fatal
        if (level == DiagnosticsLogLevel.FATAL) {
            scope.launch { flush() }
        }
    }

    /**
     * Record a trace span (auto-instrumented)
     */
    suspend fun <T> trace(name: String, operation: suspend () -> T): T {
        val spanId = generateId()
        val startTime = dateFormat.format(Date())

        breadcrumbs.add(
            category = "trace",
            message = "Starting: $name",
            data = mapOf("spanId" to spanId)
        )

        return try {
            val result = operation()
            val endTime = dateFormat.format(Date())
            val durationMs = calculateDuration(startTime, endTime)

            val span = DiagnosticsTraceSpan(
                spanId = spanId,
                name = name,
                startTime = startTime,
                endTime = endTime,
                durationMs = durationMs,
                status = DiagnosticsSpanStatus.OK
            )

            synchronized(traceBuffer) {
                traceBuffer.add(span)
            }

            breadcrumbs.add(
                category = "trace",
                message = "Completed: $name",
                data = mapOf("spanId" to spanId, "durationMs" to durationMs.toString())
            )

            result
        } catch (e: Exception) {
            val endTime = dateFormat.format(Date())
            val durationMs = calculateDuration(startTime, endTime)

            val span = DiagnosticsTraceSpan(
                spanId = spanId,
                name = name,
                startTime = startTime,
                endTime = endTime,
                durationMs = durationMs,
                status = DiagnosticsSpanStatus.ERROR,
                statusMessage = e.message
            )

            synchronized(traceBuffer) {
                traceBuffer.add(span)
            }

            breadcrumbs.add(
                category = "trace",
                message = "Failed: $name",
                data = mapOf("spanId" to spanId, "error" to (e.message ?: "Unknown"))
            )

            throw e
        }
    }

    /**
     * Add a manual breadcrumb
     */
    fun breadcrumb(category: String, message: String, data: Map<String, String>? = null) {
        breadcrumbs.add(category = category, message = message, data = data)
    }

    /**
     * Get all breadcrumbs
     */
    fun getBreadcrumbs(): List<DiagnosticsBreadcrumb> {
        return breadcrumbs.getAll()
    }

    /**
     * Collect and return device state
     */
    fun collectDeviceState(): DiagnosticsDeviceState {
        return DeviceStateCollector.collect(context)
    }

    // Private methods

    private suspend fun pollForSession() {
        try {
            val url = "${config.serverUrl}/api/diagnostics/config" +
                    "?productId=${config.productId}" +
                    "&installId=${config.anonymousInstallId}"

            val requestBuilder = Request.Builder()
                .url(url)
                .header("Accept", "application/json")

            lastEtag?.let { etag ->
                requestBuilder.header("If-None-Match", etag)
            }

            config.getAuthToken?.let { getToken ->
                try {
                    val token = getToken()
                    requestBuilder.header("Authorization", "Bearer $token")
                } catch (e: Exception) {
                    logger.error("[diagnostics] Failed to get auth token", mapOf("error" to (e.message ?: "unknown")))
                }
            }

            val request = requestBuilder.build()

            httpClient.newCall(request).execute().use { response ->
                if (response.code == 304) {
                    // No change
                    return
                }

                if (!response.isSuccessful) {
                    throw IOException("HTTP ${response.code}")
                }

                // Store ETag
                response.header("ETag")?.let { etag ->
                    lastEtag = etag
                }

                val body = response.body?.string()
                val session = body?.let {
                    try {
                        json.decodeFromString<DiagnosticsSession>(it)
                    } catch (e: Exception) {
                        null
                    }
                }

                // Update state
                if (session != null && session.status == DiagnosticsSessionStatus.ACTIVE) {
                    if (_state.value !is DiagnosticsClientState.Active) {
                        logger.info("[diagnostics] Session activated", mapOf("sessionId" to session.id))
                        breadcrumbs.add(
                            category = "diagnostics",
                            message = "Session activated",
                            data = mapOf("sessionId" to session.id)
                        )
                    }
                    _state.value = DiagnosticsClientState.Active(session)
                } else {
                    if (_state.value is DiagnosticsClientState.Active) {
                        logger.info("[diagnostics] Session ended")
                        breadcrumbs.add(category = "diagnostics", message = "Session ended")
                    }
                    _state.value = DiagnosticsClientState.Polling(null)
                }
            }
        } catch (e: Exception) {
            logger.error("[diagnostics] Failed to poll for session", mapOf("error" to (e.message ?: "unknown")))
            _state.value = DiagnosticsClientState.Error(e)
        }
    }

    private suspend fun flush() {
        val session = getCurrentSession()
        if (session == null) {
            // No active session, clear buffers
            synchronized(logBuffer) { logBuffer.clear() }
            synchronized(traceBuffer) { traceBuffer.clear() }
            synchronized(networkBuffer) { networkBuffer.clear() }
            return
        }

        // Build batch
        val batch = DiagnosticsIngestBatch(
            sessionId = session.id,
            traces = synchronized(traceBuffer) {
                if (traceBuffer.isEmpty()) null else traceBuffer.take(50).also {
                    repeat(it.size) { traceBuffer.removeAt(0) }
                }
            },
            logs = synchronized(logBuffer) {
                if (logBuffer.isEmpty()) null else logBuffer.take(50).also {
                    repeat(it.size) { logBuffer.removeAt(0) }
                }
            },
            network = synchronized(networkBuffer) {
                if (networkBuffer.isEmpty()) null else networkBuffer.take(50).also {
                    repeat(it.size) { networkBuffer.removeAt(0) }
                }
            },
            breadcrumbs = breadcrumbs.getAll().takeIf { it.isNotEmpty() }?.also {
                breadcrumbs.clear()
            }
        )

        // Skip if nothing to send
        if (batch.traces == null && batch.logs == null && batch.network == null && batch.breadcrumbs == null) {
            return
        }

        try {
            val url = "${config.serverUrl}/api/diagnostics/ingest"

            val requestBody = json.encodeToString(batch)
                .toRequestBody("application/json".toMediaType())

            val requestBuilder = Request.Builder()
                .url(url)
                .post(requestBody)

            config.getAuthToken?.let { getToken ->
                try {
                    val token = getToken()
                    requestBuilder.header("Authorization", "Bearer $token")
                } catch (e: Exception) {
                    logger.error("[diagnostics] Failed to get auth token for flush", mapOf("error" to (e.message ?: "unknown")))
                }
            }

            val request = requestBuilder.build()

            httpClient.newCall(request).execute().use { response ->
                if (!response.isSuccessful) {
                    throw IOException("HTTP ${response.code}")
                }

                logger.debug(
                    "[diagnostics] Flushed batch",
                    mapOf(
                        "logs" to (batch.logs?.size ?: 0).toString(),
                        "traces" to (batch.traces?.size ?: 0).toString(),
                        "network" to (batch.network?.size ?: 0).toString()
                    )
                )
            }
        } catch (e: Exception) {
            logger.error("[diagnostics] Failed to flush batch", mapOf("error" to (e.message ?: "unknown")))

            // Put items back in buffers for retry
            synchronized(logBuffer) { batch.logs?.let { logBuffer.addAll(0, it) } }
            synchronized(traceBuffer) { batch.traces?.let { traceBuffer.addAll(0, it) } }
            synchronized(networkBuffer) { batch.network?.let { networkBuffer.addAll(0, it) } }
        }
    }

    private fun setupNetworkCapture() {
        networkInterceptor = NetworkInterceptor { request ->
            synchronized(networkBuffer) {
                networkBuffer.add(request)
            }
        }
        networkInterceptor?.start(httpClient)
        breadcrumbs.add(category = "diagnostics", message = "Network capture enabled")
    }

    private fun generateId(): String {
        return "${System.currentTimeMillis()}_${UUID.randomUUID().toString().take(7)}"
    }

    private fun calculateDuration(startTime: String, endTime: String): Double {
        return try {
            val start = dateFormat.parse(startTime)?.time ?: 0
            val end = dateFormat.parse(endTime)?.time ?: 0
            (end - start).toDouble()
        } catch (e: Exception) {
            0.0
        }
    }
}
