package net.laenre.oasis

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.system.Os
import android.util.Log
import com.solarnethub.oasis.OasisActivity
import com.solarnethub.oasis.R
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipInputStream

/**
 * Servicio en primer plano que sostiene el backend Node.
 *
 * Vive en el proceso :node por dos razones: nodejs-mobile solo permite arrancar el
 * runtime una vez por proceso —cambiar de identidad exige matar el proceso, no vale
 * reiniciar dentro—, y asi el backend sobrevive cuando la app pasa a segundo plano,
 * que es imprescindible para recibir llamadas.
 */
class NodeService : Service() {

  companion object {
    private const val TAG = "OasisNode"
    private const val CHANNEL = "oasis-node"
    private const val NOTIF_ID = 1
    @Volatile private var started = false
  }

  override fun onBind(intent: Intent?): IBinder? = null

  override fun onCreate() {
    super.onCreate()
    startForeground(NOTIF_ID, buildNotification())
    if (started) return
    started = true
    Thread({ runNode() }, "node-main").start()
  }

  private fun buildNotification(): Notification {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      val ch = NotificationChannel(CHANNEL, getString(R.string.node_channel), NotificationManager.IMPORTANCE_LOW)
      ch.setShowBadge(false)
      (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(ch)
    }
    val b = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) Notification.Builder(this, CHANNEL)
            else @Suppress("DEPRECATION") Notification.Builder(this)
    return b.setContentTitle(getString(R.string.app_name))
            .setContentText(getString(R.string.node_running))
            .setSmallIcon(android.R.drawable.stat_notify_sync)
            .setOngoing(true)
            .build()
  }

  private fun runNode() {
    try {
      val root = File(filesDir, "nodejs-project")
      extractProjectIfNeeded(root)
      val main = File(root, "main.js")
      if (!main.exists()) { Log.e(TAG, "no encuentro main.js en ${root.absolutePath}"); return }

      // main.js hace process.env.HOME || resolve(__dirname,'..','..'). En Android HOME
      // suele venir como "/" —no escribible—, asi que se fija explicitamente al
      // almacenamiento privado de la app: ahi cuelga ~/.ssb.
      val home = filesDir.parentFile ?: filesDir
      val tmp = File(home, "tmp").apply { mkdirs() }
      Os.setenv("HOME", home.absolutePath, true)
      Os.setenv("TMPDIR", tmp.absolutePath, true)
      Log.i(TAG, "HOME=${home.absolutePath}")

      val rc = OasisActivity.startNodeWithArguments(arrayOf("node", main.absolutePath))
      Log.w(TAG, "el runtime de node ha terminado con codigo $rc")
    } catch (e: Throwable) {
      Log.e(TAG, "fallo arrancando node", e)
    }
  }

  /** Descomprime assets/nodejs-project.zip la primera vez y tras cada actualizacion. */
  private fun extractProjectIfNeeded(root: File) {
    val stamp = File(root, ".version")
    // Marca por tamano y fecha del asset, no por versionName: al reconstruir la APK
    // sin subir la version, con versionName la app seguiria con el backend anterior.
    // lastUpdateTime cambia en cada instalacion, tambien reinstalando la misma version.
    // (openFd no vale: el asset va comprimido dentro del APK y no admite descriptor.)
    val current = packageManager.getPackageInfo(packageName, 0).lastUpdateTime.toString()
    if (stamp.exists() && stamp.readText() == current) return
    if (root.exists()) root.deleteRecursively()
    root.mkdirs()
    assets.open("nodejs-project.zip").use { input ->
      ZipInputStream(input).use { zip ->
        var entry = zip.nextEntry
        while (entry != null) {
          val out = File(root, entry.name.removePrefix("nodejs-project/"))
          // no confiar en las rutas del zip: evita escribir fuera del directorio (zip slip)
          if (!out.canonicalPath.startsWith(root.canonicalPath + File.separator) &&
              out.canonicalPath != root.canonicalPath) {
            Log.w(TAG, "entrada de zip descartada por ruta sospechosa: ${entry.name}")
          } else if (entry.isDirectory) {
            out.mkdirs()
          } else {
            out.parentFile?.mkdirs()
            FileOutputStream(out).use { fos -> zip.copyTo(fos) }
          }
          zip.closeEntry()
          entry = zip.nextEntry
        }
      }
    }
    stamp.writeText(current)
  }
}
