pragma Singleton

import QtQuick
import QtQuick.Window
import Quickshell
import Quickshell.Io
import Quickshell.Services.Notifications
import qs.Commons
import qs.Services.Power
import qs.Services.UI
import "../../Helpers/sha256.js" as Checksum

Singleton {
  id: root

  // Configuration
  property int maxVisible: 5
  property int maxHistory: 100
  property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json")
  property string stateFile: Settings.cacheDir + "notifications-state.json"

  // State
  property real lastSeenTs: 0
  // Volatile property that doesn't persist to settings (similar to noctaliaPerformanceMode)
  property bool doNotDisturb: false

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

  // Internal state
  property var activeNotifications: ({}) // Maps internal ID to {notification, watcher, metadata}
  property var quickshellIdToInternalId: ({})
  property var imageQueue: []

  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", Settings.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
  property var notificationServerLoader: null

  Component {
    id: notificationServerComponent
    NotificationServer {
      keepOnReload: false
      imageSupported: true
      actionsSupported: true
      onNotification: notification => handleNotification(notification)
    }
  }

  Component {
    id: notificationWatcherComponent
    Connections {
      property var targetNotification
      property var targetDataId
      target: targetNotification

      function onSummaryChanged() {
        updateNotificationFromObject(targetDataId)
      }
      function onBodyChanged() {
        updateNotificationFromObject(targetDataId)
      }
      function onAppNameChanged() {
        updateNotificationFromObject(targetDataId)
      }
      function onUrgencyChanged() {
        updateNotificationFromObject(targetDataId)
      }
      function onAppIconChanged() {
        updateNotificationFromObject(targetDataId)
      }
      function onImageChanged() {
        updateNotificationFromObject(targetDataId)
      }
      function onActionsChanged() {
        updateNotificationFromObject(targetDataId)
      }
    }
  }

  function updateNotificationServer() {
    if (notificationServerLoader) {
      notificationServerLoader.destroy()
      notificationServerLoader = null
    }

    if (Settings.isLoaded && Settings.data.notifications.enabled !== false) {
      notificationServerLoader = notificationServerComponent.createObject(root)
    }
  }

  Component.onCompleted: {
    if (Settings.isLoaded) {
      updateNotificationServer()
    }
  }

  Connections {
    target: Settings
    function onSettingsLoaded() {
      updateNotificationServer()
    }
    function onSettingsSaved() {
      updateNotificationServer()
    }
  }

  // Helper function to generate content-based ID for deduplication
  function getContentId(summary, body, appName) {
    return Checksum.sha256(JSON.stringify({
                                            "summary": summary || "",
                                            "body": body || "",
                                            "app": appName || ""
                                          }))
  }

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

    if (root.doNotDisturb || PowerProfileService.noctaliaPerformanceMode)
      return

    // Check if this is a replacement notification
    const existingInternalId = quickshellIdToInternalId[quickshellId]
    if (existingInternalId && activeNotifications[existingInternalId]) {
      updateExistingNotification(existingInternalId, notification, data)
      return
    }

    // Check for duplicate content
    const duplicateId = findDuplicateNotification(data)
    if (duplicateId) {
      removeNotification(duplicateId)
    }

    // Add new notification
    addNewNotification(quickshellId, notification, data)
  }

  function updateExistingNotification(internalId, notification, data) {
    const index = findNotificationIndex(internalId)
    if (index < 0)
      return

    const existing = activeList.get(index)
    const oldTimestamp = existing.timestamp
    const oldProgress = existing.progress

    // Update properties (keeping original timestamp and progress)
    activeList.setProperty(index, "summary", data.summary)
    activeList.setProperty(index, "body", data.body)
    activeList.setProperty(index, "appName", data.appName)
    activeList.setProperty(index, "urgency", data.urgency)
    activeList.setProperty(index, "expireTimeout", data.expireTimeout)
    activeList.setProperty(index, "originalImage", data.originalImage)
    activeList.setProperty(index, "cachedImage", data.cachedImage)
    activeList.setProperty(index, "actionsJson", data.actionsJson)
    activeList.setProperty(index, "timestamp", oldTimestamp)
    activeList.setProperty(index, "progress", oldProgress)

    // Update stored notification object
    const notifData = activeNotifications[internalId]
    notifData.notification = notification
    notification.tracked = true
    notification.closed.connect(() => removeNotification(internalId))

    // Update metadata
    notifData.metadata.urgency = data.urgency
    notifData.metadata.duration = calculateDuration(data)
  }

  function addNewNotification(quickshellId, notification, data) {
    // Map IDs
    quickshellIdToInternalId[quickshellId] = data.id

    // Create watcher
    const watcher = notificationWatcherComponent.createObject(root, {
                                                                "targetNotification": notification,
                                                                "targetDataId": data.id
                                                              })

    // Store notification data
    activeNotifications[data.id] = {
      "notification": notification,
      "watcher": watcher,
      "metadata": {
        "timestamp": data.timestamp.getTime(),
        "duration": calculateDuration(data),
        "urgency": data.urgency,
        "paused": false,
        "pauseTime": 0
      }
    }

    notification.tracked = true
    notification.closed.connect(() => removeNotification(data.id))

    // Add to list
    activeList.insert(0, data)

    // Remove overflow
    while (activeList.count > maxVisible) {
      const last = activeList.get(activeList.count - 1)
      activeNotifications[last.id]?.notification?.dismiss()
      activeList.remove(activeList.count - 1)
      cleanupNotification(last.id)
    }
  }

  function findDuplicateNotification(data) {
    const contentId = getContentId(data.summary, data.body, data.appName)

    for (var i = 0; i < activeList.count; i++) {
      const existing = activeList.get(i)
      const existingContentId = getContentId(existing.summary, existing.body, existing.appName)
      if (existingContentId === contentId) {
        return existing.id
      }
    }
    return null
  }

  function calculateDuration(data) {
    const durations = [Settings.data.notifications?.lowUrgencyDuration * 1000 || 3000, Settings.data.notifications?.normalUrgencyDuration * 1000 || 8000, Settings.data.notifications?.criticalUrgencyDuration * 1000 || 15000]

    if (Settings.data.notifications?.respectExpireTimeout) {
      if (data.expireTimeout === 0)
        return -1 // Never expire
      if (data.expireTimeout > 0)
        return data.expireTimeout
    }

    return durations[data.urgency]
  }

  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 || n.desktopEntry || ""),
      "urgency": n.urgency < 0 || n.urgency > 2 ? 1 : n.urgency,
      "expireTimeout": n.expireTimeout,
      "timestamp": time,
      "progress": 1.0,
      "originalImage": image,
      "cachedImage": imageId ? (Settings.cacheDirImagesNotifications + imageId + ".png") : image,
      "actionsJson": JSON.stringify((n.actions || []).map(a => ({
                                                                  "text": a.text || "Action",
                                                                  "identifier": a.identifier || ""
                                                                })))
    }
  }

  function findNotificationIndex(internalId) {
    for (var i = 0; i < activeList.count; i++) {
      if (activeList.get(i).id === internalId) {
        return i
      }
    }
    return -1
  }

  function updateNotificationFromObject(internalId) {
    const notifData = activeNotifications[internalId]
    if (!notifData)
      return

    const index = findNotificationIndex(internalId)
    if (index < 0)
      return

    const data = createData(notifData.notification)
    const existing = activeList.get(index)

    // Update properties (keeping timestamp and progress)
    activeList.setProperty(index, "summary", data.summary)
    activeList.setProperty(index, "body", data.body)
    activeList.setProperty(index, "appName", data.appName)
    activeList.setProperty(index, "urgency", data.urgency)
    activeList.setProperty(index, "expireTimeout", data.expireTimeout)
    activeList.setProperty(index, "originalImage", data.originalImage)
    activeList.setProperty(index, "cachedImage", data.cachedImage)
    activeList.setProperty(index, "actionsJson", data.actionsJson)

    // Update metadata
    notifData.metadata.urgency = data.urgency
    notifData.metadata.duration = calculateDuration(data)
  }

  function removeNotification(id) {
    const index = findNotificationIndex(id)
    if (index >= 0) {
      activeList.remove(index)
    }
    cleanupNotification(id)
  }

  function cleanupNotification(id) {
    const notifData = activeNotifications[id]
    if (notifData) {
      notifData.watcher?.destroy()
      delete activeNotifications[id]
    }

    // Clean up quickshell ID mapping
    for (const qsId in quickshellIdToInternalId) {
      if (quickshellIdToInternalId[qsId] === id) {
        delete quickshellIdToInternalId[qsId]
        break
      }
    }
  }

  // Progress updates
  Timer {
    interval: 50
    repeat: true
    running: activeList.count > 0
    onTriggered: updateAllProgress()
  }

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

    for (var i = 0; i < activeList.count; i++) {
      const notif = activeList.get(i)
      const notifData = activeNotifications[notif.id]
      if (!notifData)
        continue

      const meta = notifData.metadata
      if (meta.duration === -1 || meta.paused)
        continue

      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) {
        activeList.setProperty(i, "progress", progress)
      }
    }

    if (toRemove.length > 0) {
      animateAndRemove(toRemove[0])
    }
  }

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

    const dest = Settings.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
      }
    }
  }

  // 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 - History
  FileView {
    id: historyFileView
    path: historyFile
    printErrors: false
    onLoaded: loadHistory()
    onLoadFailed: error => {
      if (error === 2)
      writeAdapter()
    }

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

  // Persistence - State
  FileView {
    id: stateFileView
    path: stateFile
    printErrors: false
    onLoaded: loadState()
    onLoadFailed: error => {
      if (error === 2)
      writeAdapter()
    }

    JsonAdapter {
      id: stateAdapter
      property real lastSeenTs: 0
    }
  }

  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.e("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 = Settings.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.e("Notifications", "Load failed:", e)
    }
  }

  function loadState() {
    try {
      root.lastSeenTs = stateAdapter.lastSeenTs || 0

      if (root.lastSeenTs === 0 && Settings.data.notifications && Settings.data.notifications.lastSeenTs) {
        root.lastSeenTs = Settings.data.notifications.lastSeenTs
        saveState()
        Logger.i("Notifications", "Migrated lastSeenTs from settings to state file")
      }
    } catch (e) {
      Logger.e("Notifications", "Load state failed:", e)
    }
  }

  function saveState() {
    try {
      stateAdapter.lastSeenTs = root.lastSeenTs
      stateFileView.writeAdapter()
    } catch (e) {
      Logger.e("Notifications", "Save state failed:", e)
    }
  }

  function updateLastSeenTs() {
    root.lastSeenTs = Time.timestamp * 1000
    saveState()
  }

  // Utility functions
  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 ""
  }

  function pauseTimeout(id) {
    const notifData = activeNotifications[id]
    if (notifData && !notifData.metadata.paused) {
      notifData.metadata.paused = true
      notifData.metadata.pauseTime = Date.now()
    }
  }

  function resumeTimeout(id) {
    const notifData = activeNotifications[id]
    if (notifData && notifData.metadata.paused) {
      notifData.metadata.timestamp += Date.now() - notifData.metadata.pauseTime
      notifData.metadata.paused = false
    }
  }

  // Public API
  function dismissActiveNotification(id) {
    activeNotifications[id]?.notification?.dismiss()
    removeNotification(id)
  }

  function dismissOldestActive() {
    if (activeList.count > 0) {
      const lastNotif = activeList.get(activeList.count - 1)
      dismissActiveNotification(lastNotif.id)
    }
  }

  function dismissAllActive() {
    for (const id in activeNotifications) {
      activeNotifications[id].notification?.dismiss()
      activeNotifications[id].watcher?.destroy()
    }
    activeList.clear()
    activeNotifications = {}
    quickshellIdToInternalId = {}
  }

  function invokeAction(id, actionId) {
    const notifData = activeNotifications[id]
    if (!notifData?.notification?.actions)
      return false

    for (const action of notifData.notification.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 "${Settings.cacheDirImagesNotifications}"*`])
    } catch (e) {
      Logger.e("Notifications", "Failed to clear cache directory:", e)
    }

    historyList.clear()
    saveHistory()
  }

  // Signals
  signal animateAndRemove(string notificationId)

  onDoNotDisturbChanged: {
    ToastService.showNotice(doNotDisturb ? I18n.tr("toast.do-not-disturb.enabled") : I18n.tr("toast.do-not-disturb.disabled"), doNotDisturb ? I18n.tr("toast.do-not-disturb.enabled-desc") : I18n.tr("toast.do-not-disturb.disabled-desc"), doNotDisturb ? "bell-off" : "bell")
  }
}
