package me.khodak.claudeusage.data

import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.util.Calendar

class PreferencesManager(context: Context) {

    private val gson = Gson()

    private var usingFallbackPrefs = false
    private val securePrefs = createSecurePrefs(context, onFallback = { usingFallbackPrefs = true })

    private val prefs = context.getSharedPreferences("claude_prefs", Context.MODE_PRIVATE)

    fun saveCookies(cookies: String) {
        // Never store cookies in plain-text fallback prefs
        if (usingFallbackPrefs) return
        try {
            securePrefs.edit().putString(KEY_COOKIES, cookies).apply()
        } catch (_: Exception) {}
    }

    fun getCookies(): String? {
        // Cookies are never written in fallback (plaintext) mode — make that invariant explicit on
        // the read side too, so any future write that bypasses the guard still can't surface here.
        if (usingFallbackPrefs) return null
        return try {
            securePrefs.getString(KEY_COOKIES, null)
        } catch (_: Exception) { null }
    }

    fun clearSession() {
        try { securePrefs.edit().clear().apply() } catch (_: Exception) {}
        prefs.edit().remove(KEY_ORG_ID).remove(KEY_SESSION_START).apply()
    }

    fun saveOrgId(id: String) = prefs.edit().putString(KEY_ORG_ID, id).apply()
    fun getOrgId(): String? = prefs.getString(KEY_ORG_ID, null)

    fun saveSessionStart(epochMs: Long) = prefs.edit().putLong(KEY_SESSION_START, epochMs).apply()
    fun getSessionStart(): Long = prefs.getLong(KEY_SESSION_START, 0L)

    fun markTodayActive() {
        val cal = Calendar.getInstance()
        val dayBit = 1 shl cal.get(Calendar.DAY_OF_WEEK) - 1
        val weekKey = getWeekKey(cal)
        // Reset mask if it's a new week
        val storedWeek = prefs.getString(KEY_ACTIVE_WEEK, "")
        val currentMask = if (storedWeek == weekKey) prefs.getInt(KEY_ACTIVE_MASK, 0) else 0
        prefs.edit()
            .putString(KEY_ACTIVE_WEEK, weekKey)
            .putInt(KEY_ACTIVE_MASK, currentMask or dayBit)
            .apply()
    }

    fun getWeeklyMask(): Int {
        val cal = Calendar.getInstance()
        val weekKey = getWeekKey(cal)
        val storedWeek = prefs.getString(KEY_ACTIVE_WEEK, "")
        return if (storedWeek == weekKey) prefs.getInt(KEY_ACTIVE_MASK, 0) else 0
    }

    private fun getWeekKey(cal: Calendar): String {
        val year = cal.get(Calendar.YEAR)
        val week = cal.get(Calendar.WEEK_OF_YEAR)
        return "$year-$week"
    }

    fun saveUsageData(data: UsageData) =
        prefs.edit().putString(KEY_USAGE_DATA, gson.toJson(data)).apply()

    fun getUsageData(): UsageData? = try {
        prefs.getString(KEY_USAGE_DATA, null)?.let { gson.fromJson(it, UsageData::class.java) }
    } catch (e: Exception) { null }

    fun isLoggedIn(): Boolean = !getCookies().isNullOrBlank()

    // Consecutive 401/403 counter — we only clear the session after several in a row, so a
    // single transient auth failure (e.g. a Cloudflare challenge) doesn't log the user out.
    fun getAuthFailCount(): Int = prefs.getInt(KEY_AUTH_FAILS, 0)
    fun incAuthFailCount(): Int {
        val n = getAuthFailCount() + 1
        prefs.edit().putInt(KEY_AUTH_FAILS, n).apply()
        return n
    }
    fun resetAuthFailCount() {
        if (getAuthFailCount() != 0) prefs.edit().putInt(KEY_AUTH_FAILS, 0).apply()
    }

    // ── Usage history (for the in-app chart) ─────────────────────────────────

    /**
     * Append a history point if [data] carries a real reading.
     * The session line uses [UsageData.sessionReadingPct] (utilization preferred, message-count
     * progress as fallback) so accounts that only expose message counts still build history.
     * De-duplicates rapid double-fires (manual refresh + background worker landing
     * together) by skipping points within [MIN_HISTORY_GAP_MS] of the last one, and
     * prunes anything older than [HISTORY_RETENTION_MS] / beyond [MAX_HISTORY_POINTS].
     */
    fun recordHistory(data: UsageData) {
        val sessionPct = data.sessionReadingPct
        if (sessionPct < 0f && data.weeklyUtilization < 0f) return
        val now = System.currentTimeMillis()
        val history = getHistory().toMutableList()
        // Throttle to at most one point per MIN_HISTORY_GAP_MS by SKIPPING a too-soon reading.
        // (Previously we deleted the last point and re-added in place — but the foreground loop
        // refreshes every 30s, well inside this 2-min window, so history could never grow past a
        // single point while the app was open and the chart stayed on "Collecting history…".)
        if (!shouldRecordHistory(history.lastOrNull()?.epochMs, now, MIN_HISTORY_GAP_MS)) return
        history.add(
            UsageSnapshot(
                epochMs = now,
                sessionPct = sessionPct,
                weeklyPct = data.weeklyUtilization
            )
        )
        val cutoff = now - HISTORY_RETENTION_MS
        val pruned = history.filter { it.epochMs >= cutoff }
            .takeLast(MAX_HISTORY_POINTS)
        prefs.edit().putString(KEY_HISTORY, gson.toJson(pruned)).apply()
    }

