"""
Runtime execution abstraction for multi-language support.

Provides a pluggable runtime system that allows PCards to execute
in different language environments (Python, JavaScript, Rust, C, etc.)
"""

import json
import logging
import subprocess
import importlib
from abc import ABC, abstractmethod
from typing import Any, Callable, Dict, List, Optional, Tuple
from enum import Enum

from mcard import MCard

# ─────────────────────────────────────────────────────────────────────────────
# Configuration
# ─────────────────────────────────────────────────────────────────────────────
#
# PTR runtime configuration is centralized in mcard.config.settings.
# Import from there for the canonical values, or use these local references
# for backward compatibility.
#
# Usage (recommended):
#     from mcard.config import settings
#     timeout = settings.ptr.lean_timeout
#
# ─────────────────────────────────────────────────────────────────────────────

MODULE_URI_PREFIX = 'module://'

# Import centralized settings
try:
    from mcard.config.settings import settings as _settings
    
    # Subprocess execution defaults (from centralized config)
    DEFAULT_TIMEOUT = _settings.ptr.default_timeout
    LEAN_TIMEOUT = _settings.ptr.lean_timeout
    JULIA_TIMEOUT = _settings.ptr.julia_timeout
    R_TIMEOUT = _settings.ptr.r_timeout
    
    # Allowed imports for sandboxed Python execution
    ALLOWED_IMPORTS = _settings.ptr.allowed_imports
    
    # Safe builtins for sandboxed Python execution
    SAFE_BUILTINS = _settings.ptr.safe_builtins
    
    # Runtime command configurations
    RUNTIME_CONFIG = _settings.ptr.runtime_config

except ImportError:
    # Fallback for edge cases where settings can't be imported
    DEFAULT_TIMEOUT = 5
    LEAN_TIMEOUT = 30
    JULIA_TIMEOUT = 15
    R_TIMEOUT = 10
    
    ALLOWED_IMPORTS = {
        'math': 'math', 'json': 'json', 'yaml': 'yaml', 'pathlib': 'pathlib',
        'typing': 'typing', 'hashlib': 'hashlib', 'mcard': 'mcard',
        'os': 'os', 'time': 'time', 'random': 'random', 'logging': 'logging',
    }
    
    SAFE_BUILTINS = {
        'int', 'float', 'str', 'len', 'range', 'list', 'dict', 'tuple', 'set',
        'abs', 'round', 'max', 'min', 'sum', 'sorted', 'enumerate', 'zip',
        'ValueError', 'Exception', '__build_class__', 'super', 'open',
        'all', 'any', 'bool', 'isinstance', 'bytes', 'print', 'globals',
    }
    
    RUNTIME_CONFIG = {
        'python': {'command': 'python3', 'version_flag': '--version'},
        'javascript': {'command': 'node', 'version_flag': '--version', 'eval_flag': '--eval'},
        'lean': {'command': 'lean', 'version_flag': '--version', 'run_flag': '--run'},
        'r': {'command': 'Rscript', 'version_flag': '--version'},
        'julia': {'command': 'julia', 'version_flag': '--version'},
        'wasm': {'command': None},
    }


# ─────────────────────────────────────────────────────────────────────────────
# Runtime Types
# ─────────────────────────────────────────────────────────────────────────────

class RuntimeType(Enum):
    """Supported runtime types."""
    PYTHON = "python"
    JAVASCRIPT = "javascript"
    RUST = "rust"
    C = "c"
    WASM = "wasm"
    LEAN = "lean"
    R = "r"
    JULIA = "julia"
    LLM = "llm"
    LAMBDA = "lambda"
    NETWORK = "network"


class BoundaryType(Enum):
    """CLM execution boundary type.
    
    Determines how a CLM is executed relative to the caller:
    
    INTRINSIC: Process runs within the same execution context (same process).
               Best for lightweight, trusted operations. No serialization overhead.
               
    EXTRINSIC: Process runs in a separate process/container/network.
               Required for untrusted code, language isolation, or distributed execution.
               Requires serialization/deserialization of inputs and outputs.
    """
    INTRINSIC = "intrinsic"
    EXTRINSIC = "extrinsic"


# ─────────────────────────────────────────────────────────────────────────────
# Base Executor Classes
# ─────────────────────────────────────────────────────────────────────────────

class RuntimeExecutor(ABC):
    """Abstract base class for runtime executors.
    
    Each executor manages a specific runtime environment (Python, JavaScript, etc.)
    and can operate in either intrinsic or extrinsic boundary mode.
    """
    
    # Default boundary type for this runtime
    boundary_type: BoundaryType = BoundaryType.INTRINSIC
    
    def __init__(self):
        self.logger = logging.getLogger(self.__class__.__name__)
    
    @abstractmethod
    def execute(self, concrete_impl: Dict[str, Any], target: MCard, context: Dict[str, Any]) -> Any:
        """Execute the operation in the runtime environment."""
        pass
    
    @abstractmethod
    def validate_environment(self) -> bool:
        """Check if runtime environment is available."""
        pass
    
    def get_boundary_type(self) -> BoundaryType:
        """Get the boundary type for this runtime.
        
        Override in subclasses to provide dynamic boundary determination.
        """
        return self.boundary_type
    
    def is_intrinsic(self) -> bool:
        """Check if this runtime operates within the same process."""
        return self.get_boundary_type() == BoundaryType.INTRINSIC
    
    def is_extrinsic(self) -> bool:
        """Check if this runtime operates in a separate process."""
        return self.get_boundary_type() == BoundaryType.EXTRINSIC
    
    def get_runtime_status(self) -> Dict[str, Any]:
        """Get detailed status information about this runtime."""
        return {
            'available': self.validate_environment(),
            'version': None,
            'command': self.__class__.__name__.replace('Runtime', '').lower(),
            'details': ''
        }


