/**
 * Hypen App Builder API
 * Implements the stateful module system from RFC-0001
 */

import type { Action } from "./types.js";
import type { Session } from "./remote/types.js";
import { type Result, Ok, Err, fromPromise, ActionError, HypenError } from "./result.js";
import { DataSourceManager, type DataSourcePlugin, type IDataSourceEngine } from "./datasource.js";

// Interface for engine compatibility (works with both engine.js and engine.browser.js)
export interface IEngine {
  setModule(name: string, actions: string[], stateKeys: string[], initialState: unknown): void;
  onAction(actionName: string, handler: (action: Action) => void | Promise<void>): void;
  notifyStateChange(paths: string[], changedValues: Record<string, unknown>): void;
}

import { createObservableState, type StateChange, getStateSnapshot } from "./state.js";
import type { HypenRouter } from "./router.js";
import type { HypenGlobalContext, ModuleReference } from "./context.js";
import { createLogger } from "./logger.js";

const log = createLogger("ModuleInstance");

export type ActionContext<P = unknown> = {
  name: string;
  payload?: P;
  sender?: string;
};

export type GlobalContext = {
  getModule: <T = unknown>(id: string) => ModuleReference<T>;
  hasModule: (id: string) => boolean;
  getModuleIds: () => string[];
  getGlobalState: () => Record<string, unknown>;
  emit: (event: string, payload?: unknown) => void;
  on: (event: string, handler: (payload?: unknown) => void) => () => void;
  router: HypenRouter | null;
};

export type LifecycleHandler<T> = (
  state: T,
  context?: GlobalContext
) => void | Promise<void>;

/**
 * Action handler context - all parameters available explicitly
 */
export interface ActionHandlerContext<T, P = unknown> {
  action: ActionContext<P>;
  state: T;
  context: GlobalContext;
  /** Access to registered data source plugins for mutations */
  dataSources: DataSourceAccessor;
}

/**
 * Provides typed access to registered data source plugins.
 * Unknown method calls are forwarded to `plugin.call()`:
 *   `dataSources.spacetime.sendMessage(text)` → `plugin.call("sendMessage", text)`
 *
 * You can also call `.call()` directly if you prefer:
 *   `dataSources.spacetime.call("sendMessage", text)`
 */
export interface DataSourceAccessor {
  [providerName: string]: DataSourcePlugin & Record<string, (...args: unknown[]) => Promise<unknown>>;
}

/**
 * Action handler - receives all context in a single object
 */
export type ActionHandler<T, P = unknown> = (ctx: ActionHandlerContext<T, P>) => void | Promise<void>;

/**
 * Context passed to onDisconnect handler
 */
export interface DisconnectContext<T> {
  /** Current state snapshot (ready to serialize and save) */
  state: T;
  /** Session information */
  session: Session;
}

/**
 * Context passed to onReconnect handler
 */
export interface ReconnectContext<T> {
  /** Session information */
  session: Session;
  /** Call this with saved state to restore it */
  restore: (savedState: T) => void;
}

/**
 * Context passed to onExpire handler
 */
export interface ExpireContext {
  /** Session information */
  session: Session;
}

/**
 * Handler called when client disconnects (session still alive for TTL)
 */
export type DisconnectHandler<T> = (ctx: DisconnectContext<T>) => void | Promise<void>;

/**
 * Handler called when client reconnects with existing session
 */
export type ReconnectHandler<T> = (ctx: ReconnectContext<T>) => void | Promise<void>;

/**
 * Handler called when session TTL expires (client never reconnected)
 */
export type ExpireHandler = (ctx: ExpireContext) => void | Promise<void>;

/**
 * Context passed to onError handler
 */
export interface ErrorContext<T> {
  /** The error that occurred */
  error: HypenError;
  /** Current state (for inspection, not mutation during error handling) */
  state: T;
  /** The action name if error occurred in an action handler */
  actionName?: string;
  /** The lifecycle phase if error occurred in a lifecycle handler */
  lifecycle?: "created" | "destroyed" | "disconnect" | "reconnect" | "expire";
}

/**
 * Error handler return type - controls error propagation
 */
