/**
 * @file stega.ts
 * @description Stega encoding support for visual editing with optional @vercel/stega dependency
 * @author Invisible Cities Agency
 * @license MIT
 */

// Edge-safe ESM import for @vercel/stega (tiny, browser/edge-friendly)
// This package has no Node dependencies and is safe in Edge bundles.
import { vercelStegaEncode as _vercelStegaEncode, vercelStegaCombine as _vercelStegaCombine, vercelStegaClean as _vercelStegaClean } from '@vercel/stega';
// NOTE: Library API: vercelStegaEncode(json) -> invisible suffix, vercelStegaCombine(text, json, skip?) -> combined string
const vercelStegaEncode: ((metadata: any) => string) | undefined = _vercelStegaEncode as any;
const vercelStegaCombine: ((value: string, metadata: any, skip?: 'auto' | boolean) => string) | undefined = _vercelStegaCombine as any;
const vercelStegaClean: (<T = any>(value: T) => T) | undefined = _vercelStegaClean as any;

/**
 * Stega configuration options
 */
export interface StegaConfig {
  enabled: boolean;
  studioUrl?: string;
  basePath?: string;
  filter?: (path: string) => boolean;
  projectId?: string;
  dataset?: string;
}

/**
 * Inline stega implementation for when @vercel/stega is not available
 * Uses Unicode code points to encode invisible metadata
 */
const STEGA_CODES = {
  // Tuple ensures index access returns number (not possibly undefined)
  base4: [8203, 8204, 8205, 65279] as const,
  hex: {
    0: 8203, 1: 8204, 2: 8205, 3: 8290, 4: 8291, 5: 8288,
    6: 65279, 7: 8289, 8: 119155, 9: 119156,
    a: 119157, b: 119158, c: 119159, d: 119160, e: 119161, f: 119162
  } as Record<string | number, number>
};

const STEGA_PREFIX = new Array(4).fill(String.fromCodePoint(STEGA_CODES.base4[0])).join('');

/**
 * Encode data as invisible Unicode characters (base4 encoding)
 */
function encodeInvisibleBase4(data: any): string {
  const jsonStr = JSON.stringify(data);
  const encoded = Array.from(jsonStr).map(char => {
    const charCode = char.charCodeAt(0);
    if (charCode > 255) {
      throw new Error(`Only ASCII can be encoded. Error on character ${char} (${charCode})`);
    }
    // Convert to base-4 and encode as invisible characters
    return Array.from(charCode.toString(4).padStart(4, '0'))
      .map(digit => {
        // digit is one of '0' | '1' | '2' | '3'
        const idx = parseInt(digit, 10) as 0 | 1 | 2 | 3;
        return String.fromCodePoint(STEGA_CODES.base4[idx]);
      })
      .join('');
  }).join('');
  
  return `${STEGA_PREFIX}${encoded}`;
}

/**
 * Check if a value should skip stega encoding
 */
function shouldSkipEncoding(value: string): boolean {
  // Skip dates and URLs as they shouldn't be encoded
  const isDate = /\d+(?:[-:\/]\d+){2}(?:T\d+(?:[-:\/]\d+){1,2}(\.\d+)?Z?)?/.test(value);
  const isUrl = (() => {
    try {
      new URL(value, value.startsWith('/') ? 'https://example.com' : undefined);
      return true;
    } catch {
      return false;
    }
  })();
  // Skip Sanity asset/file id strings like image-<hash>-<WxH>-<ext>
  const isSanityAssetId = /^(image|file)-[A-Za-z0-9]+-\d+x\d+-[A-Za-z0-9]+$/.test(value);
  
  return isDate || isUrl || isSanityAssetId;
}

/**
 * Determine if the current path points to structured fields that must not be stega-encoded
 */
function isStructuredPath(path: Array<string | number>): boolean {
  if (!path.length) return false;
  const last = String(path[path.length - 1]);
  // Any Sanity meta or reference/slug/asset/urlish fields
  const disallowed = new Set([
    '_id', '_ref', '_key', '_type',
    'slug', 'current',
    'asset', 'path',
    'href', 'url', 'src'
  ]);
  if (disallowed.has(last)) return true;
  // If parent key is 'asset', skip child values as well
  if (path.length >= 2 && String(path[path.length - 2]) === 'asset') return true;
  return false;
}

/**
 * Encode stega metadata into a string value
 */
