/**
 * StateStore — in-memory visibility matrix with persistence and change events.
 *
 * Sole keeper of the visibility state for all participants in the current session.
 * Persists state to `adapter.settings` ('visibilityMatrix') and emits
 * `scrying-pool:stateChanged` via Hooks.callAll on every mutation.
 *
 * Import rule: may only import from src/contracts/ and src/utils/.
 * Throws errors — callers (module.js / src/foundry/) are responsible for catching.
 *
 * @module core/StateStore
 */

import { VISIBILITY_STATES, isValidVisibilityMatrix } from '../contracts/visibility-matrix.js';

export const VISIBILITY_MATRIX_KEY = 'visibilityMatrix';

/**
 * In-memory visibility matrix with settings persistence and hooks event emission.
 * Uses Hooks.callAll directly (Hooks is a FoundryVTT standalone global).
 */
export class StateStore {
  /**
   * @param {{ get(key: string): unknown, set(key: string, value: unknown): Promise<unknown> }} settings
   *   Injected adapter.settings surface (already namespaced — pass short keys only).
   */
  constructor(settings) {
    if (!settings || typeof settings !== 'object') {
      throw new TypeError('StateStore: settings argument is required and must be an object');
    }
    this._settings = settings;
    /** @type {Record<string, string>} userId → VisibilityState */
    this._matrix = {};
    this._version = 1;
    this._revision = 0;
  }

  /**
   * Hydrates the in-memory matrix from the persisted world setting.
   * Called from module.js Hooks.once('ready'). Idempotent — last call wins.
   * On corrupt/missing data: logs warning and starts fresh (does not throw).
   */
  init() {
    try {
      const raw = this._settings.get(VISIBILITY_MATRIX_KEY);
      if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
        const validated = isValidVisibilityMatrix(raw);
        this._matrix = { ...validated.matrix };
        this._version = validated._version ?? 1;
        this._revision = validated._revision ?? 0;

        // Migration: 'never-connected' was incorrectly written to the matrix by socket-timeout
        // reverts when currentState was null (the fallback should have been 'active').
        // Replace persisted 'never-connected' entries with 'active' at hydration time.
        for (const userId of Object.keys(this._matrix)) {
          if (this._matrix[userId] === 'never-connected') {
            this._matrix[userId] = 'active';
          }
        }
      }
    } catch (err) {
      if (err instanceof TypeError) {
        console.warn('[ScryingPool] StateStore.init: invalid matrix data, starting fresh', err.message);
      } else {
        console.warn('[ScryingPool] StateStore.init: could not hydrate matrix, starting fresh');
      }
      this._matrix = {};
      this._revision = 0;
    }
  }

  /**
   * Returns the current VisibilityState for a participant, or null if unknown.
   * @param {string} userId
   * @returns {string|null}
   */
  getState(userId) {
    return this._matrix[userId] ?? null;
  }

  /**
   * Returns a deep snapshot of the current visibility matrix.
   * Mutations to the returned object do not affect internal state.
   * @returns {{ _version: number, matrix: Record<string, string> }}
   */
  getMatrix() {
    return { _version: this._version, matrix: { ...this._matrix } };
  }

  /**
   * Updates a single participant's visibility state, persists, and emits hook.
   * No-op if userId is empty/non-string or targetState is not a valid VISIBILITY_STATE.
   * @param {string} userId
   * @param {string} targetState - Must be a value in VISIBILITY_STATES.
   * @returns {Promise<unknown>|undefined} The persistence promise from settings.set, or undefined for no-op.
   */
  setVisibility(userId, targetState) {
    if (!userId || typeof userId !== 'string') return undefined;
    if (!VISIBILITY_STATES.includes(targetState)) return undefined;

    const previousState = this._matrix[userId] ?? null;
    this._matrix[userId] = targetState;
    this._revision++;

    const persistencePromise = this._settings.set(VISIBILITY_MATRIX_KEY, {
      _version: this._version,
      _revision: this._revision,
      matrix: { ...this._matrix },
    });

    // Fire hook immediately (per AC-2), but catch errors to prevent hook failures
    // from blocking state mutation. Persistence is async and may complete after.
    try {
      Hooks.callAll('scrying-pool:stateChanged', {
        userId,
        state: targetState,
        previousState,
        timestamp: Date.now(),
        revision: this._revision,
      });
    } catch (hookErr) {
      console.error('[ScryingPool] StateStore.setVisibility: hook emission failed', hookErr);
    }

    return persistencePromise;
  }

  /**
   * Replaces the entire matrix (used by preset apply and scene restore).
   * Throws TypeError on invalid matrix — src/core throws, callers catch.
   * @param {{ _version: number, _revision?: number, matrix: Record<string, string> }} matrix
   * @returns {Promise<unknown>} The persistence promise from settings.set.
   */
  setMatrix(matrix) {
    const validated = isValidVisibilityMatrix(matrix);
    this._matrix = { ...validated.matrix };
    this._version = validated._version ?? 1;
    this._revision++; // Always increment from current value; input _revision is informational only

    const persistencePromise = this._settings.set(VISIBILITY_MATRIX_KEY, {
      _version: this._version,
      _revision: this._revision,
      matrix: { ...this._matrix },
    });

    // Fire hook immediately, but catch errors
    try {
      Hooks.callAll('scrying-pool:stateChanged', {
        matrix: this.getMatrix(),
        timestamp: Date.now(),
        revision: this._revision,
      });
    } catch (hookErr) {
      console.error('[ScryingPool] StateStore.setMatrix: hook emission failed', hookErr);
    }

    return persistencePromise;
  }
}
