// Feature Store v3.0.0
// Per-tab, per-frame feature accumulation.
// Stores typed features produced by signal providers and rule outputs.
// Persists via chrome.storage.session to survive service worker restarts.
// Provides snapshot method for cloud analysis requests.

const MAX_FEATURES_PER_TAB = 500;
const PERSIST_DEBOUNCE_MS = 2000;
const MAX_FEATURE_VALUE_SIZE = 10000; // chars, for serialization safety

export class FeatureStore {
  constructor() {
    this.tabs = new Map(); // tabId -> TabState
    this._persistTimer = null;
    this._dirty = false;
  }

  // ─── Tab Lifecycle ─────────────────────────────────────

  /**
   * Create or reset tab state for a new page load.
   * Called on webNavigation.onCommitted for main frame.
   * @param {number} tabId
   * @param {string} url
   */
  createTab(tabId, url) {
    let domain = null;
    try { domain = new URL(url).hostname; } catch {}

    this.tabs.set(tabId, {
      url,
      domain,
      startedAt: Date.now(),
      frames: new Map(),    // frameId -> Map<featurePath, Feature>
      tabFeatures: new Map(), // tab-level features
    });

    // Create main frame entry
    this.tabs.get(tabId).frames.set(0, new Map());
    this._markDirty();
  }

  /**
   * Create or reset a frame within an existing tab.
   * Called on webNavigation.onCommitted for sub-frames.
   * @param {number} tabId
   * @param {number} frameId
   */
  createFrame(tabId, frameId) {
    const tab = this.tabs.get(tabId);
    if (!tab) return;
    tab.frames.set(frameId, new Map());
    this._markDirty();
  }

  /**
   * Remove all state for a tab.
   * Called on tabs.onRemoved.
   * @param {number} tabId
   */
  removeTab(tabId) {
    this.tabs.delete(tabId);
    // Flush immediately on tab close to avoid losing the deletion
    this._dirty = true;
    this.persist();
  }

  /**
   * Check if a tab exists in the store.
   * @param {number} tabId
   * @returns {boolean}
   */
  hasTab(tabId) {
    return this.tabs.has(tabId);
  }

  // ─── Feature Storage ───────────────────────────────────

  /**
   * Set a feature value.
   * @param {number} tabId
   * @param {string} featurePath - e.g. "url.is_punycode" or "rule.credential_overlay"
   * @param {any} value - Typed value
   * @param {object} [options]
   * @param {string} [options.source] - e.g. "provider:url.is_punycode" or "rule:my-rule-id"
   * @param {number} [options.frameId=0]
   * @param {string} [options.scope="frame"] - "frame" or "tab"
   * @param {object} [options.metadata]
   */
  set(tabId, featurePath, value, options = {}) {
    const tab = this.tabs.get(tabId);
    if (!tab) return;

    const feature = {
      value,
      source: options.source || null,
      timestamp: Date.now(),
      frameId: options.frameId ?? 0,
      metadata: options.metadata || null,
    };

    const scope = options.scope || 'frame';

    if (scope === 'tab') {
      // Tab-level feature (aggregated across frames)
      if (tab.tabFeatures.size < MAX_FEATURES_PER_TAB) {
        tab.tabFeatures.set(featurePath, feature);
      }
    } else {
      // Frame-level feature
      const frameId = options.frameId ?? 0;
      let frameFeatures = tab.frames.get(frameId);
      if (!frameFeatures) {
        frameFeatures = new Map();
        tab.frames.set(frameId, frameFeatures);
      }
      if (frameFeatures.size < MAX_FEATURES_PER_TAB) {
        frameFeatures.set(featurePath, feature);
      }
    }

    this._markDirty();
  }

  /**
   * Get a feature value.
   * Checks frame-level first, then tab-level.
   * @param {number} tabId
   * @param {string} featurePath
   * @param {object} [options]
   * @param {string} [options.scope] - "frame", "tab", or undefined (auto: frame then tab)
   * @param {number} [options.frameId=0]
   * @returns {any} The feature value, or undefined if not found
   */
  get(tabId, featurePath, options = {}) {
    const tab = this.tabs.get(tabId);
    if (!tab) return undefined;

    const frameId = options.frameId ?? 0;
    const scope = options.scope;

    if (scope === 'tab') {
      const feature = tab.tabFeatures.get(featurePath);
      return feature ? feature.value : undefined;
    }

    if (scope === 'frame') {
      const frameFeatures = tab.frames.get(frameId);
      if (!frameFeatures) return undefined;
      const feature = frameFeatures.get(featurePath);
      return feature ? feature.value : undefined;
    }

    // Auto: check frame first, then tab
    const frameFeatures = tab.frames.get(frameId);
    if (frameFeatures) {
      const feature = frameFeatures.get(featurePath);
      if (feature) return feature.value;
    }
    const tabFeature = tab.tabFeatures.get(featurePath);
    return tabFeature ? tabFeature.value : undefined;
  }

  /**
   * Get a full Feature object (with metadata).
   * @param {number} tabId
   * @param {string} featurePath
   * @param {object} [options]
   * @returns {object|null}
   */
  getFeature(tabId, featurePath, options = {}) {
    const tab = this.tabs.get(tabId);
    if (!tab) return null;

    const frameId = options.frameId ?? 0;
    const scope = options.scope;

    if (scope === 'tab') {
      return tab.tabFeatures.get(featurePath) || null;
    }

    if (scope === 'frame') {
      const frameFeatures = tab.frames.get(frameId);
      return frameFeatures ? (frameFeatures.get(featurePath) || null) : null;
    }

    // Auto
    const frameFeatures = tab.frames.get(frameId);
    if (frameFeatures && frameFeatures.has(featurePath)) {
      return frameFeatures.get(featurePath);
    }
    return tab.tabFeatures.get(featurePath) || null;
  }