class SubprocessRuntime(RuntimeExecutor):
    """
    Base class for runtimes that execute via subprocess.
    Reduces duplication across Rust/C/Lean/R/Julia.
    
    Note: Subprocess runtimes are EXTRINSIC - they run in separate processes.
    """
    
    # Subprocess runtimes are extrinsic by default
    boundary_type: BoundaryType = BoundaryType.EXTRINSIC
    
    runtime_name: str = ""
    command: Optional[str] = None
    version_flag: str = "--version"
    timeout: int = DEFAULT_TIMEOUT
    
    def _run_subprocess(
        self,
        cmd: List[str],
        target: MCard,
        context: Dict,
        timeout: Optional[int] = None
    ) -> str:
        """Execute subprocess and return output or error string."""
        try:
            result = subprocess.run(
                cmd,
                input=target.get_content(),
                capture_output=True,
                timeout=timeout or self.timeout
            )
            if result.returncode != 0:
                return f"Error: {self.runtime_name} execution failed: {result.stderr.decode()}"
            return result.stdout.decode('utf-8')
        except subprocess.TimeoutExpired:
            return f"Error: {self.runtime_name} execution timed out"
        except Exception as e:
            return f"Error executing {self.runtime_name}: {e}"
    
    def _check_command(self, cmd: str, flag: str, timeout: int = 2) -> bool:
        """Check if a command is available."""
        try:
            result = subprocess.run([cmd, flag], capture_output=True, timeout=timeout)
            return result.returncode == 0
        except Exception:
            return False
    
    def validate_environment(self) -> bool:
        if not self.command:
            return True  # No command needed (e.g., compiled binaries)
        return self._check_command(self.command, self.version_flag)


# ─────────────────────────────────────────────────────────────────────────────
# Python Runtime
# ─────────────────────────────────────────────────────────────────────────────

