package de.wechselbild.wallpaper

import android.app.WallpaperManager
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.Paint
import android.util.Base64
import android.util.LruCache
import app.tauri.plugin.JSArray
import app.tauri.plugin.JSObject
import java.io.ByteArrayOutputStream
import java.io.File
import java.security.MessageDigest
import org.json.JSONObject

object WallpaperStore {
  private const val PREFS = "wechselbild"
  private const val KEY_INDEX = "current_index"
  private const val KEY_CURRENT_ID = "current_entry_id"
  private const val KEY_INTERVAL = "interval_minutes"
  private const val CROP_PREFIX = "crop_"
  private const val HOME_PREVIEW_LIMIT = 12
  private val thumbnailCache = LruCache<String, String>(48)

  private data class CropSettings(val mode: String = "cover", val zoom: Double = 1.0, val x: Double = 0.5, val y: Double = 0.5, val rotation: Int = 0)
  private sealed class Entry {
    abstract val id: String
    abstract val preview: File
    data class Local(val file: File) : Entry() { override val id = file.name; override val preview = file }
    data class Immich(
      override val id: String,
      val assetId: String,
      val serverUrl: String,
      val addedAt: Long,
      val metadata: File,
      override val preview: File,
    ) : Entry()
  }

  fun directory(context: Context) = File(context.filesDir, "wallpapers").apply { mkdirs() }
  fun files(context: Context) = directory(context).listFiles()?.filter { it.isFile && !it.name.startsWith(".") }?.sortedBy { it.name } ?: emptyList()
  private fun immichRoot(context: Context) = File(context.filesDir, "immich-wallpapers").apply { mkdirs() }
  private fun immichEntries(context: Context) = File(immichRoot(context), "entries").apply { mkdirs() }
  private fun immichPreviews(context: Context) = File(immichRoot(context), "previews").apply { mkdirs() }
  private fun prefs(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)

  private fun serverKey(serverUrl: String): String = MessageDigest.getInstance("SHA-256")
    .digest(serverUrl.toByteArray(Charsets.UTF_8)).take(6).joinToString("") { "%02x".format(it) }

  private fun virtualKey(serverUrl: String, assetId: String) = "${serverKey(serverUrl)}-$assetId"
  private fun virtualId(serverUrl: String, assetId: String) = "immich:${serverKey(serverUrl)}:$assetId"

  private fun entries(context: Context): List<Entry> {
    val local = files(context).map { Entry.Local(it) }
    val virtual = immichEntries(context).listFiles()?.filter { it.isFile && it.extension == "json" }?.mapNotNull { metadata ->
      runCatching {
        val json = JSONObject(metadata.readText())
        val serverUrl = json.getString("serverUrl")
        val assetId = json.getString("assetId")
        val preview = File(immichPreviews(context), "${virtualKey(serverUrl, assetId)}.preview")
        if (!preview.isFile) return@runCatching null
        Entry.Immich(
          id = virtualId(serverUrl, assetId),
          assetId = assetId,
          serverUrl = serverUrl,
          addedAt = json.optLong("addedAt", metadata.lastModified()),
          metadata = metadata,
          preview = preview,
        )
      }.getOrNull()
    }?.filterNotNull()?.sortedWith(compareBy<Entry.Immich> { it.addedAt }.thenBy { it.id }) ?: emptyList()
    return local + virtual
  }

  private fun currentIndex(context: Context, items: List<Entry>): Int {
    if (items.isEmpty()) return 0
    val preferences = prefs(context)
    val currentId = preferences.getString(KEY_CURRENT_ID, null)
    val byId = items.indexOfFirst { it.id == currentId }
    if (byId >= 0) return byId
    val legacy = preferences.getInt(KEY_INDEX, 0).coerceIn(0, items.lastIndex)
    preferences.edit().putString(KEY_CURRENT_ID, items[legacy].id).apply()
    return legacy
  }

  fun intervalMinutes(context: Context): Int {
    val preferences = prefs(context)
    return if (preferences.contains(KEY_INTERVAL)) preferences.getInt(KEY_INTERVAL, 0)
    else if (preferences.getBoolean("enabled", false)) 30 else 0
  }
  fun enabled(context: Context) = intervalMinutes(context) > 0
  fun shuffle(context: Context) = prefs(context).getBoolean("shuffle", true)
  fun lockOnly(context: Context) = prefs(context).getBoolean("lockScreenOnly", true)