  /**
   * Check if a feature exists.
   * @param {number} tabId
   * @param {string} featurePath
   * @param {object} [options]
   * @returns {boolean}
   */
  has(tabId, featurePath, options = {}) {
    return this.get(tabId, featurePath, options) !== undefined;
  }

  // ─── Snapshots ─────────────────────────────────────────

  /**
   * Get a full feature snapshot for a tab (for cloud reports).
   * Merges frame-level and tab-level features into a flat map.
   * @param {number} tabId
   * @returns {object} { [featurePath]: { value, source, timestamp, frameId } }
   */
  getSnapshot(tabId) {
    const tab = this.tabs.get(tabId);
    if (!tab) return {};

    const snapshot = {};

    // Tab-level features first (lower priority)
    for (const [path, feature] of tab.tabFeatures) {
      snapshot[path] = this._serializeFeature(feature);
    }

    // Frame-level features override tab-level for same path
    for (const [frameId, frameFeatures] of tab.frames) {
      for (const [path, feature] of frameFeatures) {
        // Prefix with frame if not main frame, or just use path
        const key = frameId === 0 ? path : `frame[${frameId}].${path}`;
        snapshot[key] = this._serializeFeature(feature);
      }
    }

    return snapshot;
  }

  /**
   * Get tab metadata (url, domain, timing).
   * @param {number} tabId
   * @returns {object|null}
   */
  getTabInfo(tabId) {
    const tab = this.tabs.get(tabId);
    if (!tab) return null;
    return {
      url: tab.url,
      domain: tab.domain,
      startedAt: tab.startedAt,
      featureCount: this._countFeatures(tabId),
      frameCount: tab.frames.size,
    };
  }

  // ─── Persistence ───────────────────────────────────────

  /**
   * Persist current state to chrome.storage.session.
   * Called on a debounced timer after changes.
   */
  async persist() {
    if (!this._dirty) return;

    try {
      const serialized = {};
      for (const [tabId, tab] of this.tabs) {
        serialized[tabId] = {
          url: tab.url,
          domain: tab.domain,
          startedAt: tab.startedAt,
          tabFeatures: this._serializeMap(tab.tabFeatures),
          frames: {},
        };
        for (const [frameId, frameFeatures] of tab.frames) {
          serialized[tabId].frames[frameId] = this._serializeMap(frameFeatures);
        }
      }

      await chrome.storage.session.set({ featureStore: serialized });
      this._dirty = false;
    } catch (error) {
      console.error('[FeatureStore] Persist failed:', error.message);
    }
  }

  /**
   * Restore state from chrome.storage.session.
   * Called on service worker restart.
   */
  async restore() {
    try {
      const result = await chrome.storage.session.get(['featureStore']);
      if (!result.featureStore) return;

      const serialized = result.featureStore;
      for (const [tabIdStr, tabData] of Object.entries(serialized)) {
        const tabId = parseInt(tabIdStr, 10);

        const tab = {
          url: tabData.url,
          domain: tabData.domain,
          startedAt: tabData.startedAt,
          tabFeatures: this._deserializeMap(tabData.tabFeatures),
          frames: new Map(),
        };

        for (const [frameIdStr, frameData] of Object.entries(tabData.frames || {})) {
          tab.frames.set(parseInt(frameIdStr, 10), this._deserializeMap(frameData));
        }

        this.tabs.set(tabId, tab);
      }

      console.log(`[FeatureStore] Restored ${this.tabs.size} tabs from session storage`);
    } catch (error) {
      console.error('[FeatureStore] Restore failed:', error.message);
    }
  }

  // ─── Internal ──────────────────────────────────────────

  _markDirty() {
    this._dirty = true;
    if (!this._persistTimer) {
      this._persistTimer = setTimeout(() => {
        this._persistTimer = null;
        this.persist();
      }, PERSIST_DEBOUNCE_MS);
    }
  }

  _serializeFeature(feature) {
    let value = feature.value;
    // Truncate large string values for serialization
    if (typeof value === 'string' && value.length > MAX_FEATURE_VALUE_SIZE) {
      value = value.substring(0, MAX_FEATURE_VALUE_SIZE);
    }
    return {
      value,
      source: feature.source,
      timestamp: feature.timestamp,
      frameId: feature.frameId,
    };
  }

  _serializeMap(map) {
    const obj = {};
    for (const [key, feature] of map) {
      obj[key] = this._serializeFeature(feature);
    }
    return obj;
  }

  _deserializeMap(obj) {
    const map = new Map();
    if (!obj) return map;
    for (const [key, feature] of Object.entries(obj)) {
      map.set(key, feature);
    }
    return map;
  }

  _countFeatures(tabId) {
    const tab = this.tabs.get(tabId);
    if (!tab) return 0;
    let count = tab.tabFeatures.size;
    for (const frameFeatures of tab.frames.values()) {
      count += frameFeatures.size;
    }
    return count;
  }

  /**
   * Get store-wide statistics.
   * @returns {object}
   */
  getStats() {
    let totalFeatures = 0;
    for (const tabId of this.tabs.keys()) {
      totalFeatures += this._countFeatures(tabId);
    }
    return {
      tabCount: this.tabs.size,
      totalFeatures,
    };
  }

  /**
   * Clean up (clear timers).
   */
  destroy() {
    if (this._persistTimer) {
      clearTimeout(this._persistTimer);
      this._persistTimer = null;
    }
  }
}