class PythonRuntime(RuntimeExecutor):
    """Python runtime executor - executes Python code directly in-process."""
    
    # Operation handlers mapping
    OPERATIONS: Dict[str, Callable] = {}
    
    def __init__(self):
        super().__init__()
        # Register built-in operations
        self.OPERATIONS = {
            'identity': self._op_identity,
            'transform': self._op_transform,
            'arithmetic': self._op_arithmetic,
            'string_op': self._op_string,
            'fetch_url': self._op_fetch_url,
            'custom': self._op_custom,
            'loader': self._op_loader,
            'ingest_files': self._op_loader,
            'load_files': self._op_loader,
            'session_record': self._op_session_record,
        }
    
    def execute(self, concrete_impl: Dict[str, Any], target: MCard, context: Dict[str, Any]) -> Any:
        # Check for module:// syntax first
        code_file = concrete_impl.get('code_file', '')
        if code_file.startswith(MODULE_URI_PREFIX):
            return self._execute_module_logic(code_file, concrete_impl, target, context)
        
        # Determine operation
        operation = concrete_impl.get('operation')
        if not operation:
            operation = concrete_impl.get('builtin')
        
        if not operation:
            # Auto-detect custom operation if code is present
            if 'code_file' in concrete_impl or 'code' in concrete_impl:
                operation = 'custom'
            else:
                operation = 'identity'
                
        handler = self.OPERATIONS.get(operation)
        
        if handler:
            return handler(concrete_impl, target, context)
        return f"Executed {operation} on {target.hash}"
    
    # ─── Operations ───────────────────────────────────────────────────────────
    
    def _op_identity(self, impl: Dict, target: MCard, ctx: Dict) -> Any:
        return target.get_content()
    
    def _op_transform(self, impl: Dict, target: MCard, ctx: Dict) -> Any:
        transforms = {
            'upper_case': lambda t: t.get_content().decode('utf-8').upper().encode('utf-8'),
            'count_bytes': lambda t: len(t.get_content()),
        }
        func = impl.get('transform_function')
        return transforms.get(func, lambda t: t.get_content())(target)
    
    def _op_arithmetic(self, impl: Dict, target: MCard, ctx: Dict) -> Any:
        params = {**impl.get('params', {}), **ctx}
        op, operand = params.get('op'), params.get('operand')
        
        try:
            val = float(target.get_content().decode('utf-8'))
        except ValueError:
            return "Error: Target content is not a valid number"
        
        ops = {
            'add': lambda x, y: x + y,
            'sub': lambda x, y: x - y,
            'mul': lambda x, y: x * y,
            'div': lambda x, y: x / y if y != 0 else "Error: Division by zero"
        }
        return ops.get(op, lambda x, y: f"Error: Unknown operation '{op}'")(val, operand)
    
    def _op_string(self, impl: Dict, target: MCard, ctx: Dict) -> Any:
        params = {**impl.get('params', {}), **ctx}
        func = params.get('func')
        s = target.get_content().decode('utf-8')
        
        ops = {
            'reverse': lambda: s[::-1],
            'len': lambda: len(s),
            'split': lambda: s.split(params.get('delimiter', ' '))
        }
        return ops.get(func, lambda: f"Error: Unknown function '{func}'")()
    
    def _op_fetch_url(self, impl: Dict, target: MCard, ctx: Dict) -> Any:
        import urllib.request
        url = target.get_content().decode('utf-8').strip()
        try:
            with urllib.request.urlopen(url, timeout=5) as resp:
                return resp.read().decode('utf-8')[:1000]
        except Exception as e:
            return f"Error fetching URL: {e}"
    
    def _op_loader(self, impl: Dict, target: MCard, ctx: Dict) -> Any:
        """Built-in loader operation using mcard.loader.
        
        Loads files from a directory into a CardCollection.
        
        Context params:
            source_dir: Directory path to load files from
            db_path: Database path for storage
            recursive: Whether to recursively scan directories (default: True)
            include_problematic: Whether to include problematic files (default: False)
        """
        from pathlib import Path
        import time
        from mcard.loader import load_file_to_collection
        from mcard.model.card_collection import CardCollection
        
        # Extract parameters from context (merged balanced + test context)
        params = ctx.get('params', {})
        bal = ctx.get('balanced', {})
        
        # Get input/output args from balanced or context
        input_args = {**bal.get('input_arguments', {}), **ctx.get('input_arguments', {})}
        output_args = {**bal.get('output_arguments', {}), **ctx.get('output_arguments', {})}
        
        # Merge all parameter sources
        all_params = {**input_args, **output_args, **params}
        
        source_dir = all_params.get('source_dir', 'test_data')
        db_path = all_params.get('db_path', 'data/loader.db')
        recursive = all_params.get('recursive', True)
        include_problematic = all_params.get('include_problematic', False)
        
        # Resolve source_dir relative to project root if needed
        source_path = Path(source_dir)
        if not source_path.exists():
            # Try resolving from current working directory
            import os
            cwd = Path(os.getcwd())
            source_path = cwd / source_dir
            if not source_path.exists():
                return {
                    'success': False,
                    'error': f'Source directory not found: {source_dir}'
                }
        
        # Ensure db directory exists
        db_file = Path(db_path)
        db_file.parent.mkdir(parents=True, exist_ok=True)
        
        # Clean slate - remove existing db
        if db_file.exists():
            try:
                import os
                os.remove(db_file)
            except OSError:
                pass
        
        try:
            # Initialize collection
            collection = CardCollection(db_path=str(db_path))
            
            # Load files using built-in loader
            t0 = time.time()
            loader_response = load_file_to_collection(
                source_path,
                collection,
                recursive=recursive,
                include_problematic=include_problematic
            )
            load_time = time.time() - t0
            
            # Unpack response
            if isinstance(loader_response, dict) and "results" in loader_response:
                results = loader_response["results"]
                metrics = loader_response.get("metrics", {})
            else:
                results = loader_response
                metrics = {'files_count': len(results)}

            # Calculate metrics
            total_size = sum(r.get('size', 0) for r in results)
            
            return {
                'success': True,
                'metrics': {
                    'total_files': metrics.get('files_count', len(results)),
                    'total_directories': metrics.get('directories_count', 0),
                    'directory_levels': metrics.get('directory_levels', 0),
                    'total_size_bytes': total_size,
                    'duration_ms': round(load_time * 1000, 2)
                },
                'files': [{
                    'hash': r.get('hash', '')[:8],
                    'filename': r.get('filename', ''),
                    'content_type': r.get('content_type', '')
                } for r in results[:10]]  # Return first 10 for preview
            }
        except Exception as e:
            return {
                'success': False,
                'error': str(e)
            }
    def _op_session_record(self, impl: Dict, target: MCard, ctx: Dict) -> Any:
        # P2P Session Recording Logic (Mock/Placeholder for now)
        
        # 1. Gather all potential parameter sources
        # 'config' from yaml usually has template strings like '${params.sessionId}'
        # 'ctx' usually contains the runtime context
        # 'ctx["params"]' often contains the input arguments from test cases
        
        config = impl.get('config', {})
        ctx_params = ctx.get('params', {})
        # Merge sources: ctx params allow overriding defaults
        # We look in ctx explicitly because sometimes test runners structure it differently
        
        # 2. Extract session_id
        # Check direct keys in context/params first (highest priority)
        session_id = ctx.get('sessionId') or ctx_params.get('sessionId')
        
        # Check config with potential interpolation
        if not session_id and 'sessionId' in config:
            val = config['sessionId']
            # Simple manual interpolation if it's a string like '${params.X}'
            if isinstance(val, str) and val.startswith('${') and val.endswith('}'):
                key = val[9:-1] # strip '${params.' and '}' -> 'sessionId'
                # fallback: try simple property name if prefix differs
                if '.' in key: key = key.split('.')[-1]
                
                session_id = ctx.get(key) or ctx_params.get(key)
            else:
                # Literal value in config
                session_id = val

        # 3. Fallback to target content inspection
        if not session_id:
             try:
                 content = target.get_content().decode('utf-8')
                 data = json.loads(content)
                 if isinstance(data, dict):
                     session_id = data.get('sessionId')
             except:
                 pass
        
        if not session_id:
            return {'success': False, 'error': 'session_id is required'}
            
        return {'success': True, 'session_id': session_id, 'recorded': True}
    def _op_custom(self, impl: Dict, target: MCard, ctx: Dict) -> Any:
        code_file = impl.get('code_file', '')
        if code_file.startswith(MODULE_URI_PREFIX):
            return self._execute_module_logic(code_file, impl, target, ctx)
        
        code = impl.get('code', '')
        if not code:
            return "Error: No code provided"
        
        return self._exec_sandboxed(code, impl, target, ctx)
    
    # ─── Sandboxed Execution ──────────────────────────────────────────────────
    
    def _make_safe_import(self) -> Callable:
        """Create a safe import function."""
        import math
        def safe_import(name, globals=None, locals=None, fromlist=(), level=0):
            if name in ALLOWED_IMPORTS:
                return importlib.import_module(name)
            raise ImportError(f"Import of '{name}' is not allowed")
        return safe_import
    
    def _make_namespace(self, target: MCard, context: Dict) -> Dict:
        """Create a sandboxed namespace for code execution."""
        import math
        import builtins as builtins_module
        
        # Build safe builtins dict from the builtins module
        safe_builtins = {}
        for name in SAFE_BUILTINS:
            if hasattr(builtins_module, name):
                safe_builtins[name] = getattr(builtins_module, name)
        
        safe_builtins['__import__'] = self._make_safe_import()
        safe_builtins['__build_class__'] = builtins_module.__build_class__
        
        return {
            '__builtins__': safe_builtins,
            'target': target.get_content(),
            'context': context,
            'result': None,
            'math': math,
            '__name__': '__clm_runtime__'
        }
    
    _compilation_cache: Dict[str, Any] = {}

    def _exec_sandboxed(self, code: str, impl: Dict, target: MCard, ctx: Dict) -> Any:
        """Execute code in a sandboxed namespace with compilation caching."""
        namespace = self._make_namespace(target, ctx)
        
        try:
            # Use cached code object if available to avoid re-parsing
            if code not in self._compilation_cache:
                self._compilation_cache[code] = compile(code, '<string>', 'exec')
            
            exec(self._compilation_cache[code], namespace)
            
            entry_point = impl.get('entry_point')
            if entry_point:
                return self._call_entry_point(namespace, entry_point, impl, target, ctx)
            return namespace.get('result')
        except Exception as e:
            return f"Error executing Python code: {e}"
    
    def _call_entry_point(self, ns: Dict, entry: str, impl: Dict, target: MCard, ctx: Dict) -> Any:
        """Call a function defined in namespace."""
        if entry not in ns or not callable(ns[entry]):
            return f"Error: Entry point '{entry}' not found or not callable. Keys: {list(ns.keys())}"
        
        func = ns[entry]
        arg = self._prepare_argument(impl, target, ctx)
        return func(arg)
    
    def _prepare_argument(self, impl: Dict, target: MCard, ctx: Dict) -> Any:
        """Prepare argument for entry point function."""
        inputs_def = impl.get('implementation', {}).get('inputs', {})
        
        # If context has operation or params keys, the entry point likely expects context
        # This handles test cases that specify operation and params in 'when' clause
        if ctx.get('operation') or ctx.get('params') or ctx.get('op'):
            # Merge target content into context for functions that expect a unified context
            combined = ctx.copy()
            content = target.get_content()
            if isinstance(content, bytes):
                try:
                    content = content.decode('utf-8')
                except:
                    pass
            combined['value'] = content
            combined['__input_content__'] = content
            return combined
        
        if not inputs_def:
            return target.get_content()
        
        input_name = list(inputs_def.keys())[0]
        input_type = inputs_def[input_name]
        
        if input_name == 'context':
            return ctx
        
        raw = target.get_content()
        converters = {
            'float': lambda: float(raw.decode('utf-8')),
            'int': lambda: int(raw.decode('utf-8')),
            'str': lambda: raw.decode('utf-8'),
        }
        
        try:
            return converters.get(input_type, lambda: raw)()
        except Exception:
            return raw
    
    # ─── Module Logic ─────────────────────────────────────────────────────────
    
    def _execute_module_logic(self, uri: str, impl: Dict, target: MCard, ctx: Dict) -> Any:
        """Execute logic imported from a Python module."""
        path = uri.replace(MODULE_URI_PREFIX, '')
        module_name, func_name = (path.split(':', 1) if ':' in path 
                                  else (path, impl.get('entry_point', 'logic')))
        
        try:
            module = importlib.import_module(module_name)
            func = getattr(module, func_name)
            if not callable(func):
                return f"Error: {func_name} in {module_name} is not callable"
            
            arg = target.get_content().decode('utf-8') if hasattr(target, 'get_content') else target
            return func(arg)
        except ImportError as e:
            return f"Error importing module {module_name}: {e}"
        except AttributeError:
            return f"Error: Function {func_name} not found in {module_name}"
        except Exception as e:
            return f"Error executing module logic: {e}"
    
    def validate_environment(self) -> bool:
        return True
    
    def get_runtime_status(self) -> Dict[str, Any]:
        import sys
        return {
            'available': True,
            'version': f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
            'command': RUNTIME_CONFIG['python']['command'],
            'details': f'Python {sys.version}'
        }


