import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Commons
import qs.Services.Keyboard

Item {
  id: root

  // ===== PUBLIC INTERFACE (CompositorService compatibility) =====

  property ListModel workspaces: ListModel {}
  property var windows: []
  property int focusedWindowIndex: -1
  property bool initialized: false

  signal workspaceChanged
  signal activeWindowChanged
  signal windowListChanged
  signal displayScalesChanged

  // ===== MANGOSERVICE-SPECIFIC PROPERTIES =====

  property string selectedMonitor: ""
  property string currentLayoutSymbol: ""

  // ===== INTERNAL STATE =====

  QtObject {
    id: internal

    // Tag states per output: Map<OutputName, Array<TagState>>
    // TagState: { id, state, clients, focused }
    property var tagStates: ({})

    // Active tag per output: Map<OutputName, TagID>
    property var activeTags: ({})

    // Focused window metadata from mmsg
    property string focusedTitle: ""
    property string focusedAppId: ""
    property string focusedOutput: ""

    // Window-to-tag persistence: Map<UniqueID, TagID>
    property var windowTagMap: ({})

    // Window-to-output persistence: Map<UniqueID, OutputName>
    property var windowOutputMap: ({})

    // Toplevel-to-ID mapping: Map<ToplevelObject, UniqueID>
    // This assigns each toplevel a stable ID on first encounter
    property var toplevelIdMap: new Map()
    property int windowIdCounter: 0

    // Output name to index mapping for unique workspace IDs
    property var outputIndices: ({})
    property int outputCounter: 0

    // Monitor scales: Map<OutputName, scale>
    property var monitorScales: ({})

    // Keyboard layout
    property string currentKbLayout: ""

    // Stream buffer for mmsg -w output
    property string streamBuffer: ""

    // Window signature for change detection
    property string lastWindowSignature: ""

    // ===== REGEX PATTERNS =====

    readonly property var patterns: QtObject {
      // Detailed tag format: "OUTPUT tag N STATE CLIENTS FOCUSED"
      readonly property var tagDetail: /^(\S+)\s+tag\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$/

                                       // Binary tag format: "OUTPUT tags <occ> <sel> <urg>"
                                       readonly property var tagBinary: /^(\S+)\s+tags\s+([01]+)\s+([01]+)\s+([01]+)$/

                                       // Layout: "OUTPUT layout SYMBOL"
                                       readonly property var layout: /^(\S+)\s+layout\s+(\S+)$/

                                       // Metadata: "OUTPUT title TEXT" or "OUTPUT appid TEXT"
                                       readonly property var metadata: /^(\S+)\s+(title|appid)\s+(.*)$/

                                       // Keyboard layout: "OUTPUT kb_layout NAME"
                                       readonly property var kbLayout: /^(\S+)\s+kb_layout\s+(.*)$/

                                       // Scale: "OUTPUT scale_factor FLOAT"
                                       readonly property var scale: /^(\S+)\s+scale_factor\s+(\d+(?:\.\d+)?)$/

                                       // Selected monitor: "OUTPUT selmon 1"
                                       readonly property var selmon: /^(\S+)\s+selmon\s+1$/
    }

    // ===== PROCESS TAG DATA =====

    function processTagData(output) {
      const lines = output.trim().split('\n');
      const newTagStates = {};
      const newActiveTags = {};
      const detailedOutputTags = {};  // Track which output+tag combinations we've seen in detailed format

      for (let i = 0; i < lines.length; i++) {
        const line = lines[i].trim();
        if (!line) {
          continue;
        }

        // 1. Detailed tag format (preferred - has client counts)
        const detailMatch = line.match(patterns.tagDetail);
        if (detailMatch) {
          const outputName = detailMatch[1];
          const tagId = parseInt(detailMatch[2]);
          const state = parseInt(detailMatch[3]);
          const clients = parseInt(detailMatch[4]);
          const focused = parseInt(detailMatch[5]);

          if (!newTagStates[outputName]) {
            newTagStates[outputName] = [];
          }

          // Track that we've seen this specific tag in detailed format
          const key = `${outputName}-${tagId}`;
          detailedOutputTags[key] = true;

          const isActive = (state & 1) !== 0;
          const isUrgent = (state & 2) !== 0;

          if (isActive) {
            newActiveTags[outputName] = tagId;
          }

          newTagStates[outputName].push({
                                          id: tagId,
                                          state: state,
                                          clients: clients,
                                          focused: focused,
                                          isActive: isActive,
                                          isUrgent: isUrgent
                                        });
          continue;
        }

        // 2. Binary tag format (fallback - only use for tags we haven't seen in detailed format)
        const binaryMatch = line.match(patterns.tagBinary);
        if (binaryMatch) {
          const outputName = binaryMatch[1];

          const occ = binaryMatch[2];  // occupied tags
          const sel = binaryMatch[3];  // selected tags
          const urg = binaryMatch[4];  // urgent tags

          if (!newTagStates[outputName]) {
            newTagStates[outputName] = [];
          }

          // Parse binary strings (right-to-left indexing)
          for (let j = 0; j < occ.length; j++) {
            const tagId = j + 1;
            const charIdx = occ.length - 1 - j;

            // Skip if we already processed this tag in detailed format
            const key = `${outputName}-${tagId}`;
            if (detailedOutputTags[key]) {
              continue;
            }

            const isOccupied = occ[charIdx] === '1';
            const isActive = sel[charIdx] === '1';
            const isUrgent = urg[charIdx] === '1';

            if (isActive) {
              newActiveTags[outputName] = tagId;
            }

            newTagStates[outputName].push({
                                            id: tagId,
                                            state: (isActive ? 1 : 0) | (isUrgent ? 2 : 0),
                                            clients: isOccupied ? 1 : 0  // Approximate: 1 if occupied, 0 otherwise
                                                                  ,
                                            focused: 0,
                                            isActive: isActive,
                                            isUrgent: isUrgent
                                          });
          }
          continue;
        }

        // 3. Metadata (focused window title/appId)
        const metaMatch = line.match(patterns.metadata);
        if (metaMatch) {
          const outputName = metaMatch[1];
          const prop = metaMatch[2];
          const val = metaMatch[3];

          if (prop === "title") {
            internal.focusedTitle = val;
            internal.focusedOutput = outputName;
          } else if (prop === "appid") {
            internal.focusedAppId = val;
          }
          continue;
        }

        // 4. Layout
        const layoutMatch = line.match(patterns.layout);
        if (layoutMatch) {
          root.currentLayoutSymbol = layoutMatch[2];
          continue;
        }

        // 5. Keyboard layout
        const kbMatch = line.match(patterns.kbLayout);
        if (kbMatch) {
          const kbName = kbMatch[2];
          if (kbName !== internal.currentKbLayout) {
            internal.currentKbLayout = kbName;
            if (KeyboardLayoutService) {
              KeyboardLayoutService.setCurrentLayout(kbName);
            }
          }
          continue;
        }

        // 6. Selected monitor
        const selmonMatch = line.match(patterns.selmon);
        if (selmonMatch) {
          root.selectedMonitor = selmonMatch[1];
          continue;
        }
      }

      // Update state - MERGE instead of REPLACE to preserve other outputs' data
      for (const outputName in newTagStates) {
        internal.tagStates[outputName] = newTagStates[outputName];
      }
      for (const outputName in newActiveTags) {
        internal.activeTags[outputName] = newActiveTags[outputName];
      }

      // Rebuild workspaces
      internal.rebuildWorkspaces();

      // Update windows
      internal.updateWindows();
    }

    // ===== REBUILD WORKSPACES =====

    function rebuildWorkspaces() {
      const workspaceList = [];

      // Assign stable indices to new outputs
      for (const outputName in internal.tagStates) {
        if (internal.outputIndices[outputName] === undefined) {
          internal.outputIndices[outputName] = internal.outputCounter++;
          // Logger.d("MangoService", "Assigned index", internal.outputIndices[outputName], "to output", outputName);
        }
      }

      for (const outputName in internal.tagStates) {
        const tags = internal.tagStates[outputName];
        const outputIdx = internal.outputIndices[outputName];

        // Logger.d("MangoService", "Building workspaces for output", outputName, "- index:", outputIdx, "- tags:", tags.length);

        for (let i = 0; i < tags.length; i++) {
          const tag = tags[i];
          const isFocused = tag.isActive && (tag.focused === 1 || outputName === root.selectedMonitor);

          // Create unique workspace ID: outputIndex * 100 + tagId
          // This ensures each output's tags have non-overlapping IDs
          const uniqueId = outputIdx * 100 + tag.id;

          // Logger.d("MangoService", "  Workspace:", outputName, "tag", tag.id, "→ uniqueId:", uniqueId, "active:", tag.isActive, "occupied:", tag.clients > 0);

          workspaceList.push({
                               id: uniqueId,
                               idx: tag.id  // Keep original tag ID for switching
                                    ,
                               name: tag.id.toString(),
                               output: outputName,
                               isActive: tag.isActive,
                               isFocused: isFocused,
                               isUrgent: tag.isUrgent,
                               isOccupied: tag.clients > 0
                             });
        }
      }

      // Sort by tag ID, then by output name
      workspaceList.sort((a, b) => {
                           if (a.id !== b.id) {
                             return a.id - b.id;
                           }
                           return a.output.localeCompare(b.output);
                         });

      // Update ListModel
      root.workspaces.clear();
      for (let k = 0; k < workspaceList.length; k++) {
        root.workspaces.append(workspaceList[k]);
      }

      root.workspaceChanged();
    }

    // ===== UPDATE WINDOWS =====

    function updateWindows() {
      if (!ToplevelManager.toplevels) {
        return;
      }

      const toplevels = ToplevelManager.toplevels.values;
      const windowList = [];
      let newFocusedIdx = -1;
      const currentWindows = new Set();

      // Logger.d("MangoService", "updateWindows: Processing", toplevels.length, "toplevels");

      // Ensure all outputs seen in toplevels have indices assigned
      // This is needed because updateWindows() can be called before processTagData()
      for (let i = 0; i < toplevels.length; i++) {
        const toplevel = toplevels[i];
        if (!toplevel || toplevel.outliers) {
          continue;
        }

        if (toplevel.outputs && toplevel.outputs.length > 0) {
          const outputName = toplevel.outputs[0].name;
          if (internal.outputIndices[outputName] === undefined) {
            internal.outputIndices[outputName] = internal.outputCounter++;
            // Logger.d("MangoService", "updateWindows: Assigned index", internal.outputIndices[outputName], "to new output", outputName);
          }
        }
      }

      for (let i = 0; i < toplevels.length; i++) {
        const toplevel = toplevels[i];
        if (!toplevel || toplevel.outliers) {
          continue;
        }

        const appId = toplevel.appId || toplevel.wayland?.appId || "";
        const title = toplevel.title || toplevel.wayland?.title || "";
        const isFocused = toplevel.activated;

        // Get or assign a unique ID for this toplevel window
        // Use a Map to track toplevel objects and assign stable IDs
        let windowId;
        if (internal.toplevelIdMap.has(toplevel)) {
          windowId = internal.toplevelIdMap.get(toplevel);
        } else {
          windowId = `win-${internal.windowIdCounter++}`;
          internal.toplevelIdMap.set(toplevel, windowId);
        }

        currentWindows.add(windowId);

        // Determine output using priority:
        // 1. Focused window with mmsg metadata (most reliable)
        // 2. Remembered output from previous focus
        // 3. toplevel.outputs (least reliable but still useful)
        let outputName;
        let outputSource = "unknown";

        // Priority 1: Currently focused with mmsg metadata
        if (isFocused && title === internal.focusedTitle && appId === internal.focusedAppId && internal.focusedOutput) {
          outputName = internal.focusedOutput;
          outputSource = "mmsg-focused";
          // Store this for future
          internal.windowOutputMap[windowId] = internal.focusedOutput;
          // Logger.d("MangoService", "    Storing output", internal.focusedOutput, "for", appId);
        } else
          // Priority 2: Previously remembered output (from past focus)
          if (internal.windowOutputMap[windowId]) {
            outputName = internal.windowOutputMap[windowId];
            outputSource = "remembered";
          } else
            // Priority 3: toplevel.outputs
            if (toplevel.outputs && toplevel.outputs.length > 0) {
              outputName = toplevel.outputs[0].name;
              outputSource = "toplevel.outputs";
            } else
              // Fallback: selected monitor
            {
              outputName = root.selectedMonitor || "DP-1";
              outputSource = "fallback";
            }

        // Logger.d("MangoService", "    Output for", appId, "=", outputName, "(source:", outputSource + ")");

        // Determine tag ID (not unique workspace ID yet)
        let tagId = null;  // null means unknown
        let tagSource = "unknown";

        if (isFocused && title === internal.focusedTitle && appId === internal.focusedAppId) {
          // This is the focused window - assign to active tag and remember it
          tagId = internal.activeTags[outputName] || 1;
          internal.windowTagMap[windowId] = tagId;
          tagSource = "focused";
        } else if (internal.windowTagMap[windowId] !== undefined) {
          // Window has a known tag - use it
          tagId = internal.windowTagMap[windowId];
          tagSource = "remembered";
        } else {
          // Unknown window - skip it (don't show in taskbar until we know its tag)
          continue;
        }

        // Convert to unique workspace ID using output index
        // We ensure outputIndices is populated above, so this should never be undefined
        const outputIdx = internal.outputIndices[outputName];
        if (outputIdx === undefined) {
          Logger.e("MangoService", "ERROR: No output index for", outputName, "- this should not happen!");
          continue;  // Skip this window
        }
        const workspaceId = outputIdx * 100 + tagId;

        // Logger.d("MangoService", "  Window:", appId, "on", outputName, "tag", tagId, "→ workspaceId:", workspaceId, "outputIdx:", outputIdx);

        const windowObj = {
          id: `${outputName}:${appId}:${title}:${i}`  // Unique ID using index
              ,
          title: title,
          appId: appId,
          class: appId,
          workspaceId: workspaceId,
          isFocused: isFocused,
          output: outputName,
          handle: toplevel,
          fullscreen: toplevel.fullscreen || false,
          floating: toplevel.maximized === false && toplevel.fullscreen === false
        };

        windowList.push(windowObj);

        if (isFocused) {
          newFocusedIdx = windowList.length - 1;
        }
      }

      // Clean up stale window tracking (keep map size reasonable)
      if (Object.keys(internal.windowTagMap).length > toplevels.length + 20) {
        const newTagMap = {};
        const newOutputMap = {};
        for (const windowId of currentWindows) {
          if (internal.windowTagMap[windowId] !== undefined) {
            newTagMap[windowId] = internal.windowTagMap[windowId];
          }
          if (internal.windowOutputMap[windowId] !== undefined) {
            newOutputMap[windowId] = internal.windowOutputMap[windowId];
          }
        }
        internal.windowTagMap = newTagMap;
        internal.windowOutputMap = newOutputMap;
      }

      // Check if window list changed
      const signature = JSON.stringify(windowList.map(w => w.id + w.workspaceId + w.isFocused));
      if (signature !== internal.lastWindowSignature) {
        internal.lastWindowSignature = signature;

        // Logger.d("MangoService", "===== FINAL WINDOW LIST (", windowList.length, "windows) =====");
        // for (let i = 0; i < windowList.length; i++) {
        //   const w = windowList[i];
        //   Logger.d("MangoService", "  [" + i + "]", w.appId, "output:", w.output, "workspaceId:", w.workspaceId);
        // }

        root.windows = windowList;
        root.windowListChanged();
      }

      // Update focused window index
      if (newFocusedIdx !== root.focusedWindowIndex) {
        root.focusedWindowIndex = newFocusedIdx;
        root.activeWindowChanged();
      }
    }

    // ===== PROCESS SCALES =====

    function processScales(output) {
      const lines = output.trim().split('\n');

      for (let i = 0; i < lines.length; i++) {
        const line = lines[i].trim();
        const match = line.match(patterns.scale);

        if (match) {
          const outputName = match[1];
          const scale = parseFloat(match[2]);
          internal.monitorScales[outputName] = scale;
        }
      }

      // Notify CompositorService
      const scalesMap = {};
      for (const name in internal.monitorScales) {
        scalesMap[name] = {
          name: name,
          scale: internal.monitorScales[name] || 1.0,
          width: 0,
          height: 0,
          x: 0,
          y: 0
        };
      }

      if (CompositorService && CompositorService.onDisplayScalesUpdated) {
        CompositorService.onDisplayScalesUpdated(scalesMap);
      }

      root.displayScalesChanged();
    }
  }

  // ===== PROCESSES =====

  // Event stream (mmsg -w)
  property QtObject _eventStream: Process {
    id: eventStream
    running: false
    command: ["mmsg", "-w"]

    stdout: SplitParser {
      onRead: line => {
                internal.streamBuffer += line + "\n";

                // Process frame when we get a binary tag line (signals end of frame)
                if (line.match(internal.patterns.tagBinary)) {
                  internal.processTagData(internal.streamBuffer);
                  internal.streamBuffer = "";
                }
              }
    }

    onExited: code => {
                if (code !== 0) {
                  restartTimer.start();
                }
              }
  }

  property QtObject _restartTimer: Timer {
    id: restartTimer
    interval: 1000
    onTriggered: {
      if (root.initialized) {
        eventStream.running = true;
      }
    }
  }

  // Initial tag query (mmsg -g -t)
  property QtObject _initialQuery: Process {
    id: initialQuery
    command: ["mmsg", "-g", "-t"]

    stdout: SplitParser {
      onRead: line => {
                internal.streamBuffer += line + "\n";
              }
    }

    onExited: code => {
                if (code === 0) {
                  internal.processTagData(internal.streamBuffer);
                  internal.streamBuffer = "";
                }
              }
  }

  // Scale query (mmsg -g -A)
  property QtObject _scaleQuery: Process {
    id: scaleQuery
    command: ["mmsg", "-g", "-A"]

    property string buffer: ""

    stdout: SplitParser {
      onRead: line => {
                scaleQuery.buffer += line + "\n";
              }
    }

    onExited: code => {
                if (code === 0) {
                  internal.processScales(scaleQuery.buffer);
                  scaleQuery.buffer = "";
                }
              }
  }

  // ===== TOPLEVEL MANAGER CONNECTION =====

  property QtObject _toplevelConnection: Connections {
    target: ToplevelManager.toplevels

    function onValuesChanged() {
      internal.updateWindows();
    }
  }

  // Periodic update (safety net for missed events)
  property QtObject _updateTimer: Timer {
    interval: 500
    running: root.initialized
    repeat: true
    onTriggered: internal.updateWindows()
  }

  // ===== PUBLIC FUNCTIONS =====

  function initialize() {
    if (initialized) {
      return;
    }

    Logger.i("MangoService", "Initializing MangoWC/DWL compositor integration");

    // Start queries
    scaleQuery.running = true;
    initialQuery.running = true;
    eventStream.running = true;

    // Query monitors
    Quickshell.execDetached(["mmsg", "-g", "-o"]);

    initialized = true;
  }

  function queryDisplayScales() {
    scaleQuery.running = true;
  }

  function switchToWorkspace(workspace) {
    const tagId = workspace.idx || workspace.id || 1;
    const output = workspace.output || root.selectedMonitor || "";

    const cmd = ["mmsg", "-s", "-t", tagId.toString()];

    // Add output if multi-monitor
    if (output && Object.keys(internal.monitorScales).length > 1) {
      cmd.push("-o", output);
    }

    Quickshell.execDetached(cmd);
  }

  function focusWindow(window) {
    if (window && window.handle) {
      window.handle.activate();
    } else if (window.workspaceId) {
      switchToWorkspace({
                          id: window.workspaceId,
                          output: window.output
                        });
    }
  }

  function closeWindow(window) {
    if (window && window.handle) {
      window.handle.close();
    } else {
      Quickshell.execDetached(["mmsg", "-s", "-d", "killclient"]);
    }
  }

  function logout() {
    Quickshell.execDetached(["mmsg", "-s", "-q"]);
  }
}
