/*
 * Hermes Node Android
 * Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package net.nexlab.hermesnodeandroid

import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import net.nexlab.hermesnodeandroid.capabilities.AndroidAudioControl
import net.nexlab.hermesnodeandroid.capabilities.AndroidBrowserControl
import net.nexlab.hermesnodeandroid.capabilities.AndroidCameraControl
import net.nexlab.hermesnodeandroid.capabilities.AndroidComputerControl
import net.nexlab.hermesnodeandroid.capabilities.AndroidDesktopObserve
import net.nexlab.hermesnodeandroid.capabilities.AndroidExecControl
import net.nexlab.hermesnodeandroid.capabilities.AndroidNotificationAccess
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import org.json.JSONObject
import kotlin.math.min

class NodeAgent(
    private val context: Context,
    private val config: NodeConfig,
) : WebSocketListener() {
    private val computer = AndroidComputerControl(context)
    private val observe = AndroidDesktopObserve(context)
    private val audio = AndroidAudioControl(context)
    private val camera = AndroidCameraControl(context)
    private val browser = AndroidBrowserControl(context)
    private val notifications = AndroidNotificationAccess(context)
    private val exec = AndroidExecControl(context)
    private val mainHandler = Handler(Looper.getMainLooper())
    private var gatewayClient: GatewayClient? = null
    private var stopped = false
    private var reconnectAttempts = 0
    private var heartbeatScheduled = false

    fun start() {
        stopped = false
        StatusStore.set(context, "connecting", "${config.nodeName} -> ${config.gatewayUrl}")
        connectNow()
    }

    fun stop() {
        stopped = true
        heartbeatScheduled = false
        gatewayClient?.close()
        gatewayClient = null
        mainHandler.removeCallbacksAndMessages(null)
        StatusStore.set(context, "stopped")
    }

    private fun connectNow() {
        if (stopped) return
        gatewayClient?.close()
        gatewayClient = GatewayClient(config)
        Log.i(TAG, "connect ${config.nodeName} -> ${config.gatewayUrl}")
        gatewayClient?.connect(this)
    }

    override fun onOpen(webSocket: WebSocket, response: Response) {
        reconnectAttempts = 0
        StatusStore.set(context, "connected", "registered as ${config.nodeName}")
        Log.i(TAG, "connected; sending registration")
        webSocket.send(registrationFrame())
        scheduleHeartbeat(webSocket)
    }

    override fun onMessage(webSocket: WebSocket, text: String) {
        val command = runCatching { GatewayCommand.fromJson(text) }.getOrElse {
            Log.w(TAG, "invalid command: ${it.message}")
            return
        }
        val action = command.action ?: command.command
        if (command.type == "heartbeat_ack" || command.type == "register_ack") return
        Log.i(TAG, "tool call type=${command.type} action=$action id=${command.id}")
        val result = when (command.type) {
            "computer_control" -> computer.handle(action, command.params)
            "desktop_observe" -> observe.handle(action, command.params)
            "audio_control" -> audio.handle(action, command.params)
            "camera_control" -> camera.handle(action, command.params)
            "browser_control" -> browser.handle(action, command.params)
            "notification_access" -> notifications.handle(action, command.params)
            "exec" -> exec.handle(command)
            else -> CapabilityResult.unsupported("Unknown command type: ${command.type}")
        }
        val resultType = when (command.type) {
            "computer_control" -> "computer_control_result"
            "desktop_observe" -> "desktop_observe_result"
            "audio_control" -> "audio_control_result"
            "camera_control" -> "camera_control_result"
            "browser_control" -> "browser_control_result"
            "notification_access" -> "notification_access_result"
            "exec" -> "exec_complete"
            else -> "error"
        }
        webSocket.send(result.toWireResult(resultType, command.id, action))
    }

    override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
        if (stopped) return
        Log.w(TAG, "connection failure: ${t.message}")
        StatusStore.set(context, "disconnected", t.message ?: "connection failure")
        scheduleReconnect()
    }

    override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
        if (stopped) return
        Log.i(TAG, "connection closed: $code $reason")
        StatusStore.set(context, "disconnected", "closed: $code $reason")
        scheduleReconnect()
    }

    private fun scheduleReconnect() {
        if (stopped) return
        reconnectAttempts += 1
        val delaySeconds = min(config.reconnectIntervalSeconds * reconnectAttempts, 60L)
        StatusStore.set(context, "reconnecting", "attempt $reconnectAttempts in ${delaySeconds}s")
        mainHandler.postDelayed({ connectNow() }, delaySeconds * 1000L)
    }

    private fun scheduleHeartbeat(webSocket: WebSocket) {
        if (heartbeatScheduled || stopped) return
        heartbeatScheduled = true
        fun tick() {
            if (stopped) return
            val heartbeat = JSONObject()
                .put("type", "heartbeat")
                .put("node_name", config.nodeName)
                .put("timestamp", System.currentTimeMillis() / 1000L)
                .toString()
            webSocket.send(heartbeat)
            mainHandler.postDelayed({ tick() }, config.heartbeatIntervalSeconds * 1000L)
        }
        mainHandler.postDelayed({ tick() }, config.heartbeatIntervalSeconds * 1000L)
    }

    fun registrationFrame(): String {
        val tools = mutableListOf<String>()
        if (config.enableExec) tools += "exec"
        if (config.enableComputerControl) tools += "computer_control"
        if (config.enableDesktopObserve) tools += "desktop_observe"
        if (config.enableAudioControl) tools += "audio_control"
        if (config.enableCameraControl) tools += "camera_control"
        if (config.enableBrowserControl) tools += "browser_control"
        if (config.enableNotificationAccess) tools += "notification_access"

        return JSONObject()
            .put("type", "register")
            .put("node_name", config.nodeName)
            .put("version", "0.1.0-android")
            .put("tools", jsonArrayOf(tools))
            .put("capabilities", jsonArrayOf(tools))
            .put("capability_info", capabilityInfo())
            .toString()
    }

    private fun capabilityInfo(): JSONObject = JSONObject()
        .put("platform", "android")
        .put("platform_semantics", "android_phone_control")
        .put("enable_exec", config.enableExec)
        .put("enable_computer_control", config.enableComputerControl)
        .put("enable_desktop_observe", config.enableDesktopObserve)
        .put("enable_audio_control", config.enableAudioControl)
        .put("enable_camera_control", config.enableCameraControl)
        .put("enable_browser", config.enableBrowserControl)
        .put("enable_notification_access", config.enableNotificationAccess)
        .put("computer_control", computer.capabilityInfo())
        .put("desktop_observe", observe.capabilityInfo())
        .put("audio_control", audio.capabilityInfo())
        .put("camera_control", camera.capabilityInfo())
        .put("browser_control", browser.capabilityInfo())
        .put("notification_access", notifications.capabilityInfo())
        .put("exec", exec.capabilityInfo())

    companion object {
        private const val TAG = "HermesNodeAgent"
    }
}
