// Kotleto · tiny local JSON database adapter
// data/db.json is the seed; browser changes are persisted as JSON in localStorage.

const DB_KEY = "kotleto-local-db-v1";
const ORDER_SEQUENCE_KEY = DB_KEY + "-order-sequence";
const DB_FALLBACK = {
  version: 1,
  cart: [],
  favorites: [],
  activeOrder: null,
  orders: [],
  user: null,
  settings: { newOrder: true, dailyReport: true, lowStock: true, newReview: false },
};

const LocalDB = {
  _data: null,
  _seedLoaded: false,
  _dirtyBeforeSeed: false,
  _dirtyKeys: new Set(),
  _listeners: new Set(),
  _orderSequence: 1000,

  readStored() {
    try {
      const raw = localStorage.getItem(DB_KEY);
      const parsed = raw ? JSON.parse(raw) : null;
      return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
    } catch (_) { return null; }
  },

  snapshot() {
    if (!this._data) this._data = { ...DB_FALLBACK, ...(this.readStored() || {}) };
    return this._data;
  },

  async init() {
    const stored = this.readStored();
    if (stored) {
      this._data = { ...DB_FALLBACK, ...stored };
      this._seedLoaded = true;
      return this._data;
    }
    try {
      const response = await fetch("data/db.json", { cache: "no-store" });
      if (!response.ok) throw new Error("Local JSON seed unavailable");
      const seed = { ...DB_FALLBACK, ...(await response.json()) };
      if (this._dirtyBeforeSeed) {
        const dirtyData = {};
        this._dirtyKeys.forEach(key => { dirtyData[key] = this._data[key]; });
        this._data = { ...seed, ...dirtyData };
      } else {
        this._data = seed;
      }
      this._seedLoaded = true;
      this.persist();
    } catch (_) {
      this._data = this._data || { ...DB_FALLBACK };
      this._seedLoaded = true;
    }
    this.emit();
    return this._data;
  },

  get(key, fallback) {
    const value = this.snapshot()[key];
    return value == null ? fallback : value;
  },

  set(key, value) {
    this.patch({ [key]: value });
    return value;
  },

  // Related fields become visible together, never as a partly imported order/database.
  patch(values) {
    if (!this._seedLoaded) {
      this._dirtyBeforeSeed = true;
      Object.keys(values).forEach(key => this._dirtyKeys.add(key));
    }
    this._data = { ...this.snapshot(), ...values };
    this.persist();
    this.emit();
    return this._data;
  },

  emit() { this._listeners.forEach(listener => listener(this.snapshot())); },
  update(key, updater, fallback) { return this.set(key, updater(this.get(key, fallback))); },
  persist() { try { localStorage.setItem(DB_KEY, JSON.stringify(this.snapshot())); } catch (_) {} },
  subscribe(listener) { this._listeners.add(listener); return () => this._listeners.delete(listener); },

  // A separate reservation counter preserves short demo IDs without a repeating clock modulus.
  // Reserving an ID must not overwrite order data from another open tab.
  nextOrderId(reservedIds = []) {
    let sequence = this._orderSequence;
    try {
      const stored = Number(localStorage.getItem(ORDER_SEQUENCE_KEY));
      if (Number.isSafeInteger(stored) && stored >= 0) sequence = Math.max(sequence, stored);
    } catch (_) { /* In-memory reservation still works when storage is unavailable. */ }
    const ids = [...reservedIds];
    [this.snapshot(), this.readStored()].filter(Boolean).forEach(db => {
      (Array.isArray(db.orders) ? db.orders : []).forEach(order => ids.push(order.id));
      if (db.activeOrder) ids.push(db.activeOrder.id);
      ids.push(...Object.keys(db.adminOrderStatuses || {}));
    });
    ids.forEach(id => {
      const match = /^KT-(\d+)$/.exec(String(id));
      const number = match ? Number(match[1]) : NaN;
      if (Number.isSafeInteger(number)) sequence = Math.max(sequence, number);
    });
    if (!Number.isSafeInteger(sequence + 1)) throw new Error("Order sequence exhausted");
    this._orderSequence = sequence + 1;
    try { localStorage.setItem(ORDER_SEQUENCE_KEY, String(this._orderSequence)); } catch (_) {}
    return "KT-" + this._orderSequence;
  },

  importSnapshot(data) {
    const record = value => value !== null && typeof value === "object" && !Array.isArray(value);
    const text = value => typeof value === "string";
    const bilingual = value => text(value) || (record(value) && text(value.fa) && text(value.en));
    const amount = value => typeof value === "number" && Number.isFinite(value) && value >= 0;
    const identified = value => record(value) && text(value.id) && value.id.trim().length > 0;
    const optionalText = (value, keys) => keys.every(key => value[key] === undefined || text(value[key]));
    const cartItem = value => identified(value) && text(value.name) && amount(value.price) && Number.isSafeInteger(value.qty) && value.qty > 0
      && optionalText(value, ["photo", "emoji", "color"]);
    const statuses = ["placed", "new", "preparing", "packed", "out", "delivered"];
    const order = value => identified(value) && amount(value.total)
      && (Array.isArray(value.items) || (Number.isSafeInteger(value.items) && value.items >= 0))
      && statuses.includes(value.status)
      && optionalText(value, ["channel", "time", "note", "payment"])
      && (value.promo === undefined || value.promo === null || text(value.promo))
      && (value.placedAt === undefined || amount(value.placedAt))
      && (value.eta === undefined || (amount(value.eta) && value.eta > 0))
      && (value.address === undefined || bilingual(value.address))
      && (value.courier === undefined || bilingual(value.courier))
      && (value.customer === undefined || (record(value.customer) && text(value.customer.name) && text(value.customer.phone)))
      && (value.lineItems === undefined || (Array.isArray(value.lineItems) && value.lineItems.every(cartItem)));
    const rules = {
      version: value => value === 1,
      cart: value => Array.isArray(value) && value.every(cartItem),
      cartPromo: text,
      favorites: value => Array.isArray(value) && value.every(text),
      orders: value => Array.isArray(value) && value.every(order) && new Set(value.map(item => item.id)).size === value.length,
      activeOrder: value => value === null || order(value),
      user: value => value === null || (record(value) && ["name", "phone", "address"].every(key => text(value[key]))),
      settings: value => record(value) && Object.values(value).every(item => typeof item === "boolean"),
      adminOrderStatuses: value => record(value) && Object.values(value).every(item => statuses.includes(item)),
      adminMenu: value => Array.isArray(value) && value.every(item => identified(item) && record(item.fa) && record(item.en)
        && [item.fa.name, item.en.name, item.fa.desc, item.en.desc, item.cat].every(text) && amount(item.price)
        && optionalText(item, ["photo", "emoji", "color"]) && (item.badge === undefined || item.badge === null || bilingual(item.badge))
        && (item.active === undefined || typeof item.active === "boolean")
        && (item.spice === undefined || (Number.isInteger(item.spice) && item.spice >= 0 && item.spice <= 5))),
      adminStaff: value => Array.isArray(value) && value.every(item => identified(item) && bilingual(item.name) && bilingual(item.role) && text(item.shift)
        && ["on", "off", "break"].includes(item.status) && (item.today === undefined || amount(item.today))),
      adminHours: value => Array.isArray(value) && value.every(item => identified(item) && text(item.fa) && text(item.en) && text(item.time)
        && (item.active === undefined || typeof item.active === "boolean")),
      adminNotifications: value => Array.isArray(value) && value.every(item => identified(item) && record(item.fa) && record(item.en)
        && [item.fa.title, item.fa.body, item.en.title, item.en.body].every(text) && bilingual(item.time) && typeof item.unread === "boolean"),
      deliveryConfig: value => record(value) && Object.entries(value).every(([key, number]) => ["fee", "freeFrom", "minOrder", "maxDistance"].includes(key) && amount(number) && (key !== "maxDistance" || number > 0)),
    };
    if (!record(data) || !Object.keys(data).length || Object.entries(data).some(([key, value]) => !Object.hasOwn(rules, key) || !rules[key](value))) {
      throw new Error("Invalid Kotleto database structure");
    }
    return this.patch(data);
  },

  export() {
    const blob = new Blob([JSON.stringify(this.snapshot(), null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = `kotleto-db-${new Date().toISOString().slice(0, 10)}.json`;
    link.click();
    URL.revokeObjectURL(url);
  },
};

LocalDB.ready = LocalDB.init();
window.addEventListener("storage", event => {
  if (event.key !== DB_KEY && event.key !== null) return;
  LocalDB._data = { ...DB_FALLBACK, ...(LocalDB.readStored() || {}) };
  LocalDB.emit();
});
window.LocalDB = LocalDB;
