package org.timesafari.dailynotification

import android.content.Context
import android.os.SystemClock
import android.util.Log
import androidx.work.*
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import kotlin.text.Charsets
import org.json.JSONObject

/**
 * WorkManager implementation for content fetching
 * Implements exponential backoff and network constraints
 *
 * @author Matthew Raymer
 * @version 2.0.0
 */
class FetchWorker(
    appContext: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {

    companion object {
        private const val TAG = "DNP-FETCH"
        private const val WORK_NAME = "fetch_content"
        /** Unique work name for dual (New Activity) schedule fetch; cancel via cancelDualSchedule only. */
        const val WORK_NAME_DUAL = "fetch_dual"

        private const val KEY_IS_DUAL = "is_dual"
        private const val KEY_CACHE_SCOPE = "cache_scope"
        private const val KEY_NEXT_NOTIFY_AT = "next_notify_at"

        /**
         * Persisted for dual native fetch when the host returns no rows.
         * [DualScheduleHelper] must not display a notification; [doWork] skips chained notify.
         */
        internal val dualEmptyNativeFetchPayload: ByteArray =
            """{"skipNotification":true}""".toByteArray(Charsets.UTF_8)

        /** True when [payload] is the dual-cache sentinel for “API / native had no content”. */
        @JvmStatic
        fun isDualSkipNotificationPayload(payload: ByteArray): Boolean {
            return try {
                JSONObject(String(payload, Charsets.UTF_8)).optBoolean("skipNotification", false)
            } catch (_: Exception) {
                false
            }
        }

        fun scheduleFetch(context: Context, config: ContentFetchConfig) {
            enqueueFetch(context, config, WORK_NAME)
        }

        /**
         * Dual (New Activity) prefetch: delayed to [nextFetchAtMillis], scoped cache, optional native fetcher.
         */
        fun enqueueDualFetch(
            context: Context,
            contentFetchConfig: ContentFetchConfig,
            nextFetchAtMillis: Long,
            nextNotifyAtMillis: Long
        ) {
            val now = System.currentTimeMillis()
            val delayMs = (nextFetchAtMillis - now).coerceAtLeast(0L)
            val requiresNetwork = !contentFetchConfig.url.isNullOrBlank() ||
                DailyNotificationPlugin.getNativeFetcherStatic() != null
            val constraints = Constraints.Builder()
                .setRequiredNetworkType(
                    if (requiresNetwork) NetworkType.CONNECTED else NetworkType.NOT_REQUIRED
                )
                .build()
            val workRequest = OneTimeWorkRequestBuilder<FetchWorker>()
                .setConstraints(constraints)
                .setInitialDelay(delayMs, TimeUnit.MILLISECONDS)
                .setBackoffCriteria(
                    BackoffPolicy.EXPONENTIAL,
                    30,
                    TimeUnit.SECONDS
                )
                .setInputData(
                    Data.Builder()
                        .putString("url", contentFetchConfig.url)
                        .putInt("timeout", contentFetchConfig.timeout ?: 30000)
                        .putInt("retryAttempts", contentFetchConfig.retryAttempts ?: 3)
                        .putInt("retryDelay", contentFetchConfig.retryDelay ?: 1000)
                        .putLong("notificationTime", 0L)
                        .putBoolean(KEY_IS_DUAL, true)
                        .putString(KEY_CACHE_SCOPE, ContentCacheScope.DUAL)
                        .putLong(KEY_NEXT_NOTIFY_AT, nextNotifyAtMillis)
                        .build()
                )
                .build()
            WorkManager.getInstance(context)
                .enqueueUniqueWork(WORK_NAME_DUAL, ExistingWorkPolicy.REPLACE, workRequest)
            Log.i(
                TAG,
                "Dual fetch enqueued: delayMs=$delayMs, nextFetchAt=$nextFetchAtMillis, nextNotifyAt=$nextNotifyAtMillis"
            )
        }

        private fun enqueueFetch(context: Context, config: ContentFetchConfig, workName: String) {
            val constraints = Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build()
            val workRequest = OneTimeWorkRequestBuilder<FetchWorker>()
                .setConstraints(constraints)
                .setBackoffCriteria(
                    BackoffPolicy.EXPONENTIAL,
                    30,
                    TimeUnit.SECONDS
                )
                .setInputData(
                    Data.Builder()
                        .putString("url", config.url)
                        .putInt("timeout", config.timeout ?: 30000)
                        .putInt("retryAttempts", config.retryAttempts ?: 3)
                        .putInt("retryDelay", config.retryDelay ?: 1000)
                        .putBoolean(KEY_IS_DUAL, false)
                        .putString(KEY_CACHE_SCOPE, ContentCacheScope.DAILY)
                        .build()
                )
                .build()
            WorkManager.getInstance(context)
                .enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, workRequest)
        }

        /**
         * Schedule a delayed fetch for prefetch (5 minutes before notification)
         *
         * @param context Application context
         * @param fetchTime When to fetch (in milliseconds since epoch)
         * @param notificationTime When the notification will be shown (in milliseconds since epoch)
         * @param url Optional URL to fetch from (if null, generates mock content)
         */
        fun scheduleDelayedFetch(
            context: Context,
            fetchTime: Long,
            notificationTime: Long,
            url: String? = null
        ) {
            val currentTime = System.currentTimeMillis()
            val delayMs = fetchTime - currentTime

            Log.i(TAG, "Scheduling delayed prefetch: fetchTime=$fetchTime, notificationTime=$notificationTime, delayMs=$delayMs")

            if (delayMs <= 0) {
                Log.w(TAG, "Fetch time is in the past, scheduling immediate fetch")
                scheduleImmediateFetch(context, notificationTime, url)
                return
            }

            // Only require network if URL is provided (mock content doesn't need network)
            val constraints = Constraints.Builder()
                .apply {
                    if (url != null) {
                        setRequiredNetworkType(NetworkType.CONNECTED)
                    } else {
                        // No network required for mock content generation
                        setRequiredNetworkType(NetworkType.NOT_REQUIRED)
                    }
                }
                .build()

            // Create unique work name based on notification time to prevent duplicate fetches
            val notificationTimeMinutes = notificationTime / (60 * 1000)
            val workName = "prefetch_${notificationTimeMinutes}"

            val workRequest = OneTimeWorkRequestBuilder<FetchWorker>()
                .setConstraints(constraints)
                .setInitialDelay(delayMs, TimeUnit.MILLISECONDS)
                .setBackoffCriteria(
                    BackoffPolicy.EXPONENTIAL,
                    30,
                    TimeUnit.SECONDS
                )
                .setInputData(
                    Data.Builder()
                        .putString("url", url)
                        .putLong("fetchTime", fetchTime)
                        .putLong("notificationTime", notificationTime)
                        .putInt("timeout", 30000)
                        .putInt("retryAttempts", 3)
                        .putInt("retryDelay", 1000)
                        .putBoolean(KEY_IS_DUAL, false)
                        .putString(KEY_CACHE_SCOPE, ContentCacheScope.DAILY)
                        .build()
                )
                .addTag("prefetch")
                .build()

            WorkManager.getInstance(context)
                .enqueueUniqueWork(
                    workName,
                    ExistingWorkPolicy.REPLACE,
                    workRequest
                )

            Log.i(TAG, "Delayed prefetch scheduled: workName=$workName, delayMs=$delayMs")
        }

        /**
         * Schedule an immediate fetch (fallback when delay is in the past)
         */
        private fun scheduleImmediateFetch(
            context: Context,
            notificationTime: Long,
            url: String? = null
        ) {
            // Only require network if URL is provided (mock content doesn't need network)
            val constraints = Constraints.Builder()
                .apply {
                    if (url != null) {
                        setRequiredNetworkType(NetworkType.CONNECTED)
                    } else {
                        // No network required for mock content generation
                        setRequiredNetworkType(NetworkType.NOT_REQUIRED)
                    }
                }
                .build()

            val workRequest = OneTimeWorkRequestBuilder<FetchWorker>()
                .setConstraints(constraints)
                .setInputData(
                    Data.Builder()
                        .putString("url", url)
                        .putLong("notificationTime", notificationTime)
                        .putInt("timeout", 30000)
                        .putInt("retryAttempts", 3)
                        .putInt("retryDelay", 1000)
                        .putBoolean("immediate", true)
                        .putBoolean(KEY_IS_DUAL, false)
                        .putString(KEY_CACHE_SCOPE, ContentCacheScope.DAILY)
                        .build()
                )
                .addTag("prefetch")
                .build()

            WorkManager.getInstance(context)
                .enqueue(workRequest)

            Log.i(TAG, "Immediate prefetch scheduled")
        }
    }

    override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
        val start = SystemClock.elapsedRealtime()
        val url = inputData.getString("url")
        val timeout = inputData.getInt("timeout", 30000)
        val retryAttempts = inputData.getInt("retryAttempts", 3)
        val retryDelay = inputData.getInt("retryDelay", 1000)
        val notificationTime = inputData.getLong("notificationTime", 0L)
        val isDual = inputData.getBoolean(KEY_IS_DUAL, false)
        val cacheScope = inputData.getString(KEY_CACHE_SCOPE) ?: ContentCacheScope.LEGACY
        val nextNotifyAt = inputData.getLong(KEY_NEXT_NOTIFY_AT, 0L)

        try {
            Log.i(TAG, "Starting content fetch from: $url, notificationTime=$notificationTime, isDual=$isDual, scope=$cacheScope")

            val payload = resolvePayload(url, timeout, retryAttempts, retryDelay, isDual, nextNotifyAt)
            val skipDualChainedNotify =
                isDual && nextNotifyAt > 0L && isDualSkipNotificationPayload(payload)
            val contentCache = ContentCache(
                id = generateId(),
                fetchedAt = System.currentTimeMillis(),
                ttlSeconds = 3600, // 1 hour default TTL
                payload = payload,
                meta = "fetched_by_workmanager",
                cacheScope = cacheScope
            )

            // Store in database
            val db = DailyNotificationDatabase.getDatabase(applicationContext)
            db.contentCacheDao().upsert(contentCache)

            // If this is a prefetch for a specific notification, create NotificationContentEntity
            // so the notification worker can find it when the alarm fires
            if (notificationTime > 0) {
                try {
                    val notificationId = "notify_$notificationTime"
                    val (title, body) = parsePayload(payload)

                    val entity = org.timesafari.dailynotification.entities.NotificationContentEntity(
                        notificationId,
                        "2.1.0", // Plugin version
                        null, // timesafariDid - can be set if available
                        "daily",
                        title,
                        body,
                        notificationTime,
                        java.time.ZoneId.systemDefault().id
                    )
                    entity.priority = 0 // default priority
                    entity.vibrationEnabled = true
                    entity.soundEnabled = true
                    entity.deliveryStatus = "pending"
                    entity.createdAt = System.currentTimeMillis()
                    entity.updatedAt = System.currentTimeMillis()
                    entity.ttlSeconds = contentCache.ttlSeconds.toLong()

                    // Save to Room database so notification worker can find it
                    db.notificationContentDao().insertNotification(entity)
                    Log.i(TAG, "Created NotificationContentEntity: id=$notificationId, scheduledTime=$notificationTime")
                } catch (e: Exception) {
                    Log.e(TAG, "Failed to create NotificationContentEntity", e)
                    // Continue - at least ContentCache was saved
                }
            }

            // Record success in history
            db.historyDao().insert(
                History(
                    refId = contentCache.id,
                    kind = "fetch",
                    occurredAt = System.currentTimeMillis(),
                    durationMs = SystemClock.elapsedRealtime() - start,
                    outcome = "success"
                )
            )

            Log.i(TAG, "Content fetch completed successfully")
            if (isDual && nextNotifyAt > 0L) {
                if (!skipDualChainedNotify) {
                    DualScheduleNotifyScheduler.scheduleChainedNotifyAlarm(applicationContext, nextNotifyAt)
                } else {
                    Log.i(TAG, "Dual fetch: empty native content — skip chained notify")
                }
                DualScheduleFetchRecovery.enqueueFromPersistedConfig(applicationContext)
            }
            Result.success()
        } catch (e: IOException) {
            Log.w(TAG, "Network error during fetch", e)
            recordFailure("network_error", start, e)
            Result.retry()
        } catch (e: Exception) {
            Log.e(TAG, "Unexpected error during fetch", e)
            recordFailure("unexpected_error", start, e)
            if (isDual && nextNotifyAt > 0L) {
                DualScheduleNotifyScheduler.scheduleChainedNotifyAlarm(applicationContext, nextNotifyAt)
            }
            Result.failure()
        }
    }

    private suspend fun resolvePayload(
        url: String?,
        timeout: Int,
        retryAttempts: Int,
        retryDelay: Int,
        isDual: Boolean,
        nextNotifyAt: Long
    ): ByteArray {
        if (isDual && url.isNullOrBlank()) {
            val native = DailyNotificationPlugin.getNativeFetcherStatic()
            return if (native != null) {
                fetchNativeDualPayload(native, timeout, nextNotifyAt)
            } else {
                Log.w(TAG, "Dual fetch with no URL and no native fetcher; using mock content")
                generateMockContent()
            }
        }
        return fetchContent(url, timeout, retryAttempts, retryDelay)
    }

    private suspend fun fetchNativeDualPayload(
        native: NativeNotificationContentFetcher,
        timeoutMs: Int,
        nextNotifyAtMillis: Long
    ): ByteArray = withContext(Dispatchers.IO) {
        val metadata = java.util.HashMap<String, Any>()
        metadata["retryCount"] = 0
        metadata["immediate"] = false
        val scheduledTime: Long? = if (nextNotifyAtMillis > 0L) nextNotifyAtMillis else null
        val ctx = FetchContext(
            "prefetch",
            scheduledTime,
            System.currentTimeMillis(),
            metadata
        )
        val future = native.fetchContent(ctx)
        try {
            val contents = future.get(timeoutMs.toLong(), TimeUnit.MILLISECONDS)
            val list = contents?.toList() ?: emptyList()
            notificationContentsToDualPayloadBytes(list)
        } catch (e: Exception) {
            Log.e(TAG, "Native dual fetch failed", e)
            throw IOException("native_fetch_failed: ${e.message}", e)
        }
    }

    private fun notificationContentsToDualPayloadBytes(contents: List<NotificationContent>): ByteArray {
        if (contents.isEmpty()) {
            return dualEmptyNativeFetchPayload
        }
        val c = contents[0]
        val title = c.getTitle() ?: "Daily Notification"
        val body = c.getBody() ?: ""
        val json = JSONObject()
        json.put("title", title)
        json.put("body", body)
        json.put("content", body)
        return json.toString().toByteArray(Charsets.UTF_8)
    }

    private suspend fun fetchContent(
        url: String?,
        timeout: Int,
        retryAttempts: Int,
        retryDelay: Int
    ): ByteArray {
        if (url.isNullOrBlank()) {
            // Generate mock content for testing
            return generateMockContent()
        }

        var lastException: Exception? = null

        repeat(retryAttempts) { attempt ->
            try {
                val connection = URL(url).openConnection() as HttpURLConnection
                connection.connectTimeout = timeout
                connection.readTimeout = timeout
                connection.requestMethod = "GET"

                val responseCode = connection.responseCode
                if (responseCode == HttpURLConnection.HTTP_OK) {
                    return connection.inputStream.readBytes()
                } else {
                    throw IOException("HTTP $responseCode: ${connection.responseMessage}")
                }
            } catch (e: Exception) {
                lastException = e
                if (attempt < retryAttempts - 1) {
                    Log.w(TAG, "Fetch attempt ${attempt + 1} failed, retrying in ${retryDelay}ms", e)
                    kotlinx.coroutines.delay(retryDelay.toLong())
                }
            }
        }

        throw lastException ?: IOException("All retry attempts failed")
    }

    private fun generateMockContent(): ByteArray {
        val mockData = """
            {
                "timestamp": ${System.currentTimeMillis()},
                "content": "Daily notification content",
                "source": "mock_generator",
                "version": "2.1.4"
            }
        """.trimIndent()
        return mockData.toByteArray()
    }

    private suspend fun recordFailure(outcome: String, start: Long, error: Throwable) {
        try {
            val db = DailyNotificationDatabase.getDatabase(applicationContext)
            db.historyDao().insert(
                History(
                    refId = "fetch_${System.currentTimeMillis()}",
                    kind = "fetch",
                    occurredAt = System.currentTimeMillis(),
                    durationMs = SystemClock.elapsedRealtime() - start,
                    outcome = outcome,
                    diagJson = "{\"error\": \"${error.message}\"}"
                )
            )
        } catch (e: Exception) {
            Log.e(TAG, "Failed to record failure", e)
        }
    }

    private fun generateId(): String {
        return "fetch_${System.currentTimeMillis()}_${(1000..9999).random()}"
    }

    /**
     * Parse payload to extract title and body
     * Handles both JSON and plain text payloads
     *
     * @param payload Raw payload bytes
     * @return Pair of (title, body)
     */
    private fun parsePayload(payload: ByteArray): Pair<String, String> {
        return try {
            val payloadString = String(payload, Charsets.UTF_8)

            // Try to parse as JSON
            val json = JSONObject(payloadString)
            val title = json.optString("title", "Daily Notification")
            val body = json.optString("body", json.optString("content", payloadString))
            Pair(title, body)
        } catch (e: Exception) {
            // Not JSON, use as plain text
            val text = String(payload, Charsets.UTF_8)
            Pair("Daily Notification", text)
        }
    }
}