# ─────────────────────────────────────────────────────────────────────────────
# JavaScript Runtime
# ─────────────────────────────────────────────────────────────────────────────

class JavaScriptRuntime(SubprocessRuntime):
    """JavaScript runtime executor - executes via Node.js or Deno."""
    
    runtime_name = "JavaScript"
    
    def __init__(self, use_deno: bool = False):
        super().__init__()
        config = RUNTIME_CONFIG['deno' if use_deno else 'javascript']
        self.command = config['command']
        self.eval_flag = config.get('eval_flag', '--eval')
        self.use_deno = use_deno
    
    def execute(self, impl: Dict[str, Any], target: MCard, ctx: Dict[str, Any]) -> Any:
        if impl.get('builtin') == 'run_command':
            config = impl.get('config', {})
            cmd = config.get('command')
            if not cmd:
                return "Error: No command provided"
            
            try:
                res = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=self.timeout)
                if res.returncode != 0:
                     return f"Error: Command failed: {res.stderr}"
                return res.stdout.strip()
            except Exception as e:
                 return f"Error executing command: {e}"

        # Handle loader builtin via Node.js
        if impl.get('builtin') == 'loader':
            return self._execute_loader_builtin(impl, ctx)

        code = impl.get('code', '')
        if not code and not impl.get('entry_point', {}).get('file'):
            return "Error: No JavaScript code or file provided"
        
        input_data = {
            'target': target.get_content().decode('utf-8', errors='ignore'),
            'context': ctx
        }
        
        js_code = f"""
        const input = {json.dumps(input_data)};
        const target = input.target;
        const context = input.context;
        {code}
        console.log(JSON.stringify(result));
        """
        
        try:
            result = subprocess.run(
                [self.command, self.eval_flag, js_code],
                capture_output=True, text=True, timeout=self.timeout
            )
            if result.returncode != 0:
                return f"Error: JavaScript execution failed: {result.stderr}"
            return json.loads(result.stdout.strip())
        except subprocess.TimeoutExpired:
            return "Error: JavaScript execution timed out"
        except Exception as e:
            return f"Error executing JavaScript: {e}"

    def _execute_loader_builtin(self, impl: Dict[str, Any], ctx: Dict[str, Any]) -> Any:
        """Execute JS loader builtin via Node.js using mcard-js."""
        import os
        
        # Get parameters
        input_args = impl.get('input_arguments', {})
        source_dir = input_args.get('source_dir') or ctx.get('source_dir', 'docs')
        db_path = input_args.get('db_path') or ctx.get('db_path', 'data/loader_js.db')
        recursive = input_args.get('recursive', True)
        
        # Find mcard-js path
        project_root = os.getcwd()
        mcard_js_path = os.path.join(project_root, 'mcard-js')
        
        # Create inline script to invoke loader
        loader_script = f'''
import {{ SqliteNodeEngine }} from './src/storage/SqliteNodeEngine.js';
import {{ CardCollection }} from './src/model/CardCollection.js';
import {{ loadFileToCollection }} from './src/util/Loader.js';
import * as path from 'path';
import * as fs from 'fs';

const sourceDir = "{source_dir}";
const dbPath = "{db_path}";
const recursive = {str(recursive).lower()};

const projectRoot = path.resolve("{project_root}");
const sourcePath = path.isAbsolute(sourceDir) ? sourceDir : path.join(projectRoot, sourceDir);
const resolvedDbPath = path.isAbsolute(dbPath) ? dbPath : path.join(projectRoot, dbPath);

// Ensure directory
const dbDir = path.dirname(resolvedDbPath);
if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, {{ recursive: true }});

// Remove existing DB
if (fs.existsSync(resolvedDbPath)) fs.unlinkSync(resolvedDbPath);

const engine = new SqliteNodeEngine(resolvedDbPath);
const collection = new CardCollection(engine);

loadFileToCollection(sourcePath, collection, {{ recursive, includeProblematic: false }})
  .then(result => {{
    console.log(JSON.stringify({{
      success: true,
      metrics: {{
        total_files: result.metrics.filesCount,
        total_directories: result.metrics.directoriesCount,
        directory_levels: result.metrics.directoryLevels,
        total_size_bytes: result.results.reduce((a, r) => a + (r.size || 0), 0)
      }},
      files: result.results.slice(0, 10).map(r => ({{
        hash: r.hash.substring(0, 8),
        filename: r.filename,
        content_type: r.contentType
      }}))
    }}));
    engine.close();
    process.exit(0);
  }})
  .catch(err => {{
    console.error(JSON.stringify({{ success: false, error: err.message }}));
    process.exit(1);
  }});
'''
        
        # Write temp script
        import tempfile
        with tempfile.NamedTemporaryFile(mode='w', suffix='.mjs', dir=mcard_js_path, delete=False) as f:
            f.write(loader_script)
            script_path = f.name
        
        try:
            result = subprocess.run(
                ['npx', 'tsx', script_path],
                capture_output=True, text=True, timeout=120,
                cwd=mcard_js_path
            )
            if result.returncode != 0:
                stderr = result.stderr or ""
                return {"success": False, "error": f"JS loader failed: {stderr}"}
            try:
                # Parse last line that contains JSON (skip debug output)
                stdout = result.stdout.strip()
                lines = stdout.split('\n')
                for line in reversed(lines):
                    line = line.strip()
                    if line.startswith('{') and line.endswith('}'):
                        return json.loads(line)
                return {"success": False, "error": f"No JSON found in output. Stdout: {stdout[:200]}"}
            except json.JSONDecodeError as e:
                return {"success": False, "error": f"JSON parse error: {e}. Stdout: {result.stdout[:500]}"}
        except subprocess.TimeoutExpired:
            return {"success": False, "error": "JS loader timed out"}
        except Exception as e:
            return {"success": False, "error": str(e)}
        finally:
            try:
                os.unlink(script_path)
            except:
                pass


