package com.inou.clawdnode.service

import android.util.Log
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.inou.clawdnode.BuildConfig
import com.inou.clawdnode.ClawdNodeApp
import com.inou.clawdnode.protocol.Protocol
import com.inou.clawdnode.protocol.*
import com.inou.clawdnode.security.DeviceIdentity
import kotlinx.coroutines.*
import okhttp3.*
import java.util.UUID
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger

/**
 * WebSocket client for Clawdbot Gateway connection.
 * 
 * Implements Gateway Protocol v3:
 * 1. Connect to /ws endpoint
 * 2. Receive connect.challenge event with nonce
 * 3. Send connect request with role, caps, commands, auth
 * 4. Handle hello-ok response
 * 
 * After handshake, handles message routing for node commands.
 */
class GatewayClient(
    private val onCommand: (NodeCommand) -> Unit,
    private val onConnectionChange: (Boolean) -> Unit
) {
    private val tag = "GatewayClient"
    private val gson = Gson()
    
    private val client = OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(0, TimeUnit.SECONDS) // No timeout for WebSocket
        .writeTimeout(10, TimeUnit.SECONDS)
        .pingInterval(30, TimeUnit.SECONDS)
        .build()
    
    private var webSocket: WebSocket? = null
    private var isConnected = false
    private var isHandshakeComplete = false
    private var shouldReconnect = true
    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    private val requestIdCounter = AtomicInteger(0)
    
    private val auditLog get() = ClawdNodeApp.instance.auditLog
    private val tokenStore get() = ClawdNodeApp.instance.tokenStore
    
    private val deviceIdentity by lazy { DeviceIdentity(ClawdNodeApp.instance) }
    
    // Node capabilities
    private val caps = listOf("notifications", "calls", "voice")
    private val commands = listOf(
        "notification.action",
        "notification.dismiss", 
        "call.answer",
        "call.reject",
        "call.speak",
        "call.hangup"
    )
    
    fun connect() {
        val url = tokenStore.gatewayUrl ?: run {
            Log.w(tag, "No gateway URL configured")
            return
        }
        
        if (tokenStore.gatewayToken == null) {
            Log.w(tag, "No gateway token configured")
            return
        }
        
        shouldReconnect = true
        isHandshakeComplete = false
        
        // Build WebSocket URL - connect to /ws (not /ws/node)
        val wsUrl = buildWsUrl(url)
        Log.d(tag, "Connecting to $wsUrl")
        
        val request = Request.Builder()
            .url(wsUrl)
            .build()
        
        webSocket = client.newWebSocket(request, object : WebSocketListener() {
            override fun onOpen(webSocket: WebSocket, response: Response) {
                Log.i(tag, "WebSocket connected, waiting for challenge...")
                // Don't set isConnected yet - wait for handshake
            }
            
            override fun onMessage(webSocket: WebSocket, text: String) {
                Log.d(tag, "Received: $text")
                handleMessage(text)
            }
            
            override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
                Log.i(tag, "Connection closing: $code $reason")
                webSocket.close(1000, null)
            }
            
            override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
                Log.i(tag, "Connection closed: $code $reason")
                handleDisconnect()
            }
            
            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                Log.e(tag, "Connection failed", t)
                auditLog.log("GATEWAY_ERROR", "Connection failed: ${t.message}")
                handleDisconnect()
            }
        })
    }
    
    fun disconnect() {
        shouldReconnect = false
        webSocket?.close(1000, "Client disconnect")
        webSocket = null
        isConnected = false
        isHandshakeComplete = false
        onConnectionChange(false)
    }
    
    fun send(event: NodeEvent) {
        val requestId = generateRequestId()
        val json = event.toProtocolFrame(requestId)
        Log.d(tag, "Sending: $json")
        
        if (isConnected && isHandshakeComplete) {
            webSocket?.send(json)
        } else {
            Log.w(tag, "Not connected or handshake incomplete, cannot send event")
            // TODO: Queue for retry
        }
    }
    
    /**
     * Send a response to a node.invoke request
     */
    fun sendResponse(requestId: String, success: Boolean, payload: Any? = null, error: String? = null) {
        val frame = if (success) {
            ResponseFrame(id = requestId, ok = true, payload = payload)
        } else {
            ResponseFrame(id = requestId, ok = false, error = ErrorPayload("ERROR", error ?: "Unknown error"))
        }
        
        val json = frame.toJson()
        Log.d(tag, "Sending response: $json")
        
        if (isConnected && isHandshakeComplete) {
            webSocket?.send(json)
        }
    }
    
    private fun buildWsUrl(baseUrl: String): String {
        // Convert http(s) to ws(s)
        val wsBase = baseUrl
            .replace("http://", "ws://")
            .replace("https://", "wss://")
            .trimEnd('/')
        
        // Connect to /ws endpoint (no token in URL - sent in handshake)
        return "$wsBase/ws"
    }
    
    private fun handleMessage(json: String) {
        val frame = ProtocolFrame.fromJson(json)
        if (frame == null) {
            Log.w(tag, "Failed to parse frame: $json")
            return
        }
        
        when (frame.type) {
            "event" -> handleEvent(frame)
            "req" -> handleRequest(frame)
            "res" -> handleResponse(frame)
            else -> Log.w(tag, "Unknown frame type: ${frame.type}")
        }
    }
    
    private fun handleEvent(frame: ProtocolFrame) {
        when (frame.event) {
            "connect.challenge" -> handleConnectChallenge(frame)
            else -> Log.d(tag, "Received event: ${frame.event}")
        }
    }
    
    private fun handleConnectChallenge(frame: ProtocolFrame) {
        Log.i(tag, "Received connect.challenge, sending handshake...")
        
        val payload = frame.payload
        val nonce = payload?.get("nonce")?.asString
        
        if (nonce == null) {
            Log.e(tag, "No nonce in connect.challenge")
            return
        }
        
        val token = tokenStore.gatewayToken ?: run {
            Log.e(tag, "No token available for handshake")
            return
        }
        
        // Sign the challenge nonce
        val signedChallenge = try {
            deviceIdentity.signChallenge(nonce)
        } catch (e: Exception) {
            Log.e(tag, "Failed to sign challenge", e)
            null
        }
        
        val connectRequest = ConnectRequest(
            id = generateRequestId(),
            params = ConnectParams(
                client = ClientInfo(
                    id = Protocol.CLIENT_ID,
                    version = getAppVersion(),
                    platform = Protocol.PLATFORM,
                    mode = Protocol.MODE
                ),
                role = Protocol.ROLE,
                caps = caps,
                commands = commands,
                permissions = mapOf(
                    "notifications.read" to true,
                    "notifications.action" to true,
                    "calls.answer" to true,
                    "calls.reject" to true,
                    "calls.speak" to true
                ),
                auth = AuthInfo(token = token),
                userAgent = "clawdnode-android/${getAppVersion()}",
                device = DeviceInfo(
                    id = deviceIdentity.deviceId,
                    publicKey = signedChallenge?.let { deviceIdentity.publicKey },
                    signature = signedChallenge?.signature,
                    signedAt = signedChallenge?.signedAt,
                    nonce = signedChallenge?.nonce
                )
            )
        )
        
        val requestJson = connectRequest.toJson()
        Log.d(tag, "Sending connect request: $requestJson")
        webSocket?.send(requestJson)
    }
    
    private fun handleRequest(frame: ProtocolFrame) {
        when (frame.method) {
            "node.invoke" -> handleNodeInvoke(frame)
            else -> {
                Log.w(tag, "Unknown request method: ${frame.method}")
                // Send error response
                frame.id?.let { id ->
                    sendResponse(id, false, error = "Unknown method: ${frame.method}")
                }
            }
        }
    }
    
    private fun handleNodeInvoke(frame: ProtocolFrame) {
        val requestId = frame.id ?: run {
            Log.e(tag, "node.invoke missing request id")
            return
        }
        
        val params = frame.params ?: run {
            Log.e(tag, "node.invoke missing params")
            sendResponse(requestId, false, error = "Missing params")
            return
        }
        
        val command = params.get("command")?.asString
        val args = params.get("args")?.asJsonObject
        
        if (command == null) {
            Log.e(tag, "node.invoke missing command")
            sendResponse(requestId, false, error = "Missing command")
            return
        }
        
        Log.i(tag, "Received node.invoke: $command")
        auditLog.logCommand(command, "gateway", true)
        
        val invokeParams = NodeInvokeParams(command = command, args = args)
        val nodeCommand = NodeCommand.fromInvoke(requestId, invokeParams)
        
        if (nodeCommand != null) {
            onCommand(nodeCommand)
        } else {
            Log.w(tag, "Unknown command: $command")
            sendResponse(requestId, false, error = "Unknown command: $command")
        }
    }
    
    private fun handleResponse(frame: ProtocolFrame) {
        val ok = frame.ok ?: false
        val payload = frame.payload
        
        // Check if this is hello-ok response
        val payloadType = payload?.get("type")?.asString
        if (payloadType == "hello-ok") {
            handleHelloOk(frame)
            return
        }
        
        if (!ok) {
            val error = frame.error
            Log.e(tag, "Request failed: $error")
        }
    }
    
    private fun handleHelloOk(frame: ProtocolFrame) {
        Log.i(tag, "Received hello-ok, handshake complete!")
        
        val payload = frame.payload
        val protocol = payload?.get("protocol")?.asInt ?: Protocol.VERSION
        
        // Check for device token
        val auth = payload?.get("auth")?.asJsonObject
        val deviceToken = auth?.get("deviceToken")?.asString
        if (deviceToken != null) {
            Log.i(tag, "Received device token, storing for future connects")
            // Could store this for future use
        }
        
        isConnected = true
        isHandshakeComplete = true
        onConnectionChange(true)
        auditLog.log("GATEWAY_CONNECTED", "Protocol v$protocol handshake complete")
    }
    
    private fun handleDisconnect() {
        isConnected = false
        isHandshakeComplete = false
        onConnectionChange(false)
        
        if (shouldReconnect) {
            auditLog.log("GATEWAY_RECONNECT", "Scheduling reconnect in 5s")
            scope.launch {
                delay(5000)
                if (shouldReconnect) {
                    connect()
                }
            }
        }
    }
    
    private fun generateRequestId(): String {
        return "req_${requestIdCounter.incrementAndGet()}_${UUID.randomUUID().toString().take(8)}"
    }
    
    private fun getAppVersion(): String {
        return try {
            BuildConfig.VERSION_NAME
        } catch (e: Exception) {
            "0.1.0"
        }
    }
    
    fun isConnected() = isConnected && isHandshakeComplete
}
