import {
  App,
  ipcMain,
  IpcMainEvent,
  Menu,
  nativeImage,
  nativeTheme,
  Tray,
} from 'electron';
import { log } from 'electron-log/main';
import { IPC } from './shared-with-frontend/ipc-events.const';
import { getDistChannel } from './shared-with-frontend/get-dist-channel';
import { getIsTrayShowCurrentTask, getIsTrayShowCurrentCountdown } from './shared-state';
import { TaskCopy } from '../src/app/features/tasks/task.model';
import { release } from 'os';
import {
  initTaskWidgetSettingsListener,
  updateTaskWidgetTask,
} from './task-widget/task-widget';
import { getWin } from './main-window';

type IndicatorConfig = {
  showApp: () => void;
  quitApp: () => void;
  app: App;
  ICONS_FOLDER: string;
  forceDarkTray: boolean;
};

let tray: Tray | undefined;
let indicatorConfig: IndicatorConfig | undefined;
let _showApp: () => void;
let _quitApp: () => void;
let _todayTasks: {
  id: string;
  title: string;
  timeEstimate: number;
  timeSpent: number;
}[] = [];
let _isRunning: boolean = false;
let _currentTaskId: string | null = null;
let DIR: string;
let shouldUseDarkColors: boolean;

// Caching variables for preventing Linux tray menu flickering
let _lastMsg: string | undefined;
let _lastTrayMsg: string | undefined;
let _lastIsRunning: boolean | undefined;
let _lastCurrentTaskId: string | null | undefined;
let _lastTodayTasksStr: string | undefined;

let _lastCurrentTask: any;
let _lastIsPomodoroEnabled: boolean;
let _lastCurrentPomodoroSessionTime: number;
let _lastIsFocusModeEnabled: boolean;
let _lastCurrentFocusSessionTime: number;
let _lastFocusModeMode: string;
let _isAppListenersInitialized = false;
let _isListenersInitialized = false;

const IS_MAC = process.platform === 'darwin';
const IS_LINUX = process.platform === 'linux';
const IS_WINDOWS = process.platform === 'win32';

// Stable GUID for the NSIS (installer) build only.
// Per Electron's Tray docs, a tray-icon GUID binds to the code-signing
// signature only when that signature carries an organization in its subject;
// otherwise it binds to the executable's full path, and changing the path
// breaks tray-icon creation until a new GUID is used. The GitHub NSIS build is
// signed and installs to a fixed path, so its GUID stays valid. The Store
// (MSIX) and portable/scoop builds run from versioned directories whose path
// changes on every update, so the GUID goes stale and Shell_NotifyIcon(NIM_ADD)
// fails silently (Electron raises no JS error) — leaving an invisible tray icon
// and an unreachable window (#7282). Those builds therefore create the tray
// without a GUID, so Windows identifies it by window handle and it stays visible.
// WARNING: This GUID must never change once deployed; Windows would treat a new
// value as a new icon and reset it to the overflow area.
// Retired GUIDs (shipped in v18.10.0, now GUID-less — never reuse for a new icon):
//   portable f7c06d50-4d3e-4f8d-b9a0-2c8e7f5a1b3d, store 19b9d3fe-aa50-4792-917e-60ada97f3088
// See https://www.electronjs.org/docs/latest/api/tray (guid) and
//   https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-notifyicondataa
const WINDOWS_TRAY_NSIS_GUID = 'a2512177-8bee-4b70-a0a8-f3d18e0eab90';

const getWindowsTrayGuid = (): string | undefined =>
  getDistChannel() === 'win-nsis' ? WINDOWS_TRAY_NSIS_GUID : undefined;

export const initIndicator = ({
  showApp,
  quitApp,
  app,
  ICONS_FOLDER,
  forceDarkTray,
}: {
  showApp: () => void;
  quitApp: () => void;
  app: App;
  ICONS_FOLDER: string;
  forceDarkTray: boolean;
}): Tray | undefined => {
  indicatorConfig = {
    showApp,
    quitApp,
    app,
    ICONS_FOLDER,
    forceDarkTray,
  };
  DIR = ICONS_FOLDER + 'indicator/';
  // On macOS we always load the black (`-l`) icons and mark them as template
  // images in getTrayImage(); the system auto-inverts them for the current
  // menu bar appearance, so the static dark/light choice doesn't apply.
  shouldUseDarkColors =
    !IS_MAC &&
    (forceDarkTray ||
      IS_LINUX ||
      (IS_WINDOWS && !isWindows11()) ||
      nativeTheme.shouldUseDarkColors);

  _showApp = showApp;
  _quitApp = quitApp;

  initAppListeners(app);
  initListeners();

  return ensureIndicator();
};