  fun set(context: Context, name: String, value: Boolean) {
    require(name in setOf("shuffle", "lockScreenOnly")) { "Unbekannte Einstellung" }
    prefs(context).edit().putBoolean(name, value).apply()
  }

  fun setInterval(context: Context, minutes: Int) {
    require(minutes == 0 || minutes in 5..720) { "Ungültiges Wechselintervall" }
    prefs(context).edit().putInt(KEY_INTERVAL, minutes).putBoolean("enabled", minutes > 0).apply()
  }

  @Synchronized
  fun state(context: Context, includePreviews: Boolean = true): JSObject {
    val items = entries(context)
    val index = currentIndex(context, items)
    val previewItems = if (index < HOME_PREVIEW_LIMIT) items.take(HOME_PREVIEW_LIMIT) else listOf(items[index]) + items.take(HOME_PREVIEW_LIMIT - 1)
    val previewIndex = if (index < HOME_PREVIEW_LIMIT) index else 0
    return JSObject().apply {
      put("imageCount", items.size)
      put("enabled", enabled(context))
      put("intervalMinutes", intervalMinutes(context))
      put("shuffle", shuffle(context))
      put("lockScreenOnly", lockOnly(context))
      put("currentIndex", previewIndex)
      val previews = JSArray()
      if (includePreviews) previewItems.forEach { previews.put(thumbnailDataUrl(it.preview)) }
      put("imageUrls", previews)
    }
  }

  @Synchronized
  fun gallery(context: Context, offset: Int, limit: Int): JSObject {
    val items = entries(context)
    val selectedIndex = currentIndex(context, items)
    val safeOffset = offset.coerceAtLeast(0).coerceAtMost(items.size)
    val safeLimit = limit.coerceIn(1, 100)
    val page = JSArray()
    items.drop(safeOffset).take(safeLimit).forEachIndexed { pageIndex, entry ->
      page.put(galleryImage(context, entry, safeOffset + pageIndex == selectedIndex))
    }
    return JSObject().apply { put("total", items.size); put("items", page) }
  }

  @Synchronized
  fun imageIds(context: Context) = entries(context).map { it.id }

  private fun cropKey(id: String) = CROP_PREFIX + id
  private fun crop(context: Context, entry: Entry): CropSettings {
    val raw = prefs(context).getString(cropKey(entry.id), null) ?: return CropSettings()
    return try {
      val json = JSONObject(raw)
      CropSettings(
        mode = if (json.optString("mode") == "contain") "contain" else "cover",
        zoom = json.optDouble("zoom", 1.0).coerceIn(1.0, 3.0),
        x = json.optDouble("x", 0.5).coerceIn(0.0, 1.0),
        y = json.optDouble("y", 0.5).coerceIn(0.0, 1.0),
        rotation = json.optInt("rotation", 0).let { ((it % 360) + 360) % 360 }.let { if (it % 90 == 0) it else 0 },
      )
    } catch (_: Exception) { CropSettings() }
  }

  private fun galleryImage(context: Context, entry: Entry, selected: Boolean): JSObject {
    val crop = crop(context, entry)
    return JSObject().apply {
      put("id", entry.id)
      put("url", thumbnailDataUrl(entry.preview))
      put("selected", selected)
      put("cropMode", crop.mode)
      put("cropZoom", crop.zoom)
      put("cropPositionX", crop.x)
      put("cropPositionY", crop.y)
      put("cropRotation", crop.rotation)
    }
  }

  @Synchronized
  fun setCrop(context: Context, id: String, mode: String, zoom: Double, x: Double, y: Double, rotation: Int): JSObject? {
    val items = entries(context)
    val entry = items.firstOrNull { it.id == id } ?: return null
    val normalized = CropSettings(
      mode = if (mode == "contain") "contain" else "cover",
      zoom = zoom.coerceIn(1.0, 3.0), x = x.coerceIn(0.0, 1.0), y = y.coerceIn(0.0, 1.0),
      rotation = ((rotation % 360) + 360) % 360,
    )
    require(normalized.rotation % 90 == 0) { "Ungültige Bilddrehung" }
    val json = JSONObject().apply {
      put("mode", normalized.mode); put("zoom", normalized.zoom); put("x", normalized.x); put("y", normalized.y); put("rotation", normalized.rotation)
    }
    prefs(context).edit().putString(cropKey(id), json.toString()).apply()
    return galleryImage(context, entry, items.indexOf(entry) == currentIndex(context, items))
  }