# ─────────────────────────────────────────────────────────────────────────────
# Binary-based Runtimes (Rust, C)
# ─────────────────────────────────────────────────────────────────────────────

class BinaryRuntime(SubprocessRuntime):
    """Base class for runtimes that execute compiled binaries."""
    
    def execute(self, impl: Dict[str, Any], target: MCard, ctx: Dict[str, Any]) -> Any:
        binary_path = impl.get('binary_path')
        if not binary_path:
            return f"Error: No {self.runtime_name} binary path provided"
        return self._run_subprocess([binary_path, json.dumps(ctx)], target, ctx)


class RustRuntime(BinaryRuntime):
    """Rust runtime executor - executes via compiled binary or WASM."""
    
    runtime_name = "Rust"
    command = RUNTIME_CONFIG['rust']['command']
    
    def execute(self, impl: Dict[str, Any], target: MCard, ctx: Dict[str, Any]) -> Any:
        wasm_module = impl.get('wasm_module')
        if wasm_module:
            return self._execute_wasm(wasm_module, target, ctx)
        return super().execute(impl, target, ctx)
    
    def _execute_wasm(self, wasm_path: str, target: MCard, ctx: Dict) -> Any:
        """Execute WASM module using wasmtime."""
        try:
            from wasmtime import Store, Module, Linker, WasiConfig
            import tempfile, os
            
            store = Store()
            wasi = WasiConfig()
            wasi.inherit_stdout()
            wasi.inherit_stderr()
            wasi.argv = ["program_name", json.dumps(ctx)]
            
            # Create temp files for stdin/stdout
            with tempfile.NamedTemporaryFile(mode='w+', delete=False) as tmp_in:
                tmp_in.write(target.get_content().decode('utf-8'))
                in_path = tmp_in.name
            
            with tempfile.NamedTemporaryFile(mode='w+', delete=False) as tmp_out:
                out_path = tmp_out.name
            
            wasi.stdin_file = in_path
            wasi.stdout_file = out_path
            store.set_wasi(wasi)
            
            linker = Linker(store.engine)
            linker.define_wasi()
            
            module = Module.from_file(store.engine, wasm_path)
            instance = linker.instantiate(store, module)
            instance.exports(store)["_start"](store)
            
            with open(out_path, 'r') as f:
                output = f.read()
            
            os.unlink(in_path)
            os.unlink(out_path)
            return output
            
        except ImportError:
            return "Error: wasmtime python library not installed"
        except Exception as e:
            return f"Error executing WASM: {e}"
    
    def validate_environment(self) -> bool:
        rust_ok = self._check_command('rustc', '--version')
        wasm_ok = False
        try:
            import wasmtime
            wasm_ok = True
        except ImportError:
            pass
        return rust_ok or wasm_ok


