package expo.modules.rebreakprotection

import android.accessibilityservice.AccessibilityServiceInfo
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.VpnService
import android.os.Build
import android.provider.Settings
import android.util.Log
import android.view.accessibility.AccessibilityManager
import expo.modules.kotlin.Promise
import expo.modules.kotlin.exception.CodedException
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.rebreakprotection.accessibility.RebreakAccessibilityService
import expo.modules.rebreakprotection.vpn.RebreakVpnService
import java.io.File
import java.net.HttpURLConnection
import java.net.UnknownHostException
import java.net.URL
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import java.util.concurrent.Executors

/**
 * Step-3-Implementation des Android-Pendants zu iOS NEFilter.
 *
 *  activate()                  → VpnService.prepare() → User-Consent → start RebreakVpnService
 *  disable()                   → ACTION_STOP → Service stoppt
 *  getDeviceState()            → vpn / accessibility / tamperLock + Blocklist-Metadaten
 *  syncBlocklist()             → HTTP-Download + ETag-Caching + atomic-replace + Service-Reload
 *  runHealthProbe()            → HEAD bet365.com → blocked/loaded/timeout/offline
 *  openSystemSettings(target)  → Generic Settings-Intent
 *  isAccessibilityEnabled()    → Live-Check via AccessibilityManager + Settings.Secure
 *  openAccessibilitySettings() → Deep-link zu Rebreak-Detail-Page (fallback liste)
 *  armTamperLock()             → setzt SharedPref-Flag, requires VPN+A11y
 *  disarmTamperLock()          → cleart SharedPref-Flag
 *  getProtectionStatus()       → vpn/a11y/blocklistCount/tamperArmed
 */
class RebreakProtectionModule : Module() {

  private var pendingActivatePromise: Promise? = null
  private val ioExecutor = Executors.newCachedThreadPool()

