function Mutex() {
  let current = Promise.resolve();
  this.lock = () => {
    let _resolve;
    const p = new Promise((resolve) => {
      _resolve = () => resolve();
    });
    // Caller gets a promise that resolves when the current outstanding
    // lock resolves
    const rv = current.then(() => _resolve);
    // Don't allow the next request until the new promise is done
    current = p;
    // Return the new promise
    return rv;
  };
}

//V3 - TALK TO SERVICE WORKER ON UNAVILABLE APIS

if (!chrome.storage)
  chrome.storage = {
    sync: {
      get: (o, n) => {
        chrome.runtime.sendMessage({ type: "STORAGE_GET", data: o }, (d) => n(d));
      },
      set: (o, n) => {
        chrome.runtime.sendMessage({ type: "STORAGE_SET", data: o }, (d) => n(d));
      },
    },
  };

if (!chrome.windows)
  chrome.windows = {
    create: (o, n) => chrome.runtime.sendMessage({ type: "CREATE_WINDOW", data: o }, (d) => n(d)),
    update: (o, o1, n) =>
      chrome.runtime.sendMessage({ type: "UPDATE_WINDOW", data: [o, o1] }, (d) => n(d)),
  };

if (!chrome.notifications)
  chrome.notifications = {
    // מעביר את ה-notificationId חזרה ל-callback (ה-SW מחזיר אותו) → background ממפה id→ערים
    create: (o, cb = () => {}) =>
      chrome.runtime.sendMessage({ type: "NOTIFICATION_CREATE", data: o }, (id) => cb(id)),
  };

//V3 - TALK TO SERVICE WORKER ON UNAVILABLE APIS
class Preferences {
  static IMPORTANT_CITIES_IDS = [10000000,1047,155,716,1044,4,735,795,320,982,1809,1474,1042,412,371,754,1053,1866,755,744,484,447,88,1442,1172,810,1081,1736,701,1008,1298,886,869,1259,566,717,1137,73,1462,840,793,848,1417,1830,494,1665,1158,1669,454,1523,38,715,1499,1795,1177,1186,1271,206,726,1380,1423,1200,1112,143,1171,785,900,1477,1029,907,1518,737,1337,1351,1347,385,258,1304,1637,846,781,1486,587,1797,802,849,1865,1582,1307,1652,468,455,433,432,1749,124,141,1748,824,834,983,984,985,989,1302,1292,1336,1270,496,527,391,415,913,920,1827,1838,730,745,722,724,742,783,1003,1785,248,293,1878,1932,2005,2006,1922,191,511]
  static async getSelectedSound() {
    return new Promise((resolve, reject) =>
      chrome.storage.sync.get({ selectedSound: "bell2" }, (value) => resolve(value.selectedSound))
    );
  }
  static saveSelectedSound(soundID, callback = () => {}) {
    chrome.storage.sync.set({ selectedSound: soundID }, callback);
  }

  static async getSelectedCities() {
    return new Promise((resolve, reject) =>
      chrome.storage.sync.get({ selectedCities: "[]" }, (value) =>
        resolve(JSON.parse(value.selectedCities))
      )
    );
  }
  static saveSelectedCities(selectionCitiesIDs, callback = () => {}) {
    chrome.storage.sync.set({ selectedCities: JSON.stringify(selectionCitiesIDs) }, callback);
  }

  static async getSelectedAreas() {
    return new Promise((resolve, reject) =>
      chrome.storage.sync.get({ selectedAreas: "[]" }, (value) =>
        resolve(JSON.parse(value.selectedAreas))
      )
    );
  }
  static saveSelectedAreas(selectionAreasIDs, callback = () => {}) {
    chrome.storage.sync.set({ selectedAreas: JSON.stringify(selectionAreasIDs) }, callback);
  }