export const ensureIndicator = (): Tray | undefined => {
  if (!indicatorConfig) {
    return undefined;
  }

  if (tray) {
    return tray;
  }

  tray = createTray();
  syncTray(tray);

  return tray;
};

export const refreshIndicator = (): void => {
  if (tray) {
    syncTray(tray);
  }
};

const createTray = (): Tray => {
  const suf = shouldUseDarkColors ? '-d.png' : '-l.png';
  const trayIconPath = DIR + `stopped${suf}`;
  const trayIcon = getTrayImage(trayIconPath);
  let nextTray: Tray;
  if (IS_WINDOWS) {
    const guid = getWindowsTrayGuid();
    if (guid) {
      try {
        nextTray = new Tray(trayIcon, guid);
        log('Tray created on Windows with GUID:', guid);
      } catch (e) {
        // Log only the message, not the full error: log history is exportable
        // and the error/stack can embed the install path (e.g. C:\Users\<name>\).
        log(
          'Tray creation with GUID failed, retrying without GUID:',
          e instanceof Error ? e.message : e,
        );
        nextTray = new Tray(trayIcon);
        log('Tray created on Windows without GUID');
      }
    } else {
      nextTray = new Tray(trayIcon);
      log('Tray created on Windows without GUID (versioned install path)');
    }
  } else {
    nextTray = new Tray(trayIcon);
  }
  nextTray.setContextMenu(createContextMenu());

  nextTray.on('click', () => {
    indicatorConfig?.showApp();
  });

  return nextTray;
};

// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function initAppListeners(app: App): void {
  if (_isAppListenersInitialized) {
    return;
  }

  _isAppListenersInitialized = true;
  app.on('before-quit', () => {
    if (tray) {
      destroyTray();
    }
  });
}

// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function initListeners(): void {
  if (_isListenersInitialized) {
    return;
  }
  _isListenersInitialized = true;

  // Task widget settings are per-instance (not synced) — handled via a
  // dedicated IPC channel in task-widget.ts.
  initTaskWidgetSettingsListener();

  ipcMain.on(IPC.SET_PROGRESS_BAR, (ev: IpcMainEvent, { progress }) => {
    if (_isRunning && tray) {
      setTrayIcon(tray, getRunningIconPath(progress));
    }

    // Also update the context menu and tray title during the progress bar tick
    // This perfectly synchronizes the text "blinking" with the pie chart animation
    if (_lastCurrentTask && tray) {
      const isTrayShowCurrentTask = getIsTrayShowCurrentTask();
      const isTrayShowCurrentCountdown = getIsTrayShowCurrentCountdown();

      const menuMsg = createIndicatorMessage(
        _lastCurrentTask,
        _lastIsPomodoroEnabled || false,
        _lastCurrentPomodoroSessionTime || 0,
        true, // always show countdown in menu
        _lastIsFocusModeEnabled || false,
        _lastCurrentFocusSessionTime || 0,
        _lastFocusModeMode,
      );

      const trayMsg = createIndicatorMessage(
        _lastCurrentTask,
        _lastIsPomodoroEnabled || false,
        _lastCurrentPomodoroSessionTime || 0,
        isTrayShowCurrentCountdown,
        _lastIsFocusModeEnabled || false,
        _lastCurrentFocusSessionTime || 0,
        _lastFocusModeMode,
      );

      const todayTasksStr = JSON.stringify(
        (_todayTasks || []).map((t) => ({ id: t.id, title: t.title })),
      );

      const isMenuChanged =
        menuMsg !== _lastMsg ||
        _isRunning !== _lastIsRunning ||
        _currentTaskId !== _lastCurrentTaskId ||
        todayTasksStr !== _lastTodayTasksStr;

      if (isMenuChanged) {
        tray.setContextMenu(createContextMenu(menuMsg));
        _lastMsg = menuMsg;
        _lastIsRunning = _isRunning;
        _lastCurrentTaskId = _currentTaskId;
        _lastTodayTasksStr = todayTasksStr;
      }

      if (_lastTrayMsg !== trayMsg) {
        if (isTrayShowCurrentTask) {
          tray.setTitle(trayMsg);
          if (!IS_MAC) tray.setToolTip(trayMsg);
        } else {
          tray.setTitle('');
          if (!IS_MAC) tray.setToolTip('');
        }
        _lastTrayMsg = trayMsg;
      }
    }
  });

  ipcMain.on(
    IPC.CURRENT_TASK_UPDATED,
    (
      ev: IpcMainEvent,
      currentTask: any,
      isPomodoroEnabled: boolean,
      currentPomodoroSessionTime: number,
      isFocusModeEnabled: boolean,
      currentFocusSessionTime: number,
      focusModeMode: string,
    ) => {
      _isRunning = !!currentTask;
      _currentTaskId = currentTask ? currentTask.id : null;

      // Store current task details so SET_PROGRESS_BAR can re-render text
      _lastCurrentTask = currentTask;
      _lastIsPomodoroEnabled = isPomodoroEnabled;
      _lastCurrentPomodoroSessionTime = currentPomodoroSessionTime;
      _lastIsFocusModeEnabled = isFocusModeEnabled;
      _lastCurrentFocusSessionTime = currentFocusSessionTime;
      _lastFocusModeMode = focusModeMode;

      updateTaskWidgetTask(
        currentTask,
        isPomodoroEnabled,
        currentPomodoroSessionTime,
        isFocusModeEnabled || false,
        currentFocusSessionTime || 0,
      );

      const isTrayShowCurrentTask = getIsTrayShowCurrentTask();
      const isTrayShowCurrentCountdown = getIsTrayShowCurrentCountdown();

      const menuMsg = currentTask
        ? createIndicatorMessage(
            currentTask,
            isPomodoroEnabled,
            currentPomodoroSessionTime,
            true, // isTrayShowCurrentCountdown: true for context menu to always show timer
            isFocusModeEnabled || false,
            currentFocusSessionTime || 0,
            focusModeMode,
          )
        : '';

      const trayMsg = currentTask
        ? createIndicatorMessage(
            currentTask,
            isPomodoroEnabled,
            currentPomodoroSessionTime,
            isTrayShowCurrentCountdown,
            isFocusModeEnabled || false,
            currentFocusSessionTime || 0,
            focusModeMode,
          )
        : '';

      if (tray) {
        // tray handling
        const todayTasksStr = JSON.stringify(
          (_todayTasks || []).map((t) => ({ id: t.id, title: t.title })),
        );
        const isMenuChanged =
          menuMsg !== _lastMsg ||
          _isRunning !== _lastIsRunning ||
          _currentTaskId !== _lastCurrentTaskId ||
          todayTasksStr !== _lastTodayTasksStr;

        if (isMenuChanged) {
          tray.setContextMenu(createContextMenu(menuMsg));
          _lastMsg = menuMsg;
          _lastIsRunning = _isRunning;
          _lastCurrentTaskId = _currentTaskId;
          _lastTodayTasksStr = todayTasksStr;
        }

        if (_lastTrayMsg !== trayMsg) {
          if (currentTask && currentTask.title) {
            if (isTrayShowCurrentTask) {
              tray.setTitle(trayMsg);
              if (!IS_MAC) {
                // NOTE apparently this has no effect for gnome
                tray.setToolTip(trayMsg);
              }
            } else {
              tray.setTitle('');
              if (!IS_MAC) {
                tray.setToolTip('');
              }
            }
          } else {
            tray.setTitle('');
            if (!IS_MAC) {
              tray.setToolTip('');
            }
            const suf = shouldUseDarkColors ? '-d.png' : '-l.png';
            setTrayIcon(tray, DIR + `stopped${suf}`);
          }
          _lastTrayMsg = trayMsg;
        }

        // Set running icon immediately so it doesn't wait for the next tracking interval tick
        if (currentTask && currentTask.title && !_lastIsFocusModeEnabled) {
          const progress = currentTask.timeEstimate
            ? currentTask.timeSpent / currentTask.timeEstimate
            : undefined;
          setTrayIcon(tray, getRunningIconPath(progress));
        }
      }
    },
  );

  ipcMain.on(IPC.TODAY_TASKS_UPDATED, (ev: IpcMainEvent, tasks: any[]) => {
    _todayTasks = tasks;
    if (tray) {
      const todayTasksStr = JSON.stringify(
        (_todayTasks || []).map((t) => ({ id: t.id, title: t.title })),
      );
      if (todayTasksStr !== _lastTodayTasksStr) {
        tray.setContextMenu(createContextMenu(_lastMsg));
        _lastTodayTasksStr = todayTasksStr;
      }
    }
  });

  // ipcMain.on(IPC.POMODORO_UPDATE, (ev, params) => {
  // const isOnBreak = params.isOnBreak;
  // const currentSessionTime = params.currentSessionTime;
  // const currentSessionInitialTime = params.currentSessionInitialTime;
  // if (isGnomeShellExtInstalled) {
  //  dbus.updatePomodoro(isOnBreak, currentSessionTime, currentSessionInitialTime);
  // }
  // });
}