  override fun definition() = ModuleDefinition {
    Name("RebreakProtection")

    Events("onLayerChange")

    OnActivityResult { _, payload ->
      if (payload.requestCode != VPN_CONSENT_REQUEST_CODE) return@OnActivityResult
      val promise = pendingActivatePromise ?: return@OnActivityResult
      pendingActivatePromise = null
      if (payload.resultCode == Activity.RESULT_OK) {
        startVpnService()
        saveEnabled(true)
        promise.resolve(activateSuccessResult())
      } else {
        saveEnabled(false)
        promise.resolve(
          mapOf(
            "allLayersOn" to false,
            "missingLayers" to listOf("vpn", "accessibility", "tamperLock"),
            "errors" to listOf("vpn_permission_denied"),
          )
        )
      }
      sendLayerChange()
    }

    AsyncFunction("activate") { promise: Promise ->
      val activity = appContext.currentActivity
        ?: return@AsyncFunction promise.reject(
          CodedException("no_activity", "Activity nicht verfügbar — App im Hintergrund?", null)
        )

      val consentIntent = VpnService.prepare(activity)
      if (consentIntent == null) {
        startVpnService()
        saveEnabled(true)
        promise.resolve(activateSuccessResult())
        sendLayerChange()
      } else {
        if (pendingActivatePromise != null) {
          promise.reject(CodedException("consent_in_flight", "Bereits laufender VPN-Consent-Dialog", null))
          return@AsyncFunction
        }
        pendingActivatePromise = promise
        activity.startActivityForResult(consentIntent, VPN_CONSENT_REQUEST_CODE)
      }
    }

    AsyncFunction("disable") {
      val ctx = requireContext()
      val intent = Intent(ctx, RebreakVpnService::class.java).apply {
        action = RebreakVpnService.ACTION_STOP
      }
      try {
        ctx.startService(intent)
      } catch (e: Exception) {
        Log.w(TAG, "stopService failed: ${e.message}")
      }
      saveEnabled(false)
      // Tamper-Lock IMMER mit-disarmen — egal welcher Caller `disable()` aufruft.
      // Sonst läuft der Accessibility-Watchdog (Settings-Block + Gambling-Block)
      // nach dem Cooldown weiter, obwohl der User legitim deaktiviert hat.
      disarmTamperLock(ctx)
      sendLayerChange()
      mapOf("allLayersOff" to true)
    }

    AsyncFunction("getDeviceState") {
      val ctx = requireContext()
      buildDeviceState(ctx)
    }

    AsyncFunction("syncBlocklist") { opts: Map<String, Any?>, promise: Promise ->
      val baseURL = opts["baseURL"] as? String
      val authToken = opts["authToken"] as? String
      if (baseURL.isNullOrBlank() || authToken.isNullOrBlank()) {
        promise.reject(CodedException("missing_params", "baseURL und authToken required", null))
        return@AsyncFunction
      }
      val ctx = requireContext()
      ioExecutor.execute {
        try {
          val result = downloadBlocklist(ctx, baseURL, authToken)
          // VpnService-Reload — NUR wenn der Filter tatsächlich an ist.
          // `startService` würde den Service sonst re-createn, obwohl der
          // User den Schutz (ggf. nach Cooldown) deaktiviert hat.
          if (isVpnEffectivelyOn(ctx)) {
            val reload = Intent(ctx, RebreakVpnService::class.java).apply {
              action = RebreakVpnService.ACTION_RELOAD
            }
            try { ctx.startService(reload) } catch (_: Exception) {}
          }
          promise.resolve(result)
          sendLayerChange()
        } catch (e: Exception) {
          promise.reject(CodedException("sync_failed", e.message ?: "unknown", e))
        }
      }
    }

    AsyncFunction("runHealthProbe") { opts: Map<String, Any?>?, promise: Promise ->
      val target = (opts?.get("target") as? String) ?: "https://bet365.com"
      val timeoutMs = (((opts?.get("timeoutSeconds") as? Number) ?: 5).toInt()) * 1000
      ioExecutor.execute {
        val started = System.currentTimeMillis()
        var outcome = "offline"
        var reason = "init"
        try {
          val url = URL(target)
          val conn = (url.openConnection() as HttpURLConnection).apply {
            connectTimeout = timeoutMs
            readTimeout = timeoutMs
            requestMethod = "HEAD"
            instanceFollowRedirects = false
          }
          val code = conn.responseCode
          conn.disconnect()
          outcome = "loaded"
          reason = "http_$code"
        } catch (_: UnknownHostException) {
          outcome = "blocked"
          reason = "dns_unresolved"
        } catch (_: java.net.SocketTimeoutException) {
          outcome = "timeout"
          reason = "timeout"
        } catch (e: Exception) {
          outcome = "offline"
          reason = e::class.java.simpleName
        }
        val elapsed = (System.currentTimeMillis() - started).toInt()
        promise.resolve(
          mapOf(
            "outcome" to outcome,
            "reason" to reason,
            "durationMs" to elapsed,
            "target" to target,
          )
        )
      }
    }

    AsyncFunction("openSystemSettings") { target: String? ->
      val ctx = requireContext()
      val action = when (target) {
        "vpn" -> Settings.ACTION_VPN_SETTINGS
        "accessibility" -> Settings.ACTION_ACCESSIBILITY_SETTINGS
        "notifications" -> Settings.ACTION_APP_NOTIFICATION_SETTINGS
        else -> Settings.ACTION_SETTINGS
      }
      val intent = Intent(action).apply {
        if (target == "notifications") {
          putExtra(Settings.EXTRA_APP_PACKAGE, ctx.packageName)
        }
        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
      }
      ctx.startActivity(intent)
      Unit
    }

    // ─── Layer 2: Accessibility + Tamper ──────────────────────────────────

    AsyncFunction("isAccessibilityEnabled") {
      mapOf("enabled" to isAccessibilityServiceEnabled(requireContext()))
    }

    AsyncFunction("openAccessibilitySettings") { promise: Promise ->
      val ctx = requireContext()
      val component = ComponentName(ctx, RebreakAccessibilityService::class.java)
      val tries = listOf(
        // Direkt zur Rebreak-Detail-Seite (Android 11+, viele OEMs)
        Intent("android.settings.ACCESSIBILITY_DETAILS_SETTINGS").apply {
          putExtra(":settings:fragment_args_key", component.flattenToString())
          val args = android.os.Bundle()
          args.putString(":settings:fragment_args_key", component.flattenToString())
          putExtra(":settings:show_fragment_args", args)
          flags = Intent.FLAG_ACTIVITY_NEW_TASK
        },
        // Fallback: Allgemeine Bedienungshilfen-Liste
        Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS).apply {
          flags = Intent.FLAG_ACTIVITY_NEW_TASK
        },
      )
      for (intent in tries) {
        try {
          ctx.startActivity(intent)
          promise.resolve(mapOf("opened" to true))
          return@AsyncFunction
        } catch (e: Exception) {
          Log.w(TAG, "openAccessibilitySettings try failed: ${e.message}")
        }
      }
      promise.reject(CodedException("open_failed", "no accessibility settings activity available", null))
    }