  static async getSelectedEventTypes() {
    return new Promise((resolve) =>
      chrome.storage.sync.get({ selectedEventTypes: "[]" }, (value) =>
        resolve(JSON.parse(value.selectedEventTypes))
      )
    );
  }
  static setSelectedEventTypes(value, callback = () => {}) {
    chrome.storage.sync.set({ selectedEventTypes: JSON.stringify(value) }, callback);
  }

  static async getSelectedDesktop() {
    return new Promise((resolve, reject) =>
      chrome.storage.sync.get({ desktopNotifications: false }, (value) =>
        resolve(value.desktopNotifications)
      )
    );
  }
  static saveSelectedDesktop(value, callback = () => {}) {
    chrome.storage.sync.set({ desktopNotifications: value }, callback);
  }


  static async getSilentNotSelected() {
    return new Promise((resolve, reject) =>
      chrome.storage.sync.get({ silentNotSelected: false }, (value) =>
        resolve(value.silentNotSelected)
      )
    );
  }
  static saveSilentNotSelected(value, callback = () => {}) {
    chrome.storage.sync.set({ silentNotSelected: value }, callback);
  }

  // השתקת התרעות בטווח שעות (quiet hours): { enabled, from, to } — from/to בפורמט "HH:MM".
  // ההשתקה משתיקה צליל בלבד — ההתרעה (חלון/התראת מערכת) עדיין מוצגת (כמו "התרעות שקטות").
  static async getQuietHours() {
    return new Promise((resolve) =>
      chrome.storage.sync.get(
        { quietHours: JSON.stringify({ enabled: false, from: "22:00", to: "07:00" }) },
        (value) => {
          try {
            resolve(JSON.parse(value.quietHours));
          } catch {
            resolve({ enabled: false, from: "22:00", to: "07:00" });
          }
        }
      )
    );
  }
  static saveQuietHours(value, callback = () => {}) {
    chrome.storage.sync.set({ quietHours: JSON.stringify(value) }, callback);
  }

  // האם הזמן הנוכחי נמצא בתוך טווח ההשתקה. תומך בטווח שחוצה חצות (למשל 22:00→07:00).
  static async isWithinQuietHours(now = new Date()) {
    const q = await Preferences.getQuietHours();
    if (!q || !q.enabled) return false;
    const toMinutes = (hhmm) => {
      const [h, m] = String(hhmm).split(":").map(Number);
      return (h || 0) * 60 + (m || 0);
    };
    const from = toMinutes(q.from);
    const to = toMinutes(q.to);
    const cur = now.getHours() * 60 + now.getMinutes();
    if (from === to) return false; // טווח ריק — אין השתקה
    // טווח רגיל (08:00→17:00): cur בין from ל-to. טווח שחוצה חצות (22:00→07:00): cur אחרי from או לפני to.
    return from < to ? cur >= from && cur < to : cur >= from || cur < to;
  }

  static async getSelectedBackgroundHidePopup() {
    return new Promise((resolve, reject) =>
      chrome.storage.sync.get({ backgroundHidePopup: false }, (value) =>
        resolve(value.backgroundHidePopup)
      )
    );
  }
  static saveSelectedBackgroundHidePopup(value, callback = () => {}) {
    chrome.storage.sync.set({ backgroundHidePopup: value }, callback);
  }

  static async getPreventFrequentPopupFocus() {
    return new Promise((resolve) =>
      chrome.storage.sync.get({ preventFrequentPopupFocus: true }, (value) =>
        resolve(value.preventFrequentPopupFocus)
      )
    );
  }
  static savePreventFrequentPopupFocus(value, callback = () => {}) {
    chrome.storage.sync.set({ preventFrequentPopupFocus: value }, callback);
  }

  static async getSelectedAlertsOverSites() {
    return false;
    ////
    return new Promise((resolve, reject) =>
      chrome.storage.sync.get({ alertsOverSites: false }, (value) => resolve(value.alertsOverSites))
    );
  }
  static saveSelectedAlertsOverSites(value, callback = () => {}) {
    chrome.storage.sync.set({ alertsOverSites: value }, callback);
  }