class CRuntime(BinaryRuntime):
    """C runtime executor - executes via compiled binary."""
    runtime_name = "C"
    command = None  # No compiler check needed


# ─────────────────────────────────────────────────────────────────────────────
# Script-based Runtimes (Lean, R, Julia)
# ─────────────────────────────────────────────────────────────────────────────

class ScriptRuntime(SubprocessRuntime):
    """Base class for runtimes that execute script files."""
    
    file_key = 'code_file'
    run_args: List[str] = []
    
    def execute(self, impl: Dict[str, Any], target: MCard, ctx: Dict[str, Any]) -> Any:
        code_file = impl.get(self.file_key)
        if not code_file:
            return f"Error: No {self.runtime_name} code file provided"
        
        cmd = [self.command] + self.run_args + [code_file, json.dumps(ctx)]
        return self._run_subprocess(cmd, target, ctx, self.timeout)


class LeanRuntime(ScriptRuntime):
    """Lean 4 runtime executor."""
    runtime_name = "Lean"
    command = RUNTIME_CONFIG['lean']['command']
    run_args = ['--run']
    timeout = LEAN_TIMEOUT
    
    def validate_environment(self) -> bool:
        # Lean 4 takes longer to initialize, use extended timeout
        return self._check_command(self.command, self.version_flag, timeout=30)
    
    def execute(self, impl: Dict[str, Any], target: MCard, ctx: Dict[str, Any]) -> Any:
        """Execute Lean script, passing input as the argument."""
        code_file = impl.get(self.file_key)
        if not code_file:
            return f"Error: No {self.runtime_name} code file provided"
        
        # Determine what to pass as the argument:
        # 1. If context has 'op', 'a', 'b' keys (polyglot mode), use context as JSON
        # 2. Otherwise, use target content (standalone CLM mode)
        if ctx and ('op' in ctx or 'a' in ctx or 'n' in ctx):
            # Polyglot/advanced mode - context contains the actual input data
            input_data = json.dumps(ctx)
        else:
            # Standalone CLM mode - target content is the JSON input
            input_data = target.get_content().decode('utf-8', errors='ignore')
        
        cmd = [self.command] + self.run_args + [code_file, input_data]
        return self._run_subprocess(cmd, target, ctx, self.timeout)