    AsyncFunction("armTamperLock") { promise: Promise ->
      val ctx = requireContext()
      val vpn = isVpnEffectivelyOn(ctx)
      val a11y = isAccessibilityServiceEnabled(ctx)
      if (!vpn || !a11y) {
        promise.reject(
          CodedException(
            "preconditions_not_met",
            "VPN and Accessibility must both be active before arming",
            null,
          )
        )
        return@AsyncFunction
      }
      val prefs = ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
      prefs.edit().putBoolean(KEY_TAMPER_ARMED, true).apply()
      Log.i(TAG, "tamper-lock ARMED")
      promise.resolve(mapOf("armed" to true))
      sendLayerChange()
    }

    AsyncFunction("disarmTamperLock") {
      val ctx = requireContext()
      disarmTamperLock(ctx)
      sendLayerChange()
      mapOf("armed" to false)
    }

    AsyncFunction("getProtectionStatus") {
      val ctx = requireContext()
      val vpn = isVpnEffectivelyOn(ctx)
      val a11y = isAccessibilityServiceEnabled(ctx)
      val count = currentHashCount(ctx)
      val armed = isTamperLockArmed(ctx)
      Log.i(TAG, "getProtectionStatus: vpn=$vpn, a11y=$a11y, blocklist=$count, armed=$armed")
      mapOf(
        "vpnEnabled" to vpn,
        "accessibilityEnabled" to a11y,
        "blocklistCount" to count,
        "tamperArmed" to armed,
      )
    }