  static async getCitiesVersion() {
    // ברירת מחדל 0 — כך שגרסה ראשונה מהשרת (/lists-versions מחזיר 1) תיקלט (setCitiesVersion: value>current)
    return new Promise((resolve, reject) =>
      chrome.storage.sync.get({ citiesVersion: 0 }, (value) => resolve(value.citiesVersion))
    );
  }

  static async setCitiesVersion(value) {
    return new Promise(async (resolve, reject) => {
      const currentVersion = await Preferences.getCitiesVersion().catch(reject);
      if (value > currentVersion) chrome.storage.sync.set({ citiesVersion: value }, resolve);
      else resolve();
    });
  }

  static async getPolygonsVersion() {
    // ברירת מחדל 0 — ראה הערה ב-getCitiesVersion
    return new Promise((resolve, reject) =>
      chrome.storage.sync.get({ polygonsVersion: 0 }, (value) => resolve(value.polygonsVersion))
    );
  }
  static async setPolygonsVersion(value) {
    return new Promise(async (resolve, reject) => {
      const currentVersion = await Preferences.getPolygonsVersion().catch(reject);
      if (value > currentVersion) chrome.storage.sync.set({ polygonsVersion: value }, resolve);
      else resolve();
    });
  }

  static audio;
  // MVP: ניגון הצליל הנבחר בלבד — ללא תור הקראה וללא api.tzevaadom.co.il/reading.
  // הקראה (Web Speech) = שלב 2 (SPEC §6.4). ids/eventType נשמרים בחתימה לשימוש עתידי.
  static async playSound(ids, eventType) {
    return Preferences.startPlaying();
  }

  static async startPlaying() {
    const sound = await Preferences.getSelectedSound();
    Preferences.audio?.pause();
    Preferences.audio = new Audio("../sounds/" + sound + ".mp3");
    return Preferences.audio.play().catch(() => {});
  }

  static getDateString(date, shortYear = false) {
    const date_ = ("0" + date.getDate()).slice(-2);
    const month = ("0" + (date.getMonth() + 1)).slice(-2);
    const year = shortYear ? date.getYear().toString().substr(1, 2) : date.getFullYear();
    return date_ + "/" + month + "/" + year;
  }
  static getTimeString(date, minutesOnly = false) {
    const hours = date.getHours().toString().padStart(2, "0");
    const minutes = date.getMinutes().toString().padStart(2, "0");
    const seconds = date.getSeconds().toString().padStart(2, "0");
    return hours + ":" + minutes + (minutesOnly ? "" : ":" + seconds);
  }

  static getDateTimeString(date, minutesOnly = false) {
    return Preferences.getDateString(date) + " " + Preferences.getTimeString(date, minutesOnly);
  }

  static getRelativeTimeString(date1, date2 = new Date()) {
    const units = {
      year: 24 * 60 * 60 * 1000 * 365,
      month: (24 * 60 * 60 * 1000 * 365) / 12,
      week: 24 * 60 * 60 * 1000 * 7,
      day: 24 * 60 * 60 * 1000,
      hour: 60 * 60 * 1000,
      minute: 60 * 1000,
      second: 1000,
    };

    const rtf = new Intl.RelativeTimeFormat(City.siteLanguage?.toLowerCase() || "he", {
      numeric: "auto",
    });

    const passed = date2 - date1;
    if (passed <= units.minute) return STRINGS["now"][(City.siteLanguage ?? "he").toLowerCase()];
    if (passed > units.day) {
      date1.setHours(0, 0, 0, 0);
      date2.setHours(0, 0, 0, 0);
    }

    const elapsed = date1 - date2;

    // "Math.abs" accounts for both "past" & "future" scenarios
    for (var u in units)
      if (Math.abs(elapsed) >= units[u] || u == "second")
        return rtf.format(Math.round(elapsed / units[u]), u);
  }