  @Synchronized
  fun addImmich(context: Context, serverUrl: String, assetId: String, previewSource: File): Boolean {
    val key = virtualKey(serverUrl, assetId)
    val metadata = File(immichEntries(context), "$key.json")
    val preview = File(immichPreviews(context), "$key.preview")
    if (metadata.isFile && preview.isFile) return false
    val previewTemp = File(immichPreviews(context), ".$key.preview.tmp")
    val metadataTemp = File(immichEntries(context), ".$key.json.tmp")
    try {
      previewSource.copyTo(previewTemp, overwrite = true)
      val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
      BitmapFactory.decodeFile(previewTemp.absolutePath, bounds)
      require(bounds.outWidth > 0 && bounds.outHeight > 0) { "Immich-Vorschau konnte nicht gelesen werden." }
      if (!previewTemp.renameTo(preview)) { previewTemp.copyTo(preview, overwrite = true); previewTemp.delete() }
      metadataTemp.writeText(JSONObject().apply {
        put("version", 1); put("serverUrl", serverUrl); put("assetId", assetId); put("addedAt", System.currentTimeMillis())
      }.toString())
      if (!metadataTemp.renameTo(metadata)) { metadataTemp.copyTo(metadata, overwrite = true); metadataTemp.delete() }
      return true
    } catch (error: Exception) {
      previewTemp.delete()
      metadataTemp.delete()
      if (!metadata.isFile) preview.delete()
      throw error
    }
  }

  fun hasImmich(context: Context, serverUrl: String, assetId: String) = File(immichEntries(context), "${virtualKey(serverUrl, assetId)}.json").isFile

  @Synchronized
  fun delete(context: Context, id: String) = deleteMany(context, listOf(id)) == 1

  @Synchronized
  fun deleteMany(context: Context, ids: List<String>): Int {
    val requested = ids.toSet()
    val items = entries(context)
    val current = items.getOrNull(currentIndex(context, items))
    val deleted = items.filter { it.id in requested }.filter { entry ->
      when (entry) {
        is Entry.Local -> entry.file.delete().also { if (it) removeThumbnail(entry.preview) }
        is Entry.Immich -> {
          entry.metadata.delete().also { removed ->
            if (removed) {
              removeThumbnail(entry.preview)
              entry.preview.delete()
              ImmichClient.deleteCachedOriginal(context, entry.serverUrl, entry.assetId)
            }
          }
        }
      }
    }
    if (deleted.isEmpty()) return 0
    val editor = prefs(context).edit()
    deleted.forEach { editor.remove(cropKey(it.id)) }
    val remaining = entries(context)
    val retained = current?.let { item -> remaining.indexOfFirst { it.id == item.id } } ?: -1
    if (remaining.isEmpty()) editor.remove(KEY_CURRENT_ID).putInt(KEY_INDEX, 0)
    else {
      val next = if (retained >= 0) retained else currentIndex(context, items).coerceAtMost(remaining.lastIndex)
      editor.putString(KEY_CURRENT_ID, remaining[next].id).putInt(KEY_INDEX, next)
    }
    editor.apply()
    return deleted.size
  }

