/**
 * Workspace Manager
 *
 * Central manager for workspace state synchronization across channels.
 * Provides a channel-agnostic interface for:
 * - Two-way sync of transient state (client ↔ gateway)
 * - Two-way sync of persistent state (client ↔ gateway ↔ container)
 * - Path-based change triggers (future use)
 *
 * Each user session gets one WorkspaceManager instance.
 * Multiple channels (WebSocket, Telegram, etc.) can attach to the same workspace.
 */

import type { FastifyBaseLogger } from 'fastify';
import type { Operation as JsonPatchOp } from 'fast-json-patch';
import { SyncRegistry } from './sync-registry.js';
import type { ContainerSync } from './container-sync.js';
import type {
  StoreConfig,
  ChannelAdapter,
  PathTrigger,
  PathTriggerHandler,
  PathTriggerContext,
  WorkspaceStores,
} from './types.js';
import { DEFAULT_STORES } from './types.js';

export interface WorkspaceManagerConfig {
  userId: string;
  sessionId: string;
  stores: StoreConfig[];
  containerSync?: ContainerSync;
  logger: FastifyBaseLogger;
}

/**
 * Manages workspace state for a user session.
 */
export class WorkspaceManager {
  private userId: string;
  private sessionId: string;
  private registry: SyncRegistry;
  private containerSync?: ContainerSync;
  private logger: FastifyBaseLogger;
  private stores: StoreConfig[];

  // Current channel adapter (WebSocket, Telegram, etc.)
  private adapter: ChannelAdapter | null = null;

  // Path triggers for change notifications
  private triggers: PathTrigger[] = [];

  // Track which stores are dirty (changed since last container sync)
  private dirtyStores = new Set<string>();

  // Track initialization state
  private initialized = false;

  constructor(config: WorkspaceManagerConfig) {
    this.userId = config.userId;
    this.sessionId = config.sessionId;
    this.stores = config.stores;
    this.containerSync = config.containerSync;
    this.logger = config.logger.child({ component: 'WorkspaceManager', sessionId: config.sessionId });

    this.registry = new SyncRegistry(this.logger);

    // Register all stores
    for (const store of this.stores) {
      this.registry.register(store);
    }
  }

  /**
   * Initialize workspace - load persistent stores from container.
   */
  async initialize(): Promise<void> {
    if (this.initialized) {
      return;
    }

    this.logger.info('Initializing workspace');

    // Load persistent stores from container
    if (this.containerSync?.isConnected()) {
      const persistentStores = this.stores.filter((s) => s.persistent).map((s) => s.name);

      if (persistentStores.length > 0) {
        this.logger.debug({ stores: persistentStores }, 'Loading persistent stores from container');

        const states = await this.containerSync.loadAllStores(persistentStores);
        for (const [storeName, state] of states) {
          let migratedState = state;
          // Migrate shapes from old flat format { shapeId: {...} } to wrapped { shapes: { shapeId: {...} } }
          if (storeName === 'shapes' && state && typeof state === 'object' && !('shapes' in (state as object))) {
            migratedState = { shapes: state };
            this.dirtyStores.add(storeName); // persist migrated format immediately
            this.logger.info({ store: storeName }, 'Migrated shapes store to wrapped format');
          }
          // Migrate *_types stores from old flat format { key: {...} } to wrapped { types: { key: {...} } }
          if (
            (storeName === 'indicator_types' || storeName === 'strategy_types' || storeName === 'research_types') &&
            state && typeof state === 'object' && !('types' in (state as object))
          ) {
            migratedState = { types: state };
            this.dirtyStores.add(storeName); // persist migrated format immediately
            this.logger.info({ store: storeName }, 'Migrated types store to wrapped format');
          }
          this.registry.setState(storeName, migratedState);
          this.logger.debug({ store: storeName }, 'Loaded persistent store');
        }
      }
    } else {
      this.logger.debug('Container sync not available, using initial state for persistent stores');
    }

    this.initialized = true;
    this.logger.info('Workspace initialized');
  }

  /**
   * Shutdown workspace - save dirty persistent stores to container.
   */
  async shutdown(): Promise<void> {
    if (!this.initialized) {
      return;
    }

    this.logger.info('Shutting down workspace');

    // Save dirty persistent stores
    await this.saveDirtyStores();

    this.adapter = null;
    this.initialized = false;
    this.logger.info('Workspace shut down');
  }

  // ===========================================================================
  // Channel Adapter Management
  // ===========================================================================

  /**
   * Set the channel adapter for sending messages.
   * Only one adapter can be active at a time.
   */
  setAdapter(adapter: ChannelAdapter): void {
    this.adapter = adapter;
    this.logger.debug('Channel adapter set');
  }