  static sortCitiesByEventType(cities) {
    /*
            Sort cities array by eventType and Areas.
            example: {"0":{"גוש דן":["בני ברק"]}, "2":{"ירושלים":["ביתר עילית"]}}
        */
    var sortedList = {};
    cities.forEach((city) => {
      // Get localization names
      var cityName = city.getLocalizationCityName();
      var areaName = new Area(city.areaID, cityName).getLocalizationAreaName();
      var key = String(city.eventType);

      // Add city name to his area
      if (!sortedList.hasOwnProperty(key)) sortedList[key] = {};
      if (!sortedList[key].hasOwnProperty(areaName)) sortedList[key][areaName] = [];
      sortedList[key][areaName].push(cityName);
    });
    return sortedList;
  }

  static generateAlertMessage(alertCities, siteLanguage) {
    const dates = {};
    var list = {};
    alertCities.forEach((city) => (list[String(city.eventType)] = {}));
    alertCities.forEach((city) => {
      const key = String(city.eventType);
      list[key][city.timestamp.toString()] = [].concat(
        list[key][city.timestamp.toString()] || [],
        city
      );
      const dateStr = Preferences.getDateString(new Date(city.timestamp * 1000));
      dates[dateStr] = dateStr;
    });

    var alertText = "";
    Object.keys(list).forEach((eventTypeKey) => {
      alertText +=
        City.getLocalizationEventTypeTitle(Number(eventTypeKey)) +
        ` (${Object.values(dates).join(" - ")}):\n\n`;

      Object.keys(list[eventTypeKey]).forEach((timestamp) => {
        alertText += Preferences.getTimeString(new Date(Number(timestamp) * 1000)) + ":\n";
        const areasList = Preferences.sortCitiesByEventType(list[eventTypeKey][timestamp])[
          eventTypeKey
        ];
        alertText += Object.keys(areasList)
          .map((areaName) => `• ${areaName}: ${areasList[areaName].join(", ")}`)
          .join("\n");
        alertText += "\n\n";
      });
      alertText += "------------\n\n";
    });
    alertText +=
      (STRINGS.sentVia[siteLanguage.toLowerCase()] || STRINGS.sentVia.he) +
      "\n" +
      Preferences.getSystemsPageUrl(City.siteLanguage);
    return alertText;
  }

  // עברית בלבד — בורר השפות הוסר בעיצוב המחודש (SPEC §5.x). מחזיר תמיד "HE"
  // ומסנכרן את City.siteLanguage כדי שכל הפורמטים (זמן יחסי, כותרות) יהיו בעברית.
  static async getSelectedLanguage() {
    City.siteLanguage = "HE";
    return "HE";
  }
  static saveSelectedLanguage(languageCode, callback = () => {}) {
    chrome.storage.sync.set({ selectedLanguage: languageCode }, callback);
    City.siteLanguage = languageCode;
  }

  static async copyAlert(cities = null) {
    const siteLanguage = await Preferences.getSelectedLanguage();
    const currentAlertCities = cities
      ? cities
      : await new Promise((resolve, reject) =>
          chrome.runtime.sendMessage("currentAlertCities", (response) =>
            chrome.runtime.lastError
              ? resolve([])
              : resolve(
                  response.map(
                    (city) => new City(city.value, city.eventType, city.expireAt, city.timestamp)
                  )
                )
          )
        );
    var input = document.createElement("textarea");
    document.body.appendChild(input);
    input.value = Preferences.generateAlertMessage(currentAlertCities, siteLanguage);
    input.focus();
    input.select();
    document.execCommand("Copy");
    input.remove();
  }

  static getSystemsPageUrl(lang) {
    // אין אתר ציבורי ב-MVP; ההתרעות מוצגות בתוסף עצמו (popup + מפה).
    return SERVER_BASE_URL;
  }

  // ב-MVP אין מפת-אתר חיצונית — חלון ההתרעה (alert.html) כבר כולל מפה. no-op.
  static async launchSiteMap(currentCities) {}
}
