package il.co.organicai.agent

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.util.Base64
import android.webkit.CookieManager
import android.webkit.JavascriptInterface
import android.view.View
import android.webkit.WebView
import android.webkit.WebViewClient
import kotlinx.coroutines.suspendCancellableCoroutine
import org.json.JSONObject
import java.io.ByteArrayOutputStream
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

/**
 * The controllable browser. A WebView we own, logged in as the user, driven by the
 * SAME BROWSER_* primitives as the desktop extension (ADR-42) — but executed with
 * evaluateJavascript() instead of chrome.scripting.
 *
 * The injected JS is deliberately identical in spirit to background/service-worker.js
 * so behaviour matches across platforms: EXTRACT stamps visible interactive elements
 * with data-oai-ref; CLICK/TYPE address elements by that ref.
 */
@SuppressLint("SetJavaScriptEnabled")
class AgentWebView(private val context: Context) {


    // Async JS bridge: injected page code resolves a call by id from any thread.
    private val pending = ConcurrentHashMap<String, (String) -> Unit>()
    inner class Bridge {
        @JavascriptInterface
        fun done(id: String, json: String) { pending.remove(id)?.invoke(json) }
    }

    val view = WebView(context).apply {
        settings.javaScriptEnabled = true
        settings.domStorageEnabled = true
        settings.databaseEnabled = true
        // Default to the DESKTOP UA: the ported publish selectors target the
        // DESKTOP facebook.com DOM (a mobile UA serves m.facebook.com, different
        // layout). navigate() overrides this PER SITE — see uaFor(). (ADR-51)
        settings.userAgentString = UA_DESKTOP
        settings.useWideViewPort = true
        settings.loadWithOverviewMode = true
        // Persist the user's login across runs — this is THEIR authenticated browser.
        CookieManager.getInstance().setAcceptThirdPartyCookies(this, true)
        webViewClient = WebViewClient()
        addJavascriptInterface(Bridge(), "OAIBridge")
        // The WebView lives in a background service with no window, so it has no
        // measured size — pages wouldn't lay out and draw() would be blank. Give
        // it a real offscreen viewport so navigation renders and screenshots work.
        measure(
            View.MeasureSpec.makeMeasureSpec(VIEW_W, View.MeasureSpec.EXACTLY),
            View.MeasureSpec.makeMeasureSpec(VIEW_H, View.MeasureSpec.EXACTLY),
        )
        layout(0, 0, VIEW_W, VIEW_H)
    }

    /** BROWSER_NAVIGATE — load a url and resolve when the page finishes. */
    suspend fun navigate(url: String): JSONObject = suspendCoroutine { cont ->
        // Per-site User-Agent (ADR-51). facebook.com keeps the desktop UA because
        // the publish selectors target the desktop DOM. Everywhere else gets the
        // device's REAL mobile Chrome UA: LinkedIn (and others) reject a login
        // that claims to be "Linux desktop Chrome" while running in a WebView on
        // a phone — that mismatch is exactly what triggered their
        // "התרחשה שגיאה בלתי צפויה" on every sign-in attempt.
        view.settings.userAgentString = uaFor(url)
        view.webViewClient = object : WebViewClient() {
            override fun onPageFinished(v: WebView, u: String) {
                view.webViewClient = WebViewClient()
                // Persist the login cookies to disk so the session survives restarts.
                CookieManager.getInstance().flush()
                cont.resume(JSONObject().put("ok", true).put("url", u).put("title", v.title ?: ""))
            }
        }
        view.loadUrl(url)
    }

    /** Current page url + title, and a rough logged-in heuristic (for diagnostics). */
    suspend fun status(): JSONObject {
        val js = "JSON.stringify({url:location.href,title:document.title," +
                "loggedIn:!/login|checkpoint|\\/signin/i.test(location.href)})"
        return JSONObject(unwrap(eval(js)))
    }

    private suspend fun eval(js: String): String = suspendCoroutine { cont ->
        view.evaluateJavascript(js) { cont.resume(it ?: "null") }
    }

    /** BROWSER_EXTRACT — page text + interactive elements each stamped with a ref. */
    suspend fun extract(maxEls: Int = 60): JSONObject {
        val js = """(function(){
            var SEL='a[href],button,input,textarea,select,[role="button"],[role="link"],[contenteditable="true"]';
            var vis=[].slice.call(document.querySelectorAll(SEL)).filter(function(e){
                var r=e.getBoundingClientRect();return r.width>0&&r.height>0&&getComputedStyle(e).visibility!=='hidden';
            }).slice(0,$maxEls);
            var els=vis.map(function(e,i){
                e.setAttribute('data-oai-ref',String(i));
                var l=(e.innerText||e.value||e.getAttribute('aria-label')||e.getAttribute('placeholder')||'').trim().replace(/\s+/g,' ').slice(0,80);
                return {ref:i,tag:e.tagName.toLowerCase(),type:e.getAttribute('type'),label:l};
            });
            return JSON.stringify({url:location.href,title:document.title,text:(document.body?document.body.innerText:'').replace(/\s+/g,' ').slice(0,6000),elements:els});
        })();"""
        return JSONObject(unwrap(eval(js)))
    }