  /**
   * Clear the channel adapter.
   */
  clearAdapter(): void {
    this.adapter = null;
    this.logger.debug('Channel adapter cleared');
  }

  /**
   * Check if an adapter is connected.
   */
  hasAdapter(): boolean {
    return this.adapter !== null;
  }

  // ===========================================================================
  // Sync Protocol Handlers (called by channel adapters)
  // ===========================================================================

  /**
   * Handle hello message from client.
   * Sends snapshots or catchup patches for all stores.
   */
  async handleHello(clientSeqs: Record<string, number>): Promise<void> {
    if (!this.adapter) {
      this.logger.warn('No adapter connected, cannot respond to hello');
      return;
    }

    this.logger.debug({ clientSeqs }, 'Handling hello');

    const messages = this.registry.syncClient(clientSeqs);

    for (const msg of messages) {
      if (msg.type === 'snapshot') {
        this.adapter.sendSnapshot(msg);
      } else {
        this.adapter.sendPatch(msg);
      }
    }

    this.logger.debug({ messageCount: messages.length }, 'Sent sync messages');
  }

  /**
   * Handle patch message from client.
   * Applies patch and may send snapshot back on conflict.
   */
  async handlePatch(storeName: string, clientSeq: number, patch: JsonPatchOp[]): Promise<void> {
    this.logger.debug({ store: storeName, clientSeq, patchOps: patch.length }, 'Handling client patch');

    // Get old state for triggers
    const oldState = this.registry.getState(storeName);

    // Apply patch
    const response = this.registry.applyClientPatch(storeName, clientSeq, patch);

    // Mark as dirty if persistent
    const storeConfig = this.stores.find((s) => s.name === storeName);
    if (storeConfig?.persistent) {
      this.dirtyStores.add(storeName);
      // Persist immediately so changes survive page reloads (not just graceful shutdown)
      await this.saveDirtyStores();
    }

    // Send response if needed
    if (response && this.adapter) {
      this.adapter.sendSnapshot(response);
    }

    // Fire triggers
    const newState = this.registry.getState(storeName);
    await this.fireTriggers(storeName, oldState, newState, patch);
  }

  // ===========================================================================
  // State Access (for gateway code)
  // ===========================================================================

  /**
   * Get current state of a store.
   */
  getState<K extends keyof WorkspaceStores>(storeName: K): WorkspaceStores[K] | undefined;
  getState<T = unknown>(storeName: string): T | undefined;
  getState<T = unknown>(storeName: string): T | undefined {
    return this.registry.getState<T>(storeName);
  }

  /**
   * Update state of a store and notify client.
   */
  async setState(storeName: string, state: unknown): Promise<void> {
    // Get old state for triggers
    const oldState = this.registry.getState(storeName);

    // Update state (this computes and commits a patch)
    const patch = this.registry.updateState(storeName, () => state);

    if (patch) {
      // Mark as dirty if persistent
      const storeConfig = this.stores.find((s) => s.name === storeName);
      if (storeConfig?.persistent) {
        this.dirtyStores.add(storeName);
      }

      // Send patch to client
      if (this.adapter) {
        this.adapter.sendPatch({
          type: 'patch',
          store: storeName,
          seq: this.registry.getSeq(storeName),
          patch,
        });
      }

      // Fire triggers
      await this.fireTriggers(storeName, oldState, state, patch);
    }
  }

  /**
   * Update state with a partial merge.
   */
  async updateState<T extends Record<string, unknown>>(
    storeName: string,
    updates: Partial<T>
  ): Promise<void> {
    const current = this.registry.getState<T>(storeName);
    if (current && typeof current === 'object') {
      await this.setState(storeName, { ...current, ...updates });
    }
  }

  /**
   * Get all store names.
   */
  getStoreNames(): string[] {
    return this.registry.getStoreNames();
  }

  /**
   * Serialize entire workspace state as JSON.
   */
  serializeState(): string {
    const state: Record<string, unknown> = {};

    for (const storeConfig of this.stores) {
      const storeState = this.registry.getState(storeConfig.name);
      if (storeState !== undefined) {
        state[storeConfig.name] = storeState;
      }
    }

    return JSON.stringify(state, null, 2);
  }

  /**
   * Get the highest sequence number across all stores.
   */
  getCurrentSeq(): number {
    let maxSeq = 0;
    for (const storeName of this.registry.getStoreNames()) {
      const seq = this.registry.getSeq(storeName);
      if (seq > maxSeq) {
        maxSeq = seq;
      }
    }
    return maxSeq;
  }

  /**
   * Get all patches since a given sequence number across all stores.
   * Returns patches grouped by store name.
   */
  getChangesSince(sinceSeq: number): Record<string, JsonPatchOp[]> {
    const changes: Record<string, JsonPatchOp[]> = {};

    for (const storeConfig of this.stores) {
      const patches = this.registry.getPatchesSince(storeConfig.name, sinceSeq);
      if (patches && patches.length > 0) {
        changes[storeConfig.name] = patches;
      }
    }

    return changes;
  }