export type ErrorHandlerResult =
  | void                    // Continue with default behavior (log + emit)
  | { handled: true }       // Error was handled, skip default behavior
  | { retry: true }         // Retry the operation (only for actions)
  | { rethrow: true };      // Re-throw the error

/**
 * Handler called when an error occurs in the module
 */
export type ErrorHandler<T> = (ctx: ErrorContext<T>) => ErrorHandlerResult | Promise<ErrorHandlerResult>;

export interface HypenModuleDefinition<T = unknown> {
  name?: string;
  actions: string[];
  stateKeys: string[];
  persist?: boolean;
  version?: number;
  initialState: T;
  /**
   * Inline UI template for single-file components.
   * Set via the `.ui(hypen`...`)` method.
   */
  template?: string;
  /**
   * Data source plugins registered via `.useDataSource()`.
   * Each entry contains the plugin and its configuration.
   */
  dataSources?: Array<{ plugin: DataSourcePlugin; config: unknown }>;
  handlers: {
    onCreated?: LifecycleHandler<T>;
    onAction: Map<string, ActionHandler<T, any>>;
    onDestroyed?: LifecycleHandler<T>;
    /** Called when client disconnects (session persists for TTL) */
    onDisconnect?: DisconnectHandler<T>;
    /** Called when client reconnects with existing session */
    onReconnect?: ReconnectHandler<T>;
    /** Called when session TTL expires */
    onExpire?: ExpireHandler;
    /** Called when any error occurs in the module */
    onError?: ErrorHandler<T>;
  };
}

/**
 * Alias for HypenModuleDefinition for backward compatibility
 */
export type HypenModule<T = unknown> = HypenModuleDefinition<T>;

/**
 * Builder for creating Hypen app modules
 */
export class HypenAppBuilder<T> {
  private initialState: T;
  private options: { persist?: boolean; version?: number; name?: string };
  private createdHandler?: LifecycleHandler<T>;
  private actionHandlers: Map<string, ActionHandler<T, any>> = new Map();
  private destroyedHandler?: LifecycleHandler<T>;
  private disconnectHandler?: DisconnectHandler<T>;
  private reconnectHandler?: ReconnectHandler<T>;
  private expireHandler?: ExpireHandler;
  private errorHandler?: ErrorHandler<T>;
  private template?: string;
  private _registry?: Map<string, HypenModuleDefinition>;
  private dataSourceEntries: Array<{ plugin: DataSourcePlugin; config: unknown }> = [];

  constructor(
    initialState: T,
    options?: { persist?: boolean; version?: number; name?: string },
    registry?: Map<string, HypenModuleDefinition>
  ) {
    this.initialState = initialState;
    this.options = options || {};
    this._registry = registry;
  }

  /**
   * Register a handler for module creation
   */
  onCreated(fn: LifecycleHandler<T>): this {
    this.createdHandler = fn;
    return this;
  }

  /**
   * Register a handler for a specific action.
   * Optionally provide a payload type parameter for typed action payloads.
   *
   * @example
   * ```typescript
   * // Typed payload:
   * .onAction<{ amount: number }>("add", ({ action, state }) => {
   *   action.payload.amount; // fully typed
   * })
   *
   * // Untyped (payload is unknown):
   * .onAction("add", ({ action, state }) => {
   *   action.payload; // unknown
   * })
   * ```
   */
  onAction<P = unknown>(name: string, fn: ActionHandler<T, P>): this {
    this.actionHandlers.set(name, fn as ActionHandler<T, any>);
    return this;
  }

  /**
   * Register a handler for module destruction
   */
  onDestroyed(fn: LifecycleHandler<T>): this {
    this.destroyedHandler = fn;
    return this;
  }

  /**
   * Register a handler for client disconnection
   * Called when client disconnects; session persists for TTL
   * Use this to save state for later restoration
   */
  onDisconnect(fn: DisconnectHandler<T>): this {
    this.disconnectHandler = fn;
    return this;
  }

  /**
   * Register a handler for session reconnection
   * Called when client reconnects with an existing session ID
   * Use restore() to hydrate state from saved data
   */
  onReconnect(fn: ReconnectHandler<T>): this {
    this.reconnectHandler = fn;
    return this;
  }