    /** BROWSER_CLICK — by ref (from extract) or CSS selector. */
    suspend fun click(ref: Int?, selector: String?): JSONObject {
        val sel = selector ?: "[data-oai-ref=\"$ref\"]"
        val js = """(function(){var el=document.querySelector(${JSONObject.quote(sel)});
            if(!el)return JSON.stringify({clicked:false,error:'not found'});
            el.scrollIntoView({block:'center'});el.click();
            return JSON.stringify({clicked:true});})();"""
        return JSONObject(unwrap(eval(js)))
    }

    /** BROWSER_TYPE — native value setter so React doesn't swallow the input. */
    suspend fun type(ref: Int?, selector: String?, text: String): JSONObject {
        val sel = selector ?: "[data-oai-ref=\"$ref\"]"
        val js = """(function(){var el=document.querySelector(${JSONObject.quote(sel)});
            if(!el)return JSON.stringify({typed:false,error:'not found'});
            el.focus();
            if(el.isContentEditable){el.textContent=${JSONObject.quote(text)};}
            else{var p=el instanceof HTMLTextAreaElement?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;
                 var s=Object.getOwnPropertyDescriptor(p,'value').set;s.call(el,${JSONObject.quote(text)});}
            el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));
            return JSON.stringify({typed:true});})();"""
        return JSONObject(unwrap(eval(js)))
    }

    /**
     * BROWSER_SCREENSHOT — trivial on Android: draw the WebView to a Bitmap. No
     * chrome.debugger, no background-tab problem (the desktop agent's hard part).
     * Returns a jpeg data URL.
     */
    fun screenshot(quality: Int = 50): String {
        val w = if (view.width > 0) view.width else VIEW_W
        val h = if (view.height > 0) view.height else VIEW_H
        val bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
        view.draw(Canvas(bmp))
        val out = ByteArrayOutputStream()
        bmp.compress(Bitmap.CompressFormat.JPEG, quality, out)
        return "data:image/jpeg;base64," + Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP)
    }

    private companion object {
        const val VIEW_W = 1080; const val VIEW_H = 1920

        /** facebook.com only — its publish selectors target the desktop DOM. */
        const val UA_DESKTOP =
            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"

        /** Everywhere else — the device's REAL mobile Chrome identity. Not const:
         *  Build.MODEL is only known at runtime. */
        val UA_MOBILE =
            "Mozilla/5.0 (Linux; Android ${android.os.Build.VERSION.RELEASE}; ${android.os.Build.MODEL}) " +
                "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36"

        /** Desktop UA for facebook.com; the honest mobile UA for every other host. */
        fun uaFor(url: String): String {
            val host = try { java.net.URI(url).host?.lowercase() ?: "" } catch (e: Exception) { "" }
            val isFacebook = host == "facebook.com" || host.endsWith(".facebook.com")
            return if (isFacebook) UA_DESKTOP else UA_MOBILE
        }
    }

    /**
     * Run an async JS expression (one that resolves a value/Promise) and await the
     * result via the OAIBridge. evaluateJavascript alone can't await a Promise —
     * this wraps the expression so the page calls back when it settles.
     */
    suspend fun runAsync(expr: String): JSONObject = suspendCancellableCoroutine { cont ->
        val id = UUID.randomUUID().toString()
        pending[id] = { json -> if (cont.isActive) cont.resume(runCatching { JSONObject(json) }.getOrElse { JSONObject().put("ok", false).put("error", "bad json") }) }
        val js = "Promise.resolve(($expr)).then(function(r){OAIBridge.done('$id',JSON.stringify(r||{}));})" +
                ".catch(function(e){OAIBridge.done('$id',JSON.stringify({ok:false,error:String((e&&e.message)||e)}));});"
        view.post { view.evaluateJavascript(js, null) }
        cont.invokeOnCancellation { pending.remove(id) }
    }

    /** Read an app asset (e.g. injected automation JS) as text. */
    private fun readAsset(path: String): String =
        context.assets.open(path).bufferedReader().use { it.readText() }

    /**
     * Publish a text post to the currently-loaded Facebook group/page. Injects the
     * ported publish flow (assets/inject/fb_publish.js) then runs it. The page must
     * already be navigated to the target (BROWSER_NAVIGATE) and logged in.
     */
    suspend fun publishFacebook(text: String): JSONObject {
        view.post { view.evaluateJavascript(readAsset("inject/fb_publish.js"), null) }
        kotlinx.coroutines.delay(150) // let the IIFE define the entry point
        return runAsync("window.__oaiPublishFacebook(${JSONObject.quote(text)})")
    }

    /** Publish a text post to the LinkedIn or X (Twitter) feed. platform ∈ linkedin|twitter. */
    suspend fun publishFeed(platform: String, text: String): JSONObject {
        view.post { view.evaluateJavascript(readAsset("inject/feed_publish.js"), null) }
        kotlinx.coroutines.delay(150)
        val fn = if (platform == "twitter" || platform == "x") "__oaiPublishTwitter" else "__oaiPublishLinkedIn"
        return runAsync("window.$fn(${JSONObject.quote(text)})")
    }

    // evaluateJavascript returns a JSON-encoded string ("\"{...}\""); unwrap one level.
    private fun unwrap(raw: String): String =
        if (raw.startsWith("\"")) JSONObject("{\"v\":$raw}").getString("v") else raw
}