  // ===========================================================================
  // Path Triggers
  // ===========================================================================

  /**
   * Register a trigger for path changes.
   * Returns unsubscribe function.
   */
  onPathChange(path: string, handler: PathTriggerHandler): () => void {
    const trigger: PathTrigger = { path, handler };
    this.triggers.push(trigger);

    this.logger.debug({ path }, 'Registered path trigger');

    return () => {
      const index = this.triggers.indexOf(trigger);
      if (index >= 0) {
        this.triggers.splice(index, 1);
        this.logger.debug({ path }, 'Unregistered path trigger');
      }
    };
  }

  /**
   * Fire triggers for paths affected by a patch.
   */
  private async fireTriggers(
    storeName: string,
    oldState: unknown,
    newState: unknown,
    patch: JsonPatchOp[]
  ): Promise<void> {
    if (this.triggers.length === 0) {
      return;
    }

    const context: PathTriggerContext = {
      store: storeName,
      path: '',
      seq: this.registry.getSeq(storeName),
      userId: this.userId,
      sessionId: this.sessionId,
    };

    // Check each patch operation against triggers
    for (const op of patch) {
      const fullPath = `/${storeName}${op.path}`;

      for (const trigger of this.triggers) {
        if (this.pathMatches(fullPath, trigger.path)) {
          context.path = fullPath;

          // Extract old and new values at the path
          const oldValue = this.getValueAtPath(oldState, op.path);
          const newValue = this.getValueAtPath(newState, op.path);

          try {
            await trigger.handler(oldValue, newValue, context);
          } catch (error) {
            this.logger.error(
              { path: trigger.path, error },
              'Error in path trigger handler'
            );
          }
        }
      }
    }
  }

  /**
   * Check if a path matches a trigger path pattern.
   * Currently supports exact match and prefix match with wildcard.
   */
  private pathMatches(path: string, pattern: string): boolean {
    // Exact match
    if (path === pattern) {
      return true;
    }

    // Prefix match (e.g., /chartState/* matches /chartState/symbol)
    if (pattern.endsWith('/*')) {
      const prefix = pattern.slice(0, -2);
      return path.startsWith(prefix + '/');
    }

    return false;
  }

  /**
   * Get value at a JSON pointer path.
   */
  private getValueAtPath(obj: unknown, path: string): unknown {
    if (!path || path === '/') {
      return obj;
    }

    const parts = path.split('/').filter(Boolean);
    let current: any = obj;

    for (const part of parts) {
      if (current === null || current === undefined) {
        return undefined;
      }
      current = current[part];
    }

    return current;
  }

  // ===========================================================================
  // Container Persistence
  // ===========================================================================

  /**
   * Save dirty persistent stores to container.
   */
  async saveDirtyStores(): Promise<void> {
    if (!this.containerSync?.isConnected()) {
      this.logger.debug('Container sync not available, skipping save');
      return;
    }

    if (this.dirtyStores.size === 0) {
      this.logger.debug('No dirty stores to save');
      return;
    }

    const toSave = new Map<string, unknown>();

    for (const storeName of this.dirtyStores) {
      const storeConfig = this.stores.find((s) => s.name === storeName);
      if (storeConfig?.persistent) {
        const state = this.registry.getState(storeName);
        if (state !== undefined) {
          toSave.set(storeName, state);
        }
      }
    }

    if (toSave.size > 0) {
      this.logger.debug({ stores: Array.from(toSave.keys()) }, 'Saving dirty stores to container');
      await this.containerSync.saveAllStores(toSave);
      this.dirtyStores.clear();
    }
  }

  /**
   * Force save a specific store to container.
   */
  async saveStore(storeName: string): Promise<void> {
    if (!this.containerSync?.isConnected()) {
      this.logger.warn({ store: storeName }, 'Container sync not available');
      return;
    }

    const storeConfig = this.stores.find((s) => s.name === storeName);
    if (!storeConfig?.persistent) {
      this.logger.warn({ store: storeName }, 'Store is not persistent');
      return;
    }

    const state = this.registry.getState(storeName);
    if (state !== undefined) {
      await this.containerSync.saveStore(storeName, state);
      this.dirtyStores.delete(storeName);
    }
  }

  // ===========================================================================
  // Accessors
  // ===========================================================================

  getUserId(): string {
    return this.userId;
  }

  getSessionId(): string {
    return this.sessionId;
  }

  isInitialized(): boolean {
    return this.initialized;
  }
}

// Re-export DEFAULT_STORES for convenience
export { DEFAULT_STORES };