  /**
   * Register a handler for session expiration
   * Called when TTL expires and client never reconnected
   * Use this to clean up stored state
   */
  onExpire(fn: ExpireHandler): this {
    this.expireHandler = fn;
    return this;
  }

  /**
   * Register an error handler for the module
   * Called when any error occurs in action handlers or lifecycle hooks
   *
   * @example
   * ```typescript
   * app
   *   .defineState({ count: 0 })
   *   .onAction("increment", ({ state }) => {
   *     if (state.count > 100) throw new Error("Count too high");
   *     state.count += 1;
   *   })
   *   .onError(({ error, actionName, state }) => {
   *     console.error(`Error in ${actionName}:`, error.message);
   *     // Optionally recover
   *     if (error.message.includes("too high")) {
   *       return { handled: true }; // Don't propagate
   *     }
   *     // Or retry
   *     // return { retry: true };
   *   })
   *   .build();
   * ```
   *
   * @param fn - Error handler function
   * @returns Builder for chaining
   */
  onError(fn: ErrorHandler<T>): this {
    this.errorHandler = fn;
    return this;
  }

  /**
   * Register a data source plugin for live database subscriptions.
   *
   * @example
   * ```typescript
   * import { SpacetimeDBPlugin } from "@hypen-space/plugin-spacetimedb";
   *
   * export default app
   *   .defineState({ messageText: "" })
   *   .useDataSource(new SpacetimeDBPlugin(), {
   *     uri: "ws://localhost:3000",
   *     moduleName: "chat",
   *     tables: ["user", "message"],
   *   })
   *   .onAction("sendMessage", async ({ state, dataSources }) => {
   *     await dataSources.spacetime.sendMessage(state.messageText);
   *     state.messageText = "";
   *   })
   *   .build();
   * ```
   *
   * In Hypen DSL, bind to data source tables with `$provider.table`:
   * ```hypen
   * ForEach(items: $spacetime.message, key: "id") {
   *   Text("${item.text}")
   * }
   * ```
   */
  useDataSource<C>(plugin: DataSourcePlugin<C>, config: C): this {
    this.dataSourceEntries.push({ plugin: plugin as DataSourcePlugin, config });
    return this;
  }

  /**
   * Define the component's UI template inline (single-file component).
   *
   * Use with the `hypen` tagged template literal and binding proxies:
   *
   * @example
   * ```typescript
   * import { app, hypen, state } from "@hypen-space/core";
   *
   * export default app
   *   .defineState({ count: 0 })
   *   .onAction("increment", ({ state }) => {
   *     state.count += 1;
   *   })
   *   .ui(hypen`
   *     Column {
   *       Text("Count: ${state.count}")
   *       Button { Text("+") }
   *         .onClick("@actions.increment")
   *     }
   *   `);
   * ```
   *
   * @param template - The Hypen DSL template string
   * @returns The built module definition (calls build() internally)
   */
  ui(template: string): HypenModuleDefinition<T> {
    this.template = template;
    return this.build();
  }

  /**
   * Load a UI template from a .hypen file on disk.
   *
   * @example
   * ```typescript
   * import { app } from "@hypen-space/core";
   *
   * export default app
   *   .defineState({ count: 0 })
   *   .onAction("increment", ({ state }) => { state.count += 1; })
   *   .uiFile("./counter.hypen");
   * ```
   *
   * @param path - Path to a .hypen template file
   * @returns The built module definition (calls build() internally)
   */
  uiFile(path: string): HypenModuleDefinition<T> {
    const fs = require("fs");
    this.template = fs.readFileSync(path, "utf-8").trim();
    return this.build();
  }

  /**
   * Build the module definition
   */
  build(): HypenModuleDefinition<T> {
    // Safe way to get keys from initialState
    const stateKeys = this.initialState !== null && typeof this.initialState === 'object'
      ? Object.keys(this.initialState)
      : [];

    const definition: HypenModuleDefinition<T> = {
      name: this.options.name,
      actions: Array.from(this.actionHandlers.keys()),
      stateKeys,
      persist: this.options.persist,
      version: this.options.version,
      initialState: this.initialState,
      template: this.template,
      dataSources: this.dataSourceEntries.length > 0 ? this.dataSourceEntries : undefined,
      handlers: {
        onCreated: this.createdHandler,
        onAction: this.actionHandlers,
        onDestroyed: this.destroyedHandler,
        onDisconnect: this.disconnectHandler,
        onReconnect: this.reconnectHandler,
        onExpire: this.expireHandler,
        onError: this.errorHandler,
      },
    };

    // Auto-register in the app registry when the module has a name
    if (this.options.name && this._registry) {
      this._registry.set(this.options.name, definition as HypenModuleDefinition);
    }

    return definition;
  }
}