    fun getHistory(): List<UsageSnapshot> {
        val json = prefs.getString(KEY_HISTORY, null) ?: return emptyList()
        return try {
            val type = object : TypeToken<List<UsageSnapshot>>() {}.type
            gson.fromJson<List<UsageSnapshot>>(json, type) ?: emptyList()
        } catch (e: Exception) { emptyList() }
    }

    // ── Widget style ─────────────────────────────────────────────────────────

    /** Home-screen widget visual style: [STYLE_BARS] (default) or [STYLE_RINGS]. */
    fun getWidgetStyle(): String = prefs.getString(KEY_WIDGET_STYLE, STYLE_BARS) ?: STYLE_BARS
    fun setWidgetStyle(style: String) = prefs.edit().putString(KEY_WIDGET_STYLE, style).apply()

    // ── Notification settings ────────────────────────────────────────────────

    fun isNotifyEnabled(): Boolean = prefs.getBoolean(KEY_NOTIFY_ENABLED, true)
    fun setNotifyEnabled(v: Boolean) = prefs.edit().putBoolean(KEY_NOTIFY_ENABLED, v).apply()

    /**
     * Per-level "already alerted" flag (e.g. "session_90"). Set when the level is crossed,
     * cleared when usage drops back below it — so each level fires once per window.
     */
    fun wasNotified(key: String): Boolean = prefs.getBoolean("notified_$key", false)
    fun setNotified(key: String, v: Boolean) =
        prefs.edit().putBoolean("notified_$key", v).apply()

    companion object {
        private const val KEY_COOKIES = "session_cookies"
        private const val KEY_ORG_ID = "org_id"
        private const val KEY_SESSION_START = "session_start"
        private const val KEY_USAGE_DATA = "usage_data"
        private const val KEY_ACTIVE_WEEK = "active_week"
        private const val KEY_ACTIVE_MASK = "active_mask"
        private const val KEY_HISTORY = "usage_history"
        private const val KEY_NOTIFY_ENABLED = "notify_enabled"
        private const val KEY_AUTH_FAILS = "auth_fail_count"
        private const val KEY_WIDGET_STYLE = "widget_style"

        const val STYLE_BARS = "bars"
        const val STYLE_RINGS = "rings"

        internal const val MIN_HISTORY_GAP_MS = 2 * 60 * 1000L      // collapse readings <2 min apart
        private const val HISTORY_RETENTION_MS = 7 * 24 * 60 * 60 * 1000L  // keep 7 days
        private const val MAX_HISTORY_POINTS = 600

        /**
         * Pure throttle decision for [recordHistory]: should a new point be appended?
         * Returns false only when a previous point exists ([lastEpochMs] != null) AND the gap to
         * [now] is below [minGapMs]; true otherwise (including the first-ever point, lastEpochMs == null).
         * No Android dependencies — kept separate so the throttle rule is unit-testable.
         */
        internal fun shouldRecordHistory(lastEpochMs: Long?, now: Long, minGapMs: Long): Boolean {
            if (lastEpochMs == null) return true
            return now - lastEpochMs >= minGapMs
        }

        fun createSecurePrefs(context: Context, onFallback: (() -> Unit)? = null): android.content.SharedPreferences {
            return try {
                buildEncryptedPrefs(context)
            } catch (e: Exception) {
                if (isKeyPermanentlyInvalidated(e)) {
                    // Key permanently gone (biometric/PIN changed) — must wipe; user must re-login.
                    try {
                        context.deleteSharedPreferences("claude_secure")
                        buildEncryptedPrefs(context)
                    } catch (_: Exception) {
                        onFallback?.invoke()
                        context.getSharedPreferences("claude_secure_fallback", Context.MODE_PRIVATE)
                    }
                } else {
                    // Transient failure (Keystore busy, cold boot, screen locked during BG work).
                    // Do NOT delete the encrypted file — it will be readable next session.
                    onFallback?.invoke()
                    context.getSharedPreferences("claude_secure_fallback", Context.MODE_PRIVATE)
                }
            }
        }

        private fun buildEncryptedPrefs(context: Context): android.content.SharedPreferences {
            val masterKey = MasterKey.Builder(context)
                .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
                .build()
            return EncryptedSharedPreferences.create(
                context, "claude_secure", masterKey,
                EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
                EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
            )
        }

        private fun isKeyPermanentlyInvalidated(e: Exception): Boolean {
            var t: Throwable? = e
            while (t != null) {
                if (t is android.security.keystore.KeyPermanentlyInvalidatedException) return true
                t = t.cause
            }
            return false
        }
    }
}