function encodeStegaString(value: string, metadata: any, config: StegaConfig): string {
  if (!config.enabled || !metadata) {
    return value;
  }
  
  // Skip encoding for dates and URLs
  if (shouldSkipEncoding(value)) {
    return value;
  }
  
  // Skip encoding when there is no visible content to preserve
  if (typeof value !== 'string' || value.trim().length === 0) {
    return value;
  }

  // Skip if already stega-encoded to avoid double payloads
  try {
    if (vercelStegaClean && vercelStegaClean(value) !== value) {
      return value;
    }
  } catch {
    // ignore and continue
  }
  
  // Use @vercel/stega if available, otherwise use inline implementation
  if (vercelStegaEncode) {
    // Prefer combine when available; fallback to appending encoded invisible suffix
    if (vercelStegaCombine) return vercelStegaCombine(value, metadata, 'auto');
    return `${value}${vercelStegaEncode(metadata)}`;
  }
  
  // Inline implementation
  return `${value}${encodeInvisibleBase4(metadata)}`;
}

/**
 * Recursively encode stega metadata into result data
 */
function encodeStegaInResult(
  data: any,
  sourceMap: any,
  config: StegaConfig,
  path: Array<string | number> = []
): any {
  if (!config.enabled || !sourceMap) {
    return data;
  }
  
  // Handle null/undefined
  if (data == null) {
    return data;
  }
  
  // Handle strings
  if (typeof data === 'string') {
    // Never encode structured/system fields
    if (isStructuredPath(path)) return data;
    const metadata = resolveSourceMapForPath(sourceMap, path, config);
    // Only encode mapped leaf values
    if (metadata && (metadata.type === undefined || metadata.type === 'value')) {
      return encodeStegaString(data, metadata, config);
    }
    return data;
  }
  
  // Handle arrays
  if (Array.isArray(data)) {
    return data.map((item, index) => 
      encodeStegaInResult(item, sourceMap, config, [...path, index])
    );
  }
  
  // Handle objects
  if (typeof data === 'object') {
    const result: any = {};
    for (const key in data) {
      if (data.hasOwnProperty(key)) {
        result[key] = encodeStegaInResult(
          data[key],
          sourceMap,
          config,
          [...path, key]
        );
      }
    }
    return result;
  }
  
  // Return primitives as-is
  return data;
}

/**
 * Resolve source map metadata for a given path
 */
function resolveSourceMapForPath(sourceMap: any, path: Array<string | number>, config?: StegaConfig): any {
  if (!sourceMap?.mappings) {
    return null;
  }
  
  // Convert path to JSONPath format for lookup
  const jsonPath = `$${path.map(segment => 
    typeof segment === 'number' ? `[${segment}]` : `['${segment}']`
  ).join('')}`;
  
  // Look for exact match or closest parent
  if (sourceMap.mappings[jsonPath]) {
    const mapping = sourceMap.mappings[jsonPath];
    const studioUrl = sourceMap.studioUrl || config?.studioUrl;

    // Attempt to build a Studio Edit Intent URL: /intent/edit/id=<docId>;path=<fieldPath>
    let href: string | undefined;
    try {
      const src = mapping?.source;
      const docId = typeof src?.document === 'number' ? sourceMap.documents?.[src.document]?._id : undefined;
      const fieldPath = typeof src?.path === 'number' ? sourceMap.paths?.[src.path] : undefined;

      if (studioUrl && docId) {
        // Normalize studio base to exclude trailing /presentation if present
        const studioBase = String(studioUrl).replace(/\/?presentation\/?$/, '').replace(/\/$/, '');
        const pathParam = fieldPath ? `;path=${encodeURIComponent(fieldPath)}` : '';
        href = `${studioBase}/intent/edit/id=${encodeURIComponent(docId)}${pathParam}`;
      }
    } catch {
      // ignore href build errors
    }

    return {
      // Canonical hints for overlay decoders
      _origin: 'sanity',
      projectId: config?.projectId,
      dataset: config?.dataset,
      studioUrl,
      path: jsonPath,
      source: sourceMap.source,
      href,
      ...mapping,
    };
  }
  
  // Try to find parent paths
  let currentPath = jsonPath;
  while (currentPath.includes('[') || currentPath.includes('.')) {
    const lastIndex = Math.max(
      currentPath.lastIndexOf('['),
      currentPath.lastIndexOf('.')
    );
    if (lastIndex === -1) break;
    
    currentPath = currentPath.substring(0, lastIndex);
    if (sourceMap.mappings[currentPath]) {
      return {
        _origin: 'sanity',
        projectId: config?.projectId,
        dataset: config?.dataset,
        studioUrl: sourceMap.studioUrl,
        source: sourceMap.source,
        path: currentPath,
        ...sourceMap.mappings[currentPath]
      };
    }
  }
  
  return null;
}