/**
 * Hypen App API — singleton factory and component registry.
 *
 * Modules built with a `name` are automatically registered here.
 * Consumers (ManagedRouter, RemoteServer, ComponentResolver) read from this
 * registry instead of requiring a separate ModuleRegistry instance.
 */
export class HypenApp {
  /** @internal */
  readonly _registry = new Map<string, HypenModuleDefinition>();

  /**
   * Define the initial state for a module
   */
  defineState<T>(
    initial: T,
    options?: { persist?: boolean; version?: number; name?: string }
  ): HypenAppBuilder<T> {
    return new HypenAppBuilder(initial, options, this._registry);
  }

  /**
   * Convenience: start a module builder with a name pre-set.
   *
   * @example
   * ```typescript
   * export default app
   *   .module("Settings")
   *   .defineState({ theme: "dark" })
   *   .ui(hypen`...`);
   * ```
   */
  module(name: string) {
    const registry = this._registry;
    return {
      defineState: <T>(
        initial: T,
        options?: { persist?: boolean; version?: number }
      ): HypenAppBuilder<T> => {
        return new HypenAppBuilder(initial, { ...options, name }, registry);
      },
    };
  }

  // ---------------------------------------------------------------------------
  // Registry API
  // ---------------------------------------------------------------------------

  /**
   * Get a registered module definition by component name.
   */
  get(name: string): HypenModuleDefinition | undefined {
    return this._registry.get(name);
  }

  /**
   * Check if a module definition is registered.
   */
  has(name: string): boolean {
    return this._registry.has(name);
  }

  /**
   * Read-only view of all registered component definitions.
   */
  get components(): ReadonlyMap<string, HypenModuleDefinition> {
    return this._registry;
  }

  /**
   * Get all registered component names.
   */
  getNames(): string[] {
    return Array.from(this._registry.keys());
  }

  /**
   * Number of registered definitions.
   */
  get size(): number {
    return this._registry.size;
  }

  /**
   * Unregister a module definition.
   */
  unregister(name: string): void {
    this._registry.delete(name);
  }

  /**
   * Clear all registered definitions.
   */
  clear(): void {
    this._registry.clear();
  }
}

/**
 * The main app instance for creating modules
 */
export const app = new HypenApp();

/**
 * Module Instance - manages a running module with typed state
 */
export class HypenModuleInstance<T extends object = any> {
  private engine: IEngine;
  private definition: HypenModuleDefinition<T>;
  private state: T;
  private isDestroyed = false;
  private router: HypenRouter | null;
  private globalContext?: HypenGlobalContext;
  private stateChangeCallbacks: Array<() => void> = [];
  private dataSourceManager?: DataSourceManager;
  private dataSourceAccessor: DataSourceAccessor = {};

