package net.laenre.oasis

import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.webkit.PermissionRequest
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.solarnethub.oasis.BuildConfig

class MainActivity : AppCompatActivity() {

  private lateinit var web: WebView
  private var pending: PermissionRequest? = null
  private var fileCallback: ValueCallback<Array<Uri>>? = null

  private val pickFiles = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { res ->
    // Si se cancela hay que devolver null igualmente: si no, el <input> queda
    // bloqueado y no vuelve a abrir el selector nunca mas.
    fileCallback?.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(res.resultCode, res.data))
    fileCallback = null
  }

  private val askAndroid = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { granted ->
    val req = pending
    pending = null
    if (req == null) return@registerForActivityResult
    // Solo se concede al contenido web lo que el sistema nos ha concedido a nosotros.
    val allowed = req.resources.filter { res ->
      when (res) {
        PermissionRequest.RESOURCE_VIDEO_CAPTURE -> granted[Manifest.permission.CAMERA] == true
        PermissionRequest.RESOURCE_AUDIO_CAPTURE -> granted[Manifest.permission.RECORD_AUDIO] == true
        else -> false
      }
    }.toTypedArray()
    if (allowed.isEmpty()) req.deny() else req.grant(allowed)
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    if (Build.VERSION.SDK_INT >= 33 && !has("android.permission.POST_NOTIFICATIONS")) {
      askAndroid.launch(arrayOf("android.permission.POST_NOTIFICATIONS"))
    }
    startService(Intent(this, NodeService::class.java))

    web = WebView(this)
    setContentView(web)
    WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)

    web.settings.apply {
      javaScriptEnabled = true
      domStorageEnabled = true
      mediaPlaybackRequiresUserGesture = false   // permite reproducir el audio remoto de la llamada
      allowFileAccess = false
      allowContentAccess = false
    }

    // La navegacion se queda dentro del backend local; lo de fuera va al navegador.
    web.webViewClient = object : WebViewClient() {
      override fun shouldOverrideUrlLoading(v: WebView?, r: WebResourceRequest?): Boolean {
        val u = r?.url ?: return false
        if (isLocal(u)) return false
        startActivity(Intent(Intent.ACTION_VIEW, u))
        return true
      }

      /**
       * Red de seguridad: si una pagina del backend no carga, se vuelve a esperar
       * y se reintenta, en vez de dejar el ERR_CONNECTION_REFUSED de Chromium en
       * pantalla. Pasa cuando el backend todavia no escucha (arranque lento), y
       * tambien cuando se ha reiniciado —al cambiar de identidad, o porque Android
       * mato el proceso del servicio estando en segundo plano—.
       */
      override fun onPageFinished(v: WebView?, url: String?) {
        // se recuerda para poder volver aqui si luego hay que reintentar
        if (url != null && url.startsWith("http://127.0.0.1:3000")) lastGoodUrl = url
      }

      override fun onReceivedError(v: WebView?, req: WebResourceRequest?, err: WebResourceError?) {
        if (req?.isForMainFrame != true) return          // un css o una imagen no cuentan
        val u = req.url ?: return
        if (!isLocal(u)) return                          // paginas de fuera, ni tocarlas
        Log.w("OasisNode", "la pagina no ha cargado (${'$'}u); reintentando")
        loaded = false
        waitAndLoad()
      }
    }

    // Sin este WebChromeClient.onPermissionRequest, el WebView DENIEGA por defecto
    // toda peticion de getUserMedia y no hay videollamada posible, aunque los
    // permisos esten declarados en el manifest. Es la puerta que faltaba.
    web.webChromeClient = object : WebChromeClient() {
      override fun onShowFileChooser(
        view: WebView?,
        callback: ValueCallback<Array<Uri>>?,
        params: FileChooserParams?
      ): Boolean {
        fileCallback?.onReceiveValue(null)   // descartar uno anterior sin resolver
        fileCallback = callback
        return try {
          pickFiles.launch(params?.createIntent())
          true
        } catch (e: Exception) {
          fileCallback = null
          false
        }
      }

      override fun onPermissionRequest(request: PermissionRequest) {
        runOnUiThread {
          if (!isLocal(request.origin)) { request.deny(); return@runOnUiThread }
          val needed = mutableListOf<String>()
          for (res in request.resources) {
            when (res) {
              PermissionRequest.RESOURCE_VIDEO_CAPTURE ->
                if (!has(Manifest.permission.CAMERA)) needed += Manifest.permission.CAMERA
              PermissionRequest.RESOURCE_AUDIO_CAPTURE ->
                if (!has(Manifest.permission.RECORD_AUDIO)) needed += Manifest.permission.RECORD_AUDIO
            }
          }
          if (needed.isEmpty()) {
            request.grant(request.resources)
          } else {
            // Aqui es donde el usuario ve el dialogo del sistema: al pulsar Llamar,
            // no al instalar la app.
            pending = request
            askAndroid.launch(needed.toTypedArray())
          }
        }
      }
    }

    waitAndLoad()
  }

  private fun has(p: String) =
    ContextCompat.checkSelfPermission(this, p) == PackageManager.PERMISSION_GRANTED

  private fun isLocal(u: Uri?): Boolean {
    val h = u?.host ?: return false
    return (h == "127.0.0.1" || h == "localhost") && u.port == 3000
  }

  private fun showStatus(attempt: Int) {
    val s = attempt / 2
    web.loadDataWithBaseURL(null, """
      <html><head><meta name='viewport' content='width=device-width,initial-scale=1'>
      <style>body{background:#121212;color:#FFB400;font-family:sans-serif;display:flex;
      height:100vh;margin:0;align-items:center;justify-content:center;text-align:center}
      div{padding:24px}p{opacity:.7;font-size:14px;line-height:1.5}</style></head>
      <body><div><h2>Oasis</h2>
      <p>Preparando la aplicaci&oacute;n&hellip;<br>El primer arranque descomprime el
      servidor y puede tardar un minuto. No hace falta hacer nada.</p><p>${s}s</p></div></body></html>
    """.trimIndent(), "text/html", "utf-8", null)
  }

  /**
   * Carga la interfaz reintentando hasta que el backend responda.
   *
   * Se hace con el propio WebView en vez de sondear con HttpURLConnection: es el
   * canal que de verdad va a servir la app, y evita depender de que la peticion de
   * comprobacion pase los mismos filtros (el backend, por ejemplo, responde 400 a HEAD).
   */
  private var loadAttempt = 0
  private var loaded = false
  @Volatile private var waiting = false        // hay un hilo esperando al backend
  private var lastGoodUrl: String? = null       // ultima pagina que cargo bien

  /**
   * Espera a que el backend acepte conexiones y entonces carga la interfaz.
   *
   * La comprobacion es un socket TCP, no una peticion HTTP: no depende del metodo
   * (el backend responde 400 a HEAD), ni de redirecciones (/ redirige a /activity),
   * ni de la politica de trafico en claro. Si el puerto acepta, esta listo.
   *
   * La pantalla de estado se pinta ANTES de empezar a esperar, nunca entre medias:
   * cargarla mientras el WebView trae la pagina buena cancelaria esa carga.
   */
  /** Ruta a abrir: la del intent si viene con una, o la raiz. */
  private fun targetUrl(): String {
    val d = intent?.data
    if (d != null && isLocal(d)) return d.toString()
    val path = intent?.getStringExtra("path")
    if (!path.isNullOrBlank() && path.startsWith("/")) return "http://127.0.0.1:3000${'$'}path"
    return "http://127.0.0.1:3000/"
  }

  override fun onNewIntent(newIntent: Intent) {
    super.onNewIntent(newIntent)
    intent = newIntent
    if (loaded) web.loadUrl(targetUrl())
  }

  private fun waitAndLoad() {
    // onReceivedError puede dispararse varias veces seguidas (la pagina y sus
    // recursos); sin esto se acumularian hilos esperando al mismo puerto
    if (waiting) return
    waiting = true
    showStatus(0)
    Log.i("OasisNode", "esperando al backend en 127.0.0.1:3000")
    Thread {
      var i = 0
      while (i < 600 && !loaded) {           // hasta 5 minutos
        val ok = try {
          java.net.Socket().use { it.connect(java.net.InetSocketAddress("127.0.0.1", 3000), 1000); true }
        } catch (e: Exception) {
          if (i % 10 == 0) Log.w("OasisNode", "socket #" + i + ": " + e.javaClass.simpleName + ": " + e.message)
          false
        }
        if (ok) {
          Handler(Looper.getMainLooper()).post {
            loaded = true
            waiting = false
            Log.i("OasisNode", "backend listo tras " + (i / 2) + "s, cargando interfaz")
            web.loadUrl(lastGoodUrl ?: targetUrl())
          }
          return@Thread
        }
        if (i % 8 == 0) Handler(Looper.getMainLooper()).post { if (!loaded) showStatus(i) }
        Thread.sleep(500); i++
      }
      waiting = false
    }.start()
  }

  @Deprecated("Deprecated in Java")
  override fun onBackPressed() {
    if (web.canGoBack()) web.goBack() else super.onBackPressed()
  }
}