const syncTray = (tr: Tray): void => {
  const menuMsg = _lastCurrentTask
    ? createIndicatorMessage(
        _lastCurrentTask,
        _lastIsPomodoroEnabled || false,
        _lastCurrentPomodoroSessionTime || 0,
        true,
        _lastIsFocusModeEnabled || false,
        _lastCurrentFocusSessionTime || 0,
        _lastFocusModeMode,
      )
    : _lastMsg;

  tr.setContextMenu(createContextMenu(menuMsg));

  const isTrayShowCurrentTask = getIsTrayShowCurrentTask();
  const isTrayShowCurrentCountdown = getIsTrayShowCurrentCountdown();
  const trayMsg = _lastCurrentTask
    ? createIndicatorMessage(
        _lastCurrentTask,
        _lastIsPomodoroEnabled || false,
        _lastCurrentPomodoroSessionTime || 0,
        isTrayShowCurrentCountdown,
        _lastIsFocusModeEnabled || false,
        _lastCurrentFocusSessionTime || 0,
        _lastFocusModeMode,
      )
    : '';

  if (_lastCurrentTask?.title && isTrayShowCurrentTask) {
    tr.setTitle(trayMsg);
    if (!IS_MAC) {
      tr.setToolTip(trayMsg);
    }
  } else {
    tr.setTitle('');
    if (!IS_MAC) {
      tr.setToolTip('');
    }
  }
  _lastTrayMsg = trayMsg;

  if (_lastCurrentTask?.title && !_lastIsFocusModeEnabled) {
    const progress = _lastCurrentTask.timeEstimate
      ? _lastCurrentTask.timeSpent / _lastCurrentTask.timeEstimate
      : undefined;
    setTrayIcon(tr, getRunningIconPath(progress));
  } else {
    const suf = shouldUseDarkColors ? '-d.png' : '-l.png';
    setTrayIcon(tr, DIR + `stopped${suf}`);
  }
};

const destroyTray = (): void => {
  tray?.destroy();
  tray = undefined;
  curIco = undefined;
};

// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function createIndicatorMessage(
  task: TaskCopy,
  isPomodoroEnabled: boolean,
  currentPomodoroSessionTime: number,
  isTrayShowCurrentCountdown: boolean,
  isFocusModeEnabled: boolean,
  currentFocusSessionTime: number,
  focusModeMode: string | undefined,
): string {
  if (task && task.title) {
    let timeStr = '';

    if (isTrayShowCurrentCountdown) {
      // Priority 1: Focus mode with countdown/pomodoro (show countdown)
      if (isFocusModeEnabled && focusModeMode && focusModeMode !== 'Flowtime') {
        timeStr = getProgressMessage(currentFocusSessionTime);
        return timeStr;
      }

      // Priority 2: Flowtime mode (show nothing or task estimate)
      if (isFocusModeEnabled && focusModeMode === 'Flowtime') {
        if (task.timeEstimate) {
          const restOfTime = Math.max(task.timeEstimate - task.timeSpent, 0);
          timeStr = getProgressMessage(task.timeSpent, restOfTime, '-');
          return timeStr;
        }
        return getProgressMessage(task.timeSpent);
      }

      // Priority 3: Legacy pomodoro (if still used)
      if (isPomodoroEnabled) {
        timeStr = getProgressMessage(currentPomodoroSessionTime);
        return timeStr;
      }

      // Priority 4: Normal task time (no focus mode)
      if (task.timeEstimate) {
        let restOfTime = task.timeEstimate - task.timeSpent;
        const prefix = restOfTime >= 0 ? '-' : '+';
        restOfTime = Math.abs(restOfTime);
        timeStr = getProgressMessage(task.timeSpent, restOfTime, prefix);
      } else if (task.timeSpent) {
        timeStr = getProgressMessage(task.timeSpent);
      }
      return timeStr;
    }

    return task.title;
  }

  return '';
}

// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function createContextMenu(msg?: string): Menu {
  const template: any[] = [];

  // Either show the time string (if task is running) or "Super Productivity"
  if (msg) {
    template.push({ label: msg, enabled: false });
    template.push({ type: 'separator' });
  }

  if (_todayTasks && _todayTasks.length > 0) {
    _todayTasks.forEach((t) => {
      template.push({
        label: t.title.length > 40 ? t.title.substring(0, 37) + '...' : t.title,
        type: 'radio',
        checked: _currentTaskId === t.id && _isRunning,
        click: () => {
          const mainWindow = getWin();
          if (mainWindow) {
            if (_currentTaskId === t.id) {
              // Clicked the active task again -> toggle start/pause
              mainWindow.webContents.send(IPC.TASK_TOGGLE_START);
            } else {
              // Clicked a different task -> switch to it
              mainWindow.webContents.send(IPC.SWITCH_TASK, t.id);
            }
          }
        },
      });
    });
    template.push({ type: 'separator' });
  }

  template.push({ label: 'Show App', click: _showApp });
  template.push({ label: 'Quit', click: _quitApp });

  return Menu.buildFromTemplate(template);
}

// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function getRunningIconPath(progress?: number): string {
  const suf = shouldUseDarkColors ? '-d' : '-l';
  // Linux StatusNotifierItem hosts redraw the whole tray item on every
  // Tray.setImage(); cycling through the 16 progress-animation frames makes the
  // icon visibly flicker between a full and a partial ring (#4905). Show a single
  // static running icon there instead — elapsed/remaining time still shows in the
  // tray context-menu label (and the title/OS taskbar where the host supports it).
  if (!IS_LINUX && typeof progress === 'number' && progress > 0 && isFinite(progress)) {
    const f = Math.min(Math.round(progress * 15), 15);
    return DIR + `running-anim${suf}/${f || 0}.png`;
  }
  return DIR + `running${suf}.png`;
}

let curIco: string | undefined;

// GNOME AppIndicator can fall back to a generic "three dots" icon for
// sandboxed Electron apps when given only a file path. Passing a NativeImage
// keeps the actual pixel data attached to the tray item.
// On macOS we also need a NativeImage so we can mark it as a template image —
// the system then inverts it for the current menu bar appearance (light/dark,
// highlight state, accessibility), so it stays visible on any background.
const getTrayImage = (icoPath: string): string | Electron.NativeImage => {
  if (!IS_LINUX && !IS_MAC) {
    return icoPath;
  }

  const image = nativeImage.createFromPath(icoPath);
  if (image.isEmpty()) {
    log('Tray icon NativeImage is empty, falling back to icon path:', icoPath);
    return icoPath;
  }

  if (IS_MAC) {
    image.setTemplateImage(true);
  }

  return image;
};

// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function setTrayIcon(tr: Tray, icoPath: string): void {
  if (icoPath !== curIco) {
    curIco = icoPath;
    tr.setImage(getTrayImage(icoPath));
  }
}

// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function isWindows11(): boolean {
  if (!IS_WINDOWS) {
    return false;
  }

  const v = release();
  let isWin11 = false;
  if (v.startsWith('11.')) {
    isWin11 = true;
  } else if (v.startsWith('10.')) {
    const ss = v.split('.');
    isWin11 = ss.length > 2 && parseInt(ss[2]) >= 22000 ? true : false;
  }

  return isWin11;
}

// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function getProgressMessage(
  elapsedMs: number,
  diffMs?: number,
  prefix: string = '',
): string {
  const formatTime = (ms: number): string => {
    const numValue = Number(ms) || 0;
    const hours = Math.floor(numValue / 3600000);
    const minutes = Math.floor((numValue - hours * 3600000) / 60000); // eslint-disable-line no-mixed-operators

    const parsed = (hours > 0 ? hours + 'h ' : '') + (minutes > 0 ? minutes + 'm ' : '');

    return parsed.trim() || '0m';
  };

  const elapsedStr = formatTime(elapsedMs);

  if (diffMs !== undefined) {
    // If the difference rounds to exactly 0 minutes, avoid "-0m"
    const diffMinutes = Math.floor(diffMs / 60000);
    if (diffMinutes === 0) {
      return `${elapsedStr}`;
    }
    const diffStr = formatTime(diffMs);
    return `${elapsedStr} (${prefix}${diffStr})`;
  }

  return elapsedStr;
}