class RRuntime(ScriptRuntime):
    """R runtime executor."""
    runtime_name = "R"
    command = RUNTIME_CONFIG['r']['command']
    timeout = R_TIMEOUT


class JuliaRuntime(ScriptRuntime):
    """Julia runtime executor."""
    runtime_name = "Julia"
    command = RUNTIME_CONFIG['julia']['command']
    timeout = JULIA_TIMEOUT


# ─────────────────────────────────────────────────────────────────────────────
# Lambda Calculus Runtime
# ─────────────────────────────────────────────────────────────────────────────

class LambdaRuntimeExecutor(RuntimeExecutor):
    """Lambda Calculus runtime executor.
    
    Executes λ-calculus operations (α-β-η conversions) on MCard-stored terms.
    """
    
    def __init__(self, collection=None):
        super().__init__()
        self.collection = collection
        self._lambda_runtime = None
    
    def _get_runtime(self):
        """Lazy initialization of LambdaRuntime."""
        if self._lambda_runtime is None:
            from mcard.ptr.lambda_calc import LambdaRuntime
            from mcard.model.card_collection import CardCollection
            
            if self.collection is None:
                self.collection = CardCollection(db_path=":memory:")
            self._lambda_runtime = LambdaRuntime(self.collection)
        return self._lambda_runtime
    
    def execute(self, impl: Dict[str, Any], target: MCard, ctx: Dict[str, Any]) -> Any:
        """Execute Lambda Calculus operation."""
        runtime = self._get_runtime()
        
        # Build context from concrete implementation and context
        operation = impl.get('operation', 'normalize')
        strategy = impl.get('strategy', 'normal')
        max_steps = impl.get('maxSteps', impl.get('max_steps', 100))
        
        # Get expression from context or target
        expression = ctx.get('expression')
        if not expression:
            # Try to get from balanced input arguments
            balanced = ctx.get('balanced', {})
            input_args = balanced.get('input_arguments', {})
            expression = input_args.get('expression') or input_args.get('numeral')
        
        if not expression or expression == 'dummy_target':
            # Check if target content looks like a Lambda expression
            target_content = target.get_content().decode('utf-8', errors='ignore')
            if target_content and 'λ' in target_content or '\\' in target_content:
                expression = target_content
            else:
                return "Error: No Lambda expression provided"
        
        # Execute through LambdaRuntime
        result = runtime.execute(
            expression,
            {'expression': expression},
            {
                'operation': operation,
                'strategy': strategy,
                'max_steps': max_steps
            },
            ''
        )
        
        if result.success:
            # Return appropriate result based on operation
            if result.pretty_print:
                return result.pretty_print
            elif result.is_closed is not None:
                return result.is_closed
            elif result.free_variables is not None:
                return result.free_variables
            else:
                return result.term_hash
        else:
            return f"Error: {result.error}"
    
    def validate_environment(self) -> bool:
        """Lambda runtime is always available (pure Python)."""
        return True
    
    def get_runtime_status(self) -> Dict[str, Any]:
        return {
            'available': True,
            'version': '1.0.0',
            'command': 'lambda',
            'details': 'Lambda Calculus α-β-η converter'
        }


# ─────────────────────────────────────────────────────────────────────────────
# Runtime Factory
# ─────────────────────────────────────────────────────────────────────────────