  private fun renderForScreen(context: Context, source: Bitmap, crop: CropSettings): Bitmap {
    val metrics = context.resources.displayMetrics
    val targetWidth = metrics.widthPixels.coerceAtLeast(1)
    val targetHeight = metrics.heightPixels.coerceAtLeast(1)
    val quarterTurn = crop.rotation == 90 || crop.rotation == 270
    val rotatedWidth = if (quarterTurn) source.height else source.width
    val rotatedHeight = if (quarterTurn) source.width else source.height
    val widthScale = targetWidth.toDouble() / rotatedWidth
    val heightScale = targetHeight.toDouble() / rotatedHeight
    val baseScale = if (crop.mode == "contain") minOf(widthScale, heightScale) else maxOf(widthScale, heightScale)
    val scale = (baseScale * crop.zoom).toFloat()
    val scaledWidth = rotatedWidth * scale
    val scaledHeight = rotatedHeight * scale
    val left = ((targetWidth - scaledWidth) * crop.x).toFloat()
    val top = ((targetHeight - scaledHeight) * crop.y).toFloat()
    return Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888).also { output ->
      val canvas = Canvas(output)
      canvas.drawColor(Color.BLACK)
      val matrix = Matrix().apply {
        postTranslate(-source.width / 2f, -source.height / 2f)
        postRotate(crop.rotation.toFloat())
        postScale(scale, scale)
        postTranslate(left + scaledWidth / 2f, top + scaledHeight / 2f)
      }
      canvas.drawBitmap(source, matrix, Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG))
    }
  }

  private fun decodeForScreen(context: Context, file: File): Bitmap? {
    val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
    BitmapFactory.decodeFile(file.absolutePath, bounds)
    if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null
    val targetLongSide = maxOf(context.resources.displayMetrics.widthPixels, context.resources.displayMetrics.heightPixels).coerceAtLeast(1)
    var sample = 1
    while (maxOf(bounds.outWidth, bounds.outHeight) / (sample * 2) >= targetLongSide) sample *= 2
    return BitmapFactory.decodeFile(file.absolutePath, BitmapFactory.Options().apply { inSampleSize = sample; inPreferredConfig = Bitmap.Config.ARGB_8888 })
  }

  private fun thumbnailDataUrl(file: File): String {
    val cacheKey = "${file.absolutePath}:${file.lastModified()}:${file.length()}"
    thumbnailCache.get(cacheKey)?.let { return it }
    val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
    BitmapFactory.decodeFile(file.absolutePath, bounds)
    var sample = 1
    while (bounds.outWidth / sample > 360 || bounds.outHeight / sample > 480) sample *= 2
    val bitmap = BitmapFactory.decodeFile(file.absolutePath, BitmapFactory.Options().apply { inSampleSize = sample }) ?: return ""
    val result = ByteArrayOutputStream().use { out ->
      bitmap.compress(Bitmap.CompressFormat.JPEG, 76, out); bitmap.recycle()
      "data:image/jpeg;base64," + Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP)
    }
    thumbnailCache.put(cacheKey, result)
    return result
  }

  private fun removeThumbnail(file: File) {
    thumbnailCache.remove("${file.absolutePath}:${file.lastModified()}:${file.length()}")
  }

  @Synchronized
  fun applyNext(context: Context): Boolean {
    val items = entries(context)
    if (items.isEmpty()) return false
    val previous = currentIndex(context, items)
    val candidates = if (shuffle(context) && items.size > 1) {
      items.indices.filter { it != previous }.shuffled() + previous
    } else (1..items.size).map { (previous + it).mod(items.size) }
    val unavailableServers = mutableSetOf<String>()
    for (index in candidates) {
      val entry = items[index]
      val sourceFile = when (entry) {
        is Entry.Local -> entry.file
        is Entry.Immich -> {
          val source = ImmichClient.cachedOriginal(context, entry.serverUrl, entry.assetId, entry.serverUrl !in unavailableServers)
          if (source == null) {
            unavailableServers.add(entry.serverUrl)
            continue
          }
          source
        }
      }
      val bitmap = decodeForScreen(context, sourceFile) ?: continue
      val rendered = runCatching { renderForScreen(context, bitmap, crop(context, entry)) }.getOrNull()
      if (rendered == null) { bitmap.recycle(); continue }
      try {
        val manager = WallpaperManager.getInstance(context)
        if (android.os.Build.VERSION.SDK_INT >= 24 && lockOnly(context)) manager.setBitmap(rendered, null, true, WallpaperManager.FLAG_LOCK)
        else manager.setBitmap(rendered)
        prefs(context).edit().putString(KEY_CURRENT_ID, entry.id).putInt(KEY_INDEX, index).apply()
        return true
      } catch (_: Exception) {
        // Try the next usable entry without changing the current selection.
      } finally {
        rendered.recycle(); bitmap.recycle()
      }
    }
    return false
  }
}