/**
 * Get the studio URL from environment or config
 */
function getStudioUrl(config?: Partial<StegaConfig>): string | undefined {
  if (config?.studioUrl) {
    return config.studioUrl;
  }
  
  // Try environment variables
  const envStudioUrl = process.env.NEXT_PUBLIC_SANITY_STUDIO_URL || 
                       process.env.SANITY_STUDIO_URL;
  
  if (envStudioUrl) {
    return envStudioUrl;
  }
  
  // Default based on environment
  if (process.env.NODE_ENV === 'development') {
    // Prefer HTTPS proxy for cookie/iframe parity with Presentation
    return 'https://localhost:3334/presentation';
  }
  
  return undefined;
}

/**
 * Check if stega should be enabled
 */
export function shouldEnableStega(isDraftMode: boolean, config?: Partial<StegaConfig>): boolean {
  // Explicit config takes precedence
  if (config?.enabled !== undefined) {
    return config.enabled;
  }
  
  // Default: enable in draft mode or development
  return isDraftMode || process.env.NODE_ENV === 'development';
}

/**
 * Process Sanity response to handle stega encoding
 */
export function processStegaResponse(
  response: any,
  isDraftMode: boolean,
  config?: Partial<StegaConfig> | StegaConfig
): any {
  // If stega is not enabled, return just the result
  if (!shouldEnableStega(isDraftMode, config)) {
    return response.result;
  }
  
  // When stega is enabled, we need to encode the source map into the result
  const result = response.result;
  const sourceMap = response.resultSourceMap;
  
  if (sourceMap && config?.enabled) {
    // Add studio URL to source map for visual editing
    const studioUrl = getStudioUrl(config);
    if (studioUrl && sourceMap) {
      sourceMap.studioUrl = studioUrl;
    }
    
    // Encode stega metadata into the result
    const fullConfig = buildStegaConfig(isDraftMode, config);
    return encodeStegaInResult(result, sourceMap, fullConfig);
  }
  
  return result;
}

/**
 * Build stega configuration from environment and options
 */
export function buildStegaConfig(
  isDraftMode: boolean,
  options?: Partial<StegaConfig>
): StegaConfig {
  const config: Partial<StegaConfig> = options || {};
  const enabled = shouldEnableStega(isDraftMode, config);
  
  const studioUrl = getStudioUrl(config);
  const projectId = config.projectId || process.env.NEXT_PUBLIC_SANITY_PROJECT_ID;
  const dataset = config.dataset || process.env.NEXT_PUBLIC_SANITY_DATASET;

  return {
    enabled,
    ...(studioUrl !== undefined ? { studioUrl } : {}),
    ...(config.basePath !== undefined ? { basePath: config.basePath } : {}),
    ...(config.filter !== undefined ? { filter: config.filter } : {}),
    ...(projectId !== undefined ? { projectId } : {}),
    ...(dataset !== undefined ? { dataset } : {}),
  } as StegaConfig;
}

/**
 * Clean stega-encoded strings (remove invisible characters)
 * Useful for comparing values or using in business logic
 */
export function stegaClean<T = any>(value: T): T {
  if (vercelStegaClean) {
    return vercelStegaClean(value);
  }
  
  // Inline implementation - remove all stega Unicode characters
  const stegaChars = Object.values(STEGA_CODES.hex)
    .map(code => `\\u{${code.toString(16)}}`)
    .join('');
  const stegaRegex = new RegExp(`[${stegaChars}]{4,}`, 'gu');
  
  const cleanString = (str: string) => str.replace(stegaRegex, '');
  
  // Recursively clean all strings in the value
  if (typeof value === 'string') {
    return cleanString(value) as T;
  }
  
  if (Array.isArray(value)) {
    return value.map(item => stegaClean(item)) as T;
  }
  
  if (value && typeof value === 'object') {
    const cleaned: any = {};
    for (const key in value) {
      if ((value as any).hasOwnProperty(key)) {
        cleaned[key] = stegaClean((value as any)[key]);
      }
    }
    return cleaned;
  }
  
  return value;
}