/**
 * Copyright IBM Corp. 2024, 2025
 */

/**
 * Manages all the variable contexts with unique context.
 */
import { EnvVar, VariableContext } from './variable-context.js';

type EnvInput =
  | Record<string, any>
  | Record<string, EnvVar>
  | EnvVar[]
  | Array<Record<string, any>>;
export class ContextManager {
  private contexts = new Map<string, VariableContext>();
  // This will be accessible for entire
  private globalContext = new VariableContext();

  createContext(contextId: string): VariableContext {
    if (!this.contexts.has(contextId)) {
      this.contexts.set(contextId, new VariableContext());
    }
    return this.contexts.get(contextId)!;
  }

  getContext(contextId: string): VariableContext {
    return this.createContext(contextId)!;
  }

  deleteContext(contextId: string): void {
    this.contexts.delete(contextId);
  }

  listContexts(): string[] {
    return [...this.contexts.keys()];
  }

  getGlobalContext(): VariableContext {
    return this.globalContext;
  }

  // Don't use this unless we want to clean context manager
  clearAll(): void {
    this.contexts.clear();
    this.globalContext.clear();
  }

  loadEnv(contextId: string, envVars: EnvInput): void {
    const context = this.createContext(contextId);

    // If it's array of key-value pairs with `key`, `value`, `isSecret`
    if (
      Array.isArray(envVars) &&
      envVars.every((e) => 'key' in e && 'value' in e)
    ) {
      for (const env of envVars) {
        context.setEnvVariable(env.key, env.value, env.isSecret as boolean);
      }
      return;
    }

    // If it's array of plain objects
    const envArray = Array.isArray(envVars) ? envVars : [envVars];
    for (const env of envArray) {
      for (const [key, val] of Object.entries(env)) {
        if (
          val &&
          typeof val === 'object' &&
          'value' in val &&
          'isSecret' in val
        ) {
          context.setEnvVariable(key, val.value, val.isSecret as boolean);
        } else {
          context.setEnvVariable(key, val);
        }
      }
    }
  }

  // Resolve variable name with the value store in context
  // which will return the resolved value in the passed input
  resolve(contextId: string, input: any): any {
    if (input == undefined) {
      return input;
    }
    const context = this.contexts.get(contextId);
    if (!context) throw new Error(`Context '${contextId}' not found.`);

    if (Array.isArray(input)) {
      return input.map((item) => this.resolve(contextId, item));
    } else if (typeof FormData !== 'undefined' && input instanceof FormData) {
      return input;
    } else if (typeof input === 'object' && input !== null) {
      const result: Record<string, any> = {};
      for (const [key, value] of Object.entries(input)) {
        result[key] = this.resolve(contextId, value);
      }
      return result;
    }
    // for primitive types
    return this.resolveValue(contextId, input);
  }

  // For any string which have expression like "result.0.name" instead of variable
  // typeof input parameter is set to any, in runtime it should be only string.
  // to validate unit test, it is set to any
  resolveExpression(contextId: string, input: any): any {
    const context = this.contexts.get(contextId);
    if (!context) throw new Error(`Context '${contextId}' not found.`);

    if (typeof input !== 'string') {
      throw new Error(`${input} should be a string expression`);
    }
    const result = this.resolvePath(contextId, input);
    return result;
  }

  // ResolveValue function will understand whether the input is variable or statement with variable
  // based on that, resolveExpression will be invoked.
  private resolveValue(contextId: string, input: any): any {
    // If input is non string, then it won't be a variable
    if (typeof input !== 'string' || !input.includes('${')) {
      return input;
    }
    const fullVar = input.match(/^\$\{([^{}]+)\}$/);
    if (fullVar) {
      return this.resolvePath(contextId, fullVar[1]);
    }

    return this.replaceRecursiveExpression(contextId, input);
  }

  // For mixed string: recursively replace ALL ${...} patterns until none left
  private replaceRecursiveExpression(contextId: string, input: string) {
    if (!input) {
      return input;
    }
    while (input.includes('${')) {
      input = input.replace(/\$\{([^{}]*)\}/g, (_: any, expr: any) => {
        try {
          const value = this.resolvePath(contextId, expr);
          // If value is undefined, return empty string
          if (value === undefined) {
            return '';
          }
          // Handle circular structure in JSON stringification
          return typeof value === 'object'
            ? this.safeStringify(value)
            : String(value);
        } catch (e) {
          console.error(e);
          return ''; // Return empty string on error
        }
      });
    }
    return input;
  }

  // Safely stringify objects handling circular references
  private safeStringify(obj: any): string {
    try {
      // Use a WeakSet to track objects that have been seen
      const seen = new WeakSet();
      return JSON.stringify(obj, (key, value) => {
        // If value is an object and not null
        if (typeof value === 'object' && value !== null) {
          // If we've seen this object before, return a placeholder to avoid circular reference
          if (seen.has(value)) {
            return '[Circular Reference]';
          }
          // Add the value to our set of seen objects
          seen.add(value);
        }
        return value;
      });
    } catch {
      // Fallback if JSON.stringify still fails
      return '[Complex Object]';
    }
  }

  private resolvePath(contextId: string, expr: string): any {
    if (!expr) {
      return expr;
    }
    const context = this.createContext(contextId);
    const global = this.getGlobalContext();
    const [baseKey, ...pathParts] = expr.split('.');

    let resolved = context?.getValue(baseKey) ?? global?.getValue(baseKey);

    if (resolved === undefined) {
      return undefined;
    }

    for (const part of pathParts) {
      const key = /^\d+$/.test(part) ? Number(part) : part;
      if (resolved === undefined || resolved === null) {
        return undefined;
      }

      // If the key doesn't exist in the object, return undefined instead of throwing an error
      if (!(key in resolved)) {
        return undefined;
      }

      resolved = resolved[key];
    }
    return resolved;
  }
}

export const VCM = new ContextManager();