    /**
     * Reconcile: Wenn `filter_enabled == true` (User WILL Schutz) der VpnService
     * aber nicht läuft (z.B. nach App-Reinstall oder Low-Memory-Kill den
     * START_STICKY nicht zeitnah recovert hat) → neu starten. Sonst bleibt der
     * State „an aber tot": UI zeigt grün/„aktiv", real ist nichts geschützt.
     * Wird vom JS bei App-Start / Foreground aufgerufen.
     *
     * Wenn die VPN-Permission entzogen wurde, kann hier nicht ohne UI neu
     * established werden — `isVpnEffectivelyOn` clear't in dem Fall bereits den
     * Flag, also self-resolved das.
     */
    AsyncFunction("reconcileVpn") {
      val ctx = requireContext()
      val shouldBeOn = isEnabledFlag(ctx)
      val running = RebreakVpnService.isRunning
      if (shouldBeOn && !running) {
        val consentNeeded = try { VpnService.prepare(ctx) } catch (_: Exception) { null }
        if (consentNeeded == null) {
          startVpnService()
          Log.i(TAG, "reconcileVpn: VpnService neu gestartet (flag=true, war aber nicht running)")
          sendLayerChange()
          mapOf("restarted" to true)
        } else {
          Log.w(TAG, "reconcileVpn: flag=true aber keine VPN-Permission — re-activate via UI nötig")
          mapOf("restarted" to false, "needsConsent" to true)
        }
      } else {
        mapOf("restarted" to false)
      }
    }
  }

  // ─── Helpers ────────────────────────────────────────────────────────────

  private fun requireContext(): Context =
    appContext.reactContext
      ?: throw CodedException("no_context", "ReactContext nicht verfügbar", null)

  private fun startVpnService() {
    val ctx = requireContext()
    val intent = Intent(ctx, RebreakVpnService::class.java)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      ctx.startForegroundService(intent)
    } else {
      ctx.startService(intent)
    }
  }

  private fun buildDeviceState(ctx: Context): Map<String, Any?> {
    val blocklist = File(ctx.filesDir, BLOCKLIST_FILENAME)
    val count = if (blocklist.exists()) (blocklist.length() / 8).toInt() else 0
    val lastSyncAt = prefs(ctx).getString(PREF_LAST_SYNC, null)
    return mapOf(
      "vpn" to isVpnEffectivelyOn(ctx),
      "accessibility" to isAccessibilityServiceEnabled(ctx),
      // Ein armed-aber-Schutz-aus Tamper-Lock ist effektiv KEIN Lock — sonst
      // zeigt die UI „verriegelt" ohne dass der User je rauskommt (Desync-Fall:
      // `tamper_armed` noch true, aber `filter_enabled` schon false).
      "tamperLock" to (isTamperLockArmed(ctx) && isEnabledFlag(ctx)),
      "blocklistCount" to count,
      "blocklistLastSyncAt" to lastSyncAt,
    )
  }

  private fun activateSuccessResult(): Map<String, Any?> = mapOf(
    "allLayersOn" to false,
    "missingLayers" to listOf("accessibility", "tamperLock"),
    "errors" to emptyList<String>(),
  )

  private fun sendLayerChange() {
    val ctx = appContext.reactContext ?: return
    sendEvent("onLayerChange", buildDeviceState(ctx))
  }

  private fun prefs(ctx: Context) =
    ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE)

  private fun saveEnabled(enabled: Boolean) {
    prefs(requireContext()).edit().putBoolean(KEY_ENABLED, enabled).apply()
  }

  private fun isEnabledFlag(ctx: Context): Boolean =
    prefs(ctx).getBoolean(KEY_ENABLED, false)

  private fun isTamperLockArmed(ctx: Context): Boolean =
    prefs(ctx).getBoolean(KEY_TAMPER_ARMED, false)

  /** Cleart das Tamper-Armed-Flag. Geteilte Logik zwischen der
   *  `disarmTamperLock`-AsyncFunction und `disable()` — robust für alle Caller. */
  private fun disarmTamperLock(ctx: Context) {
    prefs(ctx).edit().putBoolean(KEY_TAMPER_ARMED, false).apply()
    Log.i(TAG, "tamper-lock DISARMED")
  }

  private fun currentHashCount(ctx: Context): Int {
    val file = File(ctx.filesDir, BLOCKLIST_FILENAME)
    return if (file.exists()) (file.length() / 8L).toInt() else 0
  }

  /**
   * Live-Check: ist unser VPN tatsächlich aktiv?
   *  1. VpnService.prepare(ctx) == null → wir haben Permission
   *  2. UND (companion-Live-Flag ODER Prefs-Flag)
   *
   * Wenn der User in System-Settings das VPN ausgeschaltet hat, returnt
   * prepare() ein non-null Intent (= "musst Permission neu holen") UND
   * unser onRevoke clear't bereits die Prefs-Flag. Doppelt abgesichert.
   */
  private fun isVpnEffectivelyOn(ctx: Context): Boolean {
    val live = RebreakVpnService.isRunning
    val intent = try {
      VpnService.prepare(ctx)
    } catch (_: Exception) {
      null
    }
    val flag = isEnabledFlag(ctx)
    Log.d(TAG, "isVpnEffectivelyOn: live=$live, prepareIntent=${intent != null}, flag=$flag")
    if (intent != null) {
      // Permission entzogen → definitely off. Auch den Tamper-Lock mit-disarmen,
      // sonst bleibt der State desynct (tamper armed, Schutz aus) → „verriegelt"-UI.
      if (flag) {
        prefs(ctx).edit()
          .putBoolean(KEY_ENABLED, false)
          .putBoolean(KEY_TAMPER_ARMED, false)
          .apply()
      }
      return false
    }
    return live || flag
  }

  /**
   * Liest Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES — kommagetrennte
   * Liste von ComponentName-Strings. Wir matchen exakt unseren Service.
   *
   * Vorteil ggü. AccessibilityManager.getEnabledAccessibilityServiceList:
   * funktioniert auch wenn der Service gerade neu installiert wurde und
   * der Cache noch nicht aktualisiert ist.
   */
  private fun isAccessibilityServiceEnabled(ctx: Context): Boolean {
    val pkg = ctx.packageName
    val expectedClass = RebreakAccessibilityService::class.java.name

    // Primary + authoritative: AccessibilityManager. Eine erfolgreich
    // gelieferte Liste (auch leer) ist die Wahrheit — der Service läuft nur
    // dann tatsächlich, wenn er hier auftaucht. `Settings.Secure` darf nur
    // einspringen wenn die AM-Abfrage technisch fehlschlägt (null/Exception);
    // andernfalls würde ein Stale-Eintrag in Settings.Secure (System hat den
    // Service deregistriert, ENABLED_ACCESSIBILITY_SERVICES nicht aufgeräumt)
    // einen aktiven Schutz vortäuschen, obwohl der Service nicht gebunden ist.
    try {
      val am = ctx.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
      if (am != null) {
        val list = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK)
        if (list != null) {
          Log.d(TAG, "a11y check via AccessibilityManager: ${list.size} services")
          for (info in list) {
            val id = info.id ?: continue
            if (id.contains(pkg) && id.contains("RebreakAccessibilityService")) {
              Log.d(TAG, "a11y MATCH via AM: $id")
              return true
            }
          }
          return false
        }
      }
    } catch (e: Exception) {
      Log.w(TAG, "AccessibilityManager check failed: ${e.message}")
    }

    // Fallback NUR wenn AM technisch unverfügbar war (null/Exception oben).
    val expected = ComponentName(ctx, RebreakAccessibilityService::class.java)
    val expectedFlat = expected.flattenToString()
    val enabled = try {
      Settings.Secure.getString(
        ctx.contentResolver,
        Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
      )
    } catch (e: Exception) {
      null
    }
    Log.d(TAG, "a11y check via Settings.Secure (fallback): expected=$expectedFlat, settings=$enabled")
    if (!enabled.isNullOrBlank()) {
      if (enabled.contains(expectedFlat)) return true
      if (enabled.contains(expectedClass)) return true
    }
    return false
  }

  private fun downloadBlocklist(
    ctx: Context, baseURL: String, authToken: String
  ): Map<String, Any?> {
    val url = URL("$baseURL/api/url-filter/blocklist.bin")
    val conn = (url.openConnection() as HttpURLConnection).apply {
      setRequestProperty("Authorization", "Bearer $authToken")
      connectTimeout = 10_000
      readTimeout = 30_000
    }
    val p = prefs(ctx)
    p.getString(PREF_ETAG, null)?.let { conn.setRequestProperty("If-None-Match", it) }

    val now = isoTimestamp()
    return try {
      when (val code = conn.responseCode) {
        304 -> {
          val existing = File(ctx.filesDir, BLOCKLIST_FILENAME)
          val count = (if (existing.exists()) existing.length() else 0L) / 8
          mapOf("updated" to false, "count" to count.toInt())
        }
        200 -> {
          val bytes = conn.inputStream.use { it.readBytes() }
          writeBlocklistAtomic(ctx, bytes)
          val edits = p.edit()
          conn.getHeaderField("etag")?.let { edits.putString(PREF_ETAG, it) }
          edits.putString(PREF_LAST_SYNC, now).apply()
          val plan = conn.getHeaderField("x-rebreak-plan")
          mapOf(
            "updated" to true,
            "count" to bytes.size / 8,
            "plan" to plan,
          )
        }
        else -> throw RuntimeException("HTTP $code")
      }
    } finally {
      conn.disconnect()
    }
  }

  private fun writeBlocklistAtomic(ctx: Context, bytes: ByteArray) {
    val finalFile = File(ctx.filesDir, BLOCKLIST_FILENAME)
    val tmpFile = File(ctx.filesDir, "$BLOCKLIST_FILENAME.tmp")
    tmpFile.writeBytes(bytes)
    if (!tmpFile.renameTo(finalFile)) {
      finalFile.delete()
      tmpFile.renameTo(finalFile)
    }
  }

  private fun isoTimestamp(): String {
    val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
    df.timeZone = TimeZone.getTimeZone("UTC")
    return df.format(Date())
  }

  companion object {
    private const val TAG = "RebreakProtection"
    private const val VPN_CONSENT_REQUEST_CODE = 42101
    private const val BLOCKLIST_FILENAME = "blocklist.bin"
    private const val PREFS = "rebreak_filter_prefs"
    private const val KEY_ENABLED = "filter_enabled"
    private const val KEY_TAMPER_ARMED = "tamper_armed"
    private const val PREF_ETAG = "rebreak_blocklist_etag"
    private const val PREF_LAST_SYNC = "rebreak_blocklist_last_sync"
  }
}