class RuntimeFactory:
    """Factory for creating runtime executors based on configuration."""
    
    _executors: Dict[RuntimeType, RuntimeExecutor] = {}
    _availability_cache: Dict[RuntimeType, bool] = {}  # Cache for availability checks
    _detailed_status_cache: Optional[Dict[str, Dict[str, Any]]] = None
    
    # Mapping from RuntimeType to executor class
    _EXECUTOR_MAP: Dict[RuntimeType, type] = {
        RuntimeType.PYTHON: PythonRuntime,
        RuntimeType.JAVASCRIPT: JavaScriptRuntime,
        RuntimeType.RUST: RustRuntime,
        RuntimeType.C: CRuntime,
        RuntimeType.WASM: RustRuntime,
        RuntimeType.LEAN: LeanRuntime,
        RuntimeType.R: RRuntime,
        RuntimeType.JULIA: JuliaRuntime,
        RuntimeType.LAMBDA: LambdaRuntimeExecutor,
    }
    
    @classmethod
    def clear_cache(cls) -> None:
        """Clear cached availability data. Useful for testing or env changes."""
        cls._availability_cache.clear()
        cls._detailed_status_cache = None
        cls._executors.clear()
    
    @classmethod
    def get_executor(cls, runtime_type: str) -> Optional[RuntimeExecutor]:
        """Get or create a runtime executor for the specified type."""
        # Handle LLM runtime specially (dynamic import to avoid circular dependency)
        if runtime_type.lower() == 'llm':
            return cls._get_llm_executor()
        
        # Handle Lambda runtime specially (needs CardCollection)
        if runtime_type.lower() == 'lambda':
            return cls._get_lambda_executor()
        
        # Handle Network runtime specially (dynamic import)
        if runtime_type.lower() == 'network':
            return cls._get_network_executor()
            
        # User defined aliases
        if runtime_type.lower() == 'node':
            runtime_type = 'javascript'
        
        try:
            rt_type = RuntimeType(runtime_type.lower())
        except ValueError:
            logging.error(f"Unsupported runtime type: {runtime_type}")
            return None
        
        if rt_type not in cls._executors:
            executor = cls._create_executor(rt_type)
            if executor and cls._is_available(rt_type, executor):
                cls._executors[rt_type] = executor
            else:
                logging.warning(f"Runtime {runtime_type} not available or invalid")
                return None
        
        return cls._executors.get(rt_type)
    
    @classmethod
    def _is_available(cls, rt_type: RuntimeType, executor: RuntimeExecutor) -> bool:
        """Check if runtime is available, using cache."""
        if rt_type not in cls._availability_cache:
            cls._availability_cache[rt_type] = executor.validate_environment()
        return cls._availability_cache[rt_type]
    
    @classmethod
    def _get_llm_executor(cls) -> Optional[RuntimeExecutor]:
        """Get LLM executor with dynamic import."""
        if RuntimeType.LLM in cls._executors:
            return cls._executors[RuntimeType.LLM]
        
        try:
            from .llm import LLMRuntime
            executor = LLMRuntime()
            if executor.validate_environment():
                cls._executors[RuntimeType.LLM] = executor
                return executor
            else:
                logging.warning("LLM runtime not available (Ollama not running?)")
                return None
        except ImportError as e:
            logging.error(f"Failed to import LLM runtime: {e}")
            return None
    
    @classmethod
    def _get_lambda_executor(cls) -> Optional[RuntimeExecutor]:
        """Get Lambda Calculus executor with dynamic import."""
        if RuntimeType.LAMBDA in cls._executors:
            return cls._executors[RuntimeType.LAMBDA]
        
        try:
            from mcard.ptr.lambda_calc import LambdaRuntime
            from mcard.model.card_collection import CardCollection
            
            # Create an in-memory collection for Lambda terms
            collection = CardCollection(db_path=":memory:")
            executor = LambdaRuntimeExecutor(collection)
            cls._executors[RuntimeType.LAMBDA] = executor
            return executor
        except ImportError as e:
            logging.error(f"Failed to import Lambda runtime: {e}")
            return None
    
    @classmethod
    def _get_network_executor(cls) -> Optional[RuntimeExecutor]:
        """Get Network IO executor with dynamic import."""
        if RuntimeType.NETWORK in cls._executors:
            return cls._executors[RuntimeType.NETWORK]
        
        try:
            from mcard.ptr.network import NetworkRuntime
            from mcard.model.card_collection import CardCollection
            
            # Create an in-memory collection for network operations
            collection = CardCollection(db_path=":memory:")
            executor = NetworkRuntime(collection)
            cls._executors[RuntimeType.NETWORK] = executor
            return executor
        except ImportError as e:
            logging.error(f"Failed to import Network runtime: {e}")
            return None
    
    @classmethod
    def _create_executor(cls, runtime_type: RuntimeType) -> Optional[RuntimeExecutor]:
        """Create a new runtime executor."""
        # Handle LLM specially (dynamic import)
        if runtime_type == RuntimeType.LLM:
            try:
                from .llm import LLMRuntime
                return LLMRuntime()
            except ImportError:
                return None
        
        executor_class = cls._EXECUTOR_MAP.get(runtime_type)
        return executor_class() if executor_class else None
    
    @classmethod
    def list_available_runtimes(cls) -> Dict[str, bool]:
        """List all runtime types and their availability (cached)."""
        result = {}
        for rt in RuntimeType:
            if rt not in cls._availability_cache:
                executor = cls._create_executor(rt)
                cls._availability_cache[rt] = executor.validate_environment() if executor else False
            result[rt.value] = cls._availability_cache[rt]
        return result
    
    @classmethod
    def get_detailed_status(cls) -> Dict[str, Dict[str, Any]]:
        """Get detailed status information for all runtimes (cached)."""
        if cls._detailed_status_cache is not None:
            return cls._detailed_status_cache
        
        status = {}
        for rt in RuntimeType:
            executor = cls._create_executor(rt)
            status[rt.value] = (executor.get_runtime_status() if executor else {
                'available': False, 'version': None, 'command': rt.value,
                'details': 'Runtime executor not implemented'
            })
        cls._detailed_status_cache = status
        return status
    
    @classmethod
    def at_least_one_available(cls) -> bool:
        """Check if at least one runtime is available."""
        return any(cls.list_available_runtimes().values())
    
    @classmethod
    def print_status(cls, verbose: bool = False) -> None:
        """Print runtime status to console."""
        print("\n=== Polyglot Runtime Status ===")
        for name, info in cls.get_detailed_status().items():
            if info['available']:
                ver = f" {info['version']}" if info['version'] else ""
                detail = f": {info['details']}" if verbose and info['details'] else ""
                print(f"✓ {name.capitalize()}{ver}{detail}")
            else:
                print(f"✗ {name.capitalize()} not found")
        
        available = sum(1 for i in cls.get_detailed_status().values() if i['available'])
        print(f"{'=' * 31}\nAvailable: {available}/{len(RuntimeType)} runtimes")

