pragma Singleton

import QtQuick
import QtQuick.Window
import Quickshell
import Quickshell.Io
import Quickshell.Services.Notifications
import qs.Utils
import qs.Services
import qs.Constants
import "../Utils/sha256.js" as Checksum

Singleton {
  id: root

  // Configuration
  property int maxVisible: 5
  property int maxHistory: 100
  property string historyFile: CacheService.notificationsCacheFile
  property string cacheDirImagesNotifications: Quickshell.env("HOME") + "/.cache/quickshell/notifications/"
  property real lowUrgencyDuration: 3
  property real normalUrgencyDuration: 8
  property real criticalUrgencyDuration: 15

  // Models
  property ListModel activeList: ListModel {}
  property ListModel historyList: ListModel {}

  // Internal state
  property var activeMap: ({})
  property var imageQueue: []

  // Performance optimization: Track notification metadata separately
  property var notificationMetadata: ({}) // Stores timestamp and duration for each notification

  PanelWindow {
    implicitHeight: 1
    implicitWidth: 1
    color: Color.transparent
    mask: Region {}

    Image {
      id: cacher
      width: 64
      height: 64
      visible: true
      cache: false
      asynchronous: true
      mipmap: true
      antialiasing: true

      onStatusChanged: {
        if (imageQueue.length === 0)
        return
        const req = imageQueue[0]

        if (status === Image.Ready) {
          Quickshell.execDetached(["mkdir", "-p", cacheDirImagesNotifications])
          grabToImage(result => {
                        if (result.saveToFile(req.dest))
                        updateImagePath(req.imageId, req.dest)
                        processNextImage()
                      })
        } else if (status === Image.Error) {
          processNextImage()
        }
      }

      function processNextImage() {
        imageQueue.shift()
        if (imageQueue.length > 0) {
          source = imageQueue[0].src
        } else {
          source = ""
        }
      }
    }
  }

  // Notification server
  NotificationServer {
    keepOnReload: false
    imageSupported: true
    actionsSupported: true
    onNotification: notification => handleNotification(notification)
  }

  // Main handler
  function handleNotification(notification) {
    const data = createData(notification)
    addToHistory(data)

    if (SettingsService.notifications.doNotDisturb)
      return

    activeMap[data.id] = notification
    notification.tracked = true
    notification.closed.connect(() => removeActive(data.id))

    // Store metadata for efficient progress calculation
    const durations = [lowUrgencyDuration * 1000, normalUrgencyDuration * 1000, criticalUrgencyDuration * 1000]

    let expire = 0
    if (data.expireTimeout === 0) {
      expire = -1 // Never expire
    } else if (data.expireTimeout > 0) {
      expire = data.expireTimeout
    } else {
      expire = durations[data.urgency]
    }

    notificationMetadata[data.id] = {
      "timestamp": data.timestamp.getTime(),
      "duration": expire,
      "urgency": data.urgency
    }

    activeList.insert(0, data)
    while (activeList.count > maxVisible) {
      const last = activeList.get(activeList.count - 1)
      activeMap[last.id]?.dismiss()
      activeList.remove(activeList.count - 1)
      delete notificationMetadata[last.id]
    }
  }

  function createData(n) {
    const time = new Date()
    const id = Checksum.sha256(JSON.stringify({
                                                "summary": n.summary,
                                                "body": n.body,
                                                "app": n.appName,
                                                "time": time.getTime()
                                              }))

    const image = n.image || getIcon(n.appIcon)
    const imageId = generateImageId(n, image)
    queueImage(image, imageId)

    return {
      "id": id,
      "summary": (n.summary || ""),
      "body": stripTags(n.body || ""),
      "appName": getAppName(n.appName),
      "urgency": n.urgency < 0 || n.urgency > 2 ? 1 : n.urgency,
      "expireTimeout": n.expireTimeout,
      "timestamp": time,
      "progress": 1.0,
      "originalImage": image,
      "cachedImage": imageId ? (cacheDirImagesNotifications + imageId + ".png") : image,
      "actionsJson": JSON.stringify((n.actions || []).map(a => ({
                                                                  "text": a.text || "Action",
                                                                  "identifier": a.identifier || ""
                                                                })))
    }
  }

  function queueImage(path, imageId) {
    if (!path || !path.startsWith("image://") || !imageId)
      return

    const dest = cacheDirImagesNotifications + imageId + ".png"

    for (const req of imageQueue) {
      if (req.imageId === imageId)
        return
    }

    imageQueue.push({
                      "src": path,
                      "dest": dest,
                      "imageId": imageId
                    })

    if (imageQueue.length === 1)
      cacher.source = path
  }

  function updateImagePath(id, path) {
    updateModel(activeList, id, "cachedImage", path)
    updateModel(historyList, id, "cachedImage", path)
    saveHistory()
  }

  function updateModel(model, id, prop, value) {
    for (var i = 0; i < model.count; i++) {
      if (model.get(i).id === id) {
        model.setProperty(i, prop, value)
        break
      }
    }
  }

  function removeActive(id) {
    for (var i = 0; i < activeList.count; i++) {
      if (activeList.get(i).id === id) {
        activeList.remove(i)
        delete activeMap[id]
        delete notificationMetadata[id]
        break
      }
    }
  }

  // Optimized batch progress update
  Timer {
    interval: 50 // Reduced from 10ms to 50ms (20 updates/sec instead of 100)
    repeat: true
    running: activeList.count > 0
    onTriggered: updateAllProgress()
  }

  function updateAllProgress() {
    const now = Date.now()
    const toRemove = []
    const updates = [] // Batch updates

    // Collect all updates first
    for (var i = 0; i < activeList.count; i++) {
      const notif = activeList.get(i)
      const meta = notificationMetadata[notif.id]

      if (!meta || meta.duration === -1)
        continue

      // Skip infinite notifications
      const elapsed = now - meta.timestamp
      const progress = Math.max(1.0 - (elapsed / meta.duration), 0.0)

      if (progress <= 0) {
        toRemove.push(notif.id)
      } else if (Math.abs(notif.progress - progress) > 0.005) {
        // Only update if change is significant
        updates.push({
                       "index": i,
                       "progress": progress
                     })
      }
    }

    // Apply batch updates
    for (const update of updates) {
      activeList.setProperty(update.index, "progress", update.progress)
    }

    // Remove expired notifications (one at a time to allow animation)
    if (toRemove.length > 0) {
      animateAndRemove(toRemove[0])
    }
  }

  // History management
  function addToHistory(data) {
    historyList.insert(0, data)

    while (historyList.count > maxHistory) {
      const old = historyList.get(historyList.count - 1)
      if (old.cachedImage && !old.cachedImage.startsWith("image://")) {
        Quickshell.execDetached(["rm", "-f", old.cachedImage])
      }
      historyList.remove(historyList.count - 1)
    }
    saveHistory()
  }

  // Persistence
  FileView {
    id: historyFileView
    path: historyFile
    printErrors: false
    onLoaded: loadHistory()
    onLoadFailed: error => {
      if (error === 2)
      writeAdapter()
    }

    JsonAdapter {
      id: adapter
      property var notifications: []
    }
  }

  Timer {
    id: saveTimer
    interval: 200
    onTriggered: performSaveHistory()
  }

  function saveHistory() {
    saveTimer.restart()
  }

  function performSaveHistory() {
    try {
      const items = []
      for (var i = 0; i < historyList.count; i++) {
        const n = historyList.get(i)
        const copy = Object.assign({}, n)
        copy.timestamp = n.timestamp.getTime()
        items.push(copy)
      }
      adapter.notifications = items
      historyFileView.writeAdapter()
    } catch (e) {
      Logger.error("Notifications", "Save history failed:", e)
    }
  }

  function loadHistory() {
    try {
      historyList.clear()
      for (const item of adapter.notifications || []) {
        const time = new Date(item.timestamp)

        let cachedImage = item.cachedImage || ""
        if (item.originalImage && item.originalImage.startsWith("image://") && !cachedImage) {
          const imageId = generateImageId(item, item.originalImage)
          if (imageId) {
            cachedImage = cacheDirImagesNotifications + imageId + ".png"
          }
        }

        historyList.append({
                             "id": item.id || "",
                             "summary": item.summary || "",
                             "body": item.body || "",
                             "appName": item.appName || "",
                             "urgency": item.urgency < 0 || item.urgency > 2 ? 1 : item.urgency,
                             "timestamp": time,
                             "originalImage": item.originalImage || "",
                             "cachedImage": cachedImage
                           })
      }
    } catch (e) {
      Logger.error("Notifications", "Load failed:", e)
    }
  }

  function getAppName(name) {
    if (!name || name.trim() === "")
      return "Unknown"

    name = name.trim()

    if (name.includes(".") && (name.startsWith("com.") || name.startsWith("org.") || name.startsWith("io.") || name.startsWith("net."))) {
      const parts = name.split(".")
      let appPart = parts[parts.length - 1]

      if (!appPart || appPart === "app" || appPart === "desktop") {
        appPart = parts[parts.length - 2] || parts[0]
      }

      if (appPart) {
        name = appPart
      }
    }

    if (name.includes(".")) {
      const parts = name.split(".")
      let displayName = parts[parts.length - 1]

      if (!displayName || /^\d+$/.test(displayName)) {
        displayName = parts[parts.length - 2] || parts[0]
      }

      if (displayName) {
        displayName = displayName.charAt(0).toUpperCase() + displayName.slice(1)
        displayName = displayName.replace(/([a-z])([A-Z])/g, '$1 $2')
        displayName = displayName.replace(/app$/i, '').trim()
        displayName = displayName.replace(/desktop$/i, '').trim()
        displayName = displayName.replace(/flatpak$/i, '').trim()

        if (!displayName) {
          displayName = parts[parts.length - 1].charAt(0).toUpperCase() + parts[parts.length - 1].slice(1)
        }
      }

      return displayName || name
    }

    let displayName = name.charAt(0).toUpperCase() + name.slice(1)
    displayName = displayName.replace(/([a-z])([A-Z])/g, '$1 $2')
    displayName = displayName.replace(/app$/i, '').trim()
    displayName = displayName.replace(/desktop$/i, '').trim()

    return displayName || name
  }

  function getIcon(icon) {
    if (!icon)
      return ""
    if (icon.startsWith("/") || icon.startsWith("file://"))
      return icon
    return ThemeIcons.iconFromName(icon)
  }

  function stripTags(text) {
    return text.replace(/<[^>]*>?/gm, '')
  }

  function generateImageId(notification, image) {
    if (image && image.startsWith("image://")) {
      if (image.startsWith("image://qsimage/")) {
        const key = (notification.appName || "") + "|" + (notification.summary || "")
        return Checksum.sha256(key)
      }
      return Checksum.sha256(image)
    }
    return ""
  }

  // Public API
  function dismissActiveNotification(id) {
    activeMap[id]?.dismiss()
    removeActive(id)
  }

  function dismissAllActive() {
    Object.values(activeMap).forEach(n => n.dismiss())
    activeList.clear()
    activeMap = {}
    notificationMetadata = {}
  }

  function invokeAction(id, actionId) {
    const n = activeMap[id]
    if (!n?.actions)
      return false

    for (const action of n.actions) {
      if (action.identifier === actionId && action.invoke) {
        action.invoke()
        return true
      }
    }
    return false
  }

  function removeFromHistory(notificationId) {
    for (var i = 0; i < historyList.count; i++) {
      const notif = historyList.get(i)
      if (notif.id === notificationId) {
        if (notif.cachedImage && !notif.cachedImage.startsWith("image://")) {
          Quickshell.execDetached(["rm", "-f", notif.cachedImage])
        }
        historyList.remove(i)
        saveHistory()
        return true
      }
    }
    return false
  }

  function clearHistory() {
    try {
      Quickshell.execDetached(["sh", "-c", `rm -rf "${cacheDirImagesNotifications}"*`])
    } catch (e) {
      Logger.error("Notifications", "Failed to clear cache directory:", e)
    }

    historyList.clear()
    saveHistory()
  }

  // Signals & connections
  signal animateAndRemove(string notificationId)

  Connections {
    target: SettingsService.notifications
    function onDoNotDisturbChanged() {
      const enabled = SettingsService.notifications.doNotDisturb
    }
  }
}