  constructor(
    engine: IEngine,
    definition: HypenModuleDefinition<T>,
    router?: HypenRouter | null,
    globalContext?: HypenGlobalContext
  ) {
    this.engine = engine;
    this.definition = definition;
    this.router = router ?? null;
    this.globalContext = globalContext;

    // Named state prefix — all lowercase per convention
    const statePrefix = (definition.name || "").toLowerCase();

    // Create observable state that sends only changed paths and values to engine
    // When a prefix is set, all paths are namespaced (e.g., "homepage.count")
    this.state = createObservableState<T>(definition.initialState as T & object, {
      onChange: (change: StateChange) => {
        // Send only the changed paths and their new values (not the whole state)
        this.engine.notifyStateChange(change.paths, change.newValues);
        // Notify all registered callbacks
        this.stateChangeCallbacks.forEach(cb => cb());
      },
      pathPrefix: statePrefix || undefined,
    });

    // Register with engine (initial state registration)
    // When prefixed, nest the state under the module name so the engine sees
    // e.g., { homepage: { count: 0 } } instead of { count: 0 }
    const snapshot = getStateSnapshot(this.state);
    const prefixedState = statePrefix
      ? { [statePrefix]: snapshot }
      : snapshot;

    this.engine.setModule(
      definition.name || "AnonymousModule",
      definition.actions,
      definition.stateKeys,
      prefixedState
    );

    // Register action handlers with flexible parameter support
    for (const [actionName, handler] of definition.handlers.onAction) {
      log.debug(`Registering action handler: ${actionName} for module ${definition.name}`);
      this.engine.onAction(actionName, async (action: Action) => {
        log.debug(`Action handler fired: ${actionName}`, action);

        const actionCtx: ActionContext = {
          name: action.name,
          payload: action.payload,
          sender: action.sender,
        };

        const context: GlobalContext | undefined = this.globalContext
          ? this.createGlobalContextAPI()
          : undefined;

        // Use Result type for error handling
        const result = await this.executeAction(actionName, handler, {
          action: actionCtx,
          state: this.state,
          context: context!,
          dataSources: this.dataSourceAccessor,
        });

        if (!result.ok) {
          const shouldRethrow = await this.handleError(result.error, { actionName });
          if (shouldRethrow) {
            throw result.error;
          }
        } else {
          log.debug(`Action handler completed: ${actionName}`);
        }
      });
    }

    // Auto-register __hypen_bind for .bind() two-way binding support
    this.engine.onAction("__hypen_bind", (action: Action) => {
      const payload = action.payload as { path?: string; value?: unknown } | null;
      if (!payload?.path) return;

      const segments = payload.path.split(".");
      let target: any = this.state;
      for (let i = 0; i < segments.length - 1; i++) {
        const seg = segments[i]!;
        target = target?.[seg];
        if (target == null) return;
      }
      const lastSeg = segments[segments.length - 1]!;
      target[lastSeg] = payload.value;
    });

    // Call onCreated
    this.callCreatedHandler();
  }

  /**
   * Create the global context API for this module
   */
  private createGlobalContextAPI(): GlobalContext {
    if (!this.globalContext) {
      throw new Error("Global context not available");
    }

    const ctx = this.globalContext;
    const api: GlobalContext = {
      getModule: (id: string) => ctx.getModule(id),
      hasModule: (id: string) => ctx.hasModule(id),
      getModuleIds: () => ctx.getModuleIds(),
      getGlobalState: () => ctx.getGlobalState(),
      emit: (event: string, payload?: any) => ctx.emit(event, payload),
      on: (event: string, handler: (payload?: any) => void) =>
        ctx.on(event, handler),
      router: this.router,
    };

    // Expose hypen engine for built-in components (if available)
    const ctxRecord = ctx as unknown as Record<string, unknown>;
    if (ctxRecord.__hypenEngine) {
      (api as Record<string, unknown>).__hypenEngine = ctxRecord.__hypenEngine;
    }

    return api;
  }

  /**
   * Execute an action handler with Result-based error handling
   * Handles both synchronous throws and async rejections
   */
  private async executeAction(
    actionName: string,
    handler: ActionHandler<T, any>,
    ctx: ActionHandlerContext<T>
  ): Promise<Result<void, ActionError>> {
    try {
      // Wrap in try-catch to handle synchronous throws
      const result = handler(ctx);
      // Await in case handler returns a promise
      await result;
      return Ok(undefined);
    } catch (e) {
      return Err(new ActionError(actionName, e));
    }
  }

  /**
   * Handle an error with module-level error handler support
   * Falls back to default behavior (emit + log) if no handler or handler doesn't suppress
   * @returns true if the error should be rethrown
   */
  private async handleError(
    error: HypenError,
    context: { actionName?: string; lifecycle?: ErrorContext<T>["lifecycle"] }
  ): Promise<boolean> {
    const errorCtx: ErrorContext<T> = {
      error,
      state: this.state,
      actionName: context.actionName,
      lifecycle: context.lifecycle,
    };

    // Call module-level error handler if defined
    if (this.definition.handlers.onError) {
      try {
        const result = await this.definition.handlers.onError(errorCtx);

        // Check if error was handled
        if (result && typeof result === "object") {
          if ("handled" in result && result.handled) {
            // Error was handled, skip default behavior
            return false;
          }
          if ("rethrow" in result && result.rethrow) {
            // Signal caller to rethrow
            return true;
          }
          // Note: 'retry' would need to be handled at the action execution level
          // For now, we just continue with default behavior
        }
      } catch (handlerError) {
        // Error in error handler - log and continue with default behavior
        log.error("Error in onError handler:", handlerError);
      }
    }

    // Default behavior: emit to global context and log
    if (this.globalContext) {
      const eventContext = context.actionName
        ? `action:${context.actionName}`
        : context.lifecycle
          ? `lifecycle:${context.lifecycle}`
          : "unknown";

      this.globalContext.emit("error", {
        message: error.message,
        error,
        context: eventContext,
      });
    }

    log.error(
      `${context.actionName ? `Action "${context.actionName}"` : `Lifecycle "${context.lifecycle}"`} error:`,
      error
    );

    return false;
  }

  /**
   * Call the onCreated handler and connect data source plugins
   */
  private async callCreatedHandler(): Promise<void> {
    // Initialize data source plugins if any are registered
    if (this.definition.dataSources?.length) {
      // The engine must implement IDataSourceEngine methods
      const dsEngine = this.engine as unknown as IDataSourceEngine;
      this.dataSourceManager = new DataSourceManager(dsEngine);

      for (const { plugin, config } of this.definition.dataSources) {
        try {
          await this.dataSourceManager.use(plugin, config);
          // Wrap plugin in a Proxy so unknown method calls forward to plugin.call():
          //   dataSources.spacetime.sendMessage(text) → plugin.call("sendMessage", text)
          this.dataSourceAccessor[plugin.name] = new Proxy(plugin, {
            get(target, prop) {
              if (typeof prop === 'string' && !(prop in target)) {
                return (...args: unknown[]) => target.call(prop, ...args);
              }
              return (target as unknown as Record<string | symbol, unknown>)[prop];
            },
          }) as typeof plugin & Record<string, (...args: unknown[]) => Promise<unknown>>;
        } catch (e) {
          log.error(`Failed to connect data source "${plugin.name}":`, e);
        }
      }
    }

    if (this.definition.handlers.onCreated) {
      const context = this.globalContext ? this.createGlobalContextAPI() : undefined;
      try {
        await this.definition.handlers.onCreated(this.state, context);
      } catch (e) {
        const error = e instanceof HypenError ? e : new ActionError("onCreated", e);
        const shouldRethrow = await this.handleError(error, { lifecycle: "created" });
        if (shouldRethrow) {
          throw error;
        }
      }
    }
  }

  /**
   * Register a callback to be notified when state changes
   */
  onStateChange(callback: () => void): void {
    this.stateChangeCallbacks.push(callback);
  }

  /**
   * Destroy the module instance and disconnect all data source plugins
   */
  async destroy(): Promise<void> {
    if (this.isDestroyed) return;

    // Disconnect all data source plugins
    if (this.dataSourceManager) {
      try {
        await this.dataSourceManager.disconnectAll();
      } catch (e) {
        log.error("Error disconnecting data sources:", e);
      }
      this.dataSourceManager = undefined;
      this.dataSourceAccessor = {};
    }

    if (this.definition.handlers.onDestroyed) {
      try {
        await this.definition.handlers.onDestroyed(this.state);
      } catch (e) {
        const error = e instanceof HypenError ? e : new ActionError("onDestroyed", e);
        const shouldRethrow = await this.handleError(error, { lifecycle: "destroyed" });
        if (shouldRethrow) {
          throw error;
        }
      }
    }

    this.isDestroyed = true;
  }

  /**
   * Get the current state (returns a snapshot)
   */
  getState(): T {
    return getStateSnapshot(this.state);
  }

  /**
   * Get the live observable state
   */
  getLiveState(): T {
    return this.state;
  }

  /**
   * Update state directly (merges with existing state)
   */
  updateState(patch: Partial<T>): void {
    Object.assign(this.state, patch);
  }
}
