"""JavaScript AST analysis using esprima via Node.js subprocess.

Catches patterns that regex misses:
- Import aliasing (const s = require('child_process').exec)
- Dynamic requires (require(userInput))
- Dynamic property access (global[funcName])
- eval variants (new Function, setTimeout with string)
- Prototype pollution patterns
"""
import json
import subprocess
import tempfile
from pathlib import Path
from typing import List, Optional

from .patterns import Finding, Severity


# JavaScript code for AST analysis
JS_ANALYZER_CODE = '''
const esprima = require('esprima');

function analyzeAST(code) {
    const findings = [];
    
    let ast;
    try {
        ast = esprima.parseScript(code, { 
            tolerant: true, 
            loc: true,
            range: true 
        });
    } catch (e) {
        // Try as module
        try {
            ast = esprima.parseModule(code, { 
                tolerant: true, 
                loc: true,
                range: true 
            });
        } catch (e2) {
            return { error: 'Parse failed: ' + e.message, findings: [] };
        }
    }
    
    // Track imports/requires for alias detection
    const dangerousModules = new Set(['child_process', 'fs', 'net', 'dgram', 'cluster', 'vm']);
    const dangerousFunctions = new Set(['exec', 'execSync', 'spawn', 'spawnSync', 'execFile']);
    const aliasedDangerous = new Map(); // alias -> {module, function}
    
    function getLocation(node) {
        return node.loc ? node.loc.start.line : 0;
    }
    
    function addFinding(severity, patternId, description, node, match) {
        findings.push({
            severity,
            pattern_id: patternId,
            description,
            line: getLocation(node),
            match: match || ''
        });
    }
    
    function walk(node, parent) {
        if (!node || typeof node !== 'object') return;
        
        // === REQUIRE ANALYSIS ===
        
        // const x = require('child_process')
        if (node.type === 'VariableDeclarator' && 
            node.init && node.init.type === 'CallExpression' &&
            node.init.callee && node.init.callee.name === 'require') {
            
            const arg = node.init.arguments[0];
            if (arg && arg.type === 'Literal' && dangerousModules.has(arg.value)) {
                const varName = node.id.name || node.id;
                aliasedDangerous.set(varName, { module: arg.value, function: null });
                addFinding('warn', 'js_ast_dangerous_require', 
                    `Requiring dangerous module: ${arg.value}`, node, `require('${arg.value}')`);
            }
            // Dynamic require
            if (arg && arg.type !== 'Literal') {
                addFinding('danger', 'js_ast_dynamic_require',
                    'Dynamic require() - module determined at runtime', node, 'require(<dynamic>)');
            }
        }
        
        // const { exec } = require('child_process')
        if (node.type === 'VariableDeclarator' &&
            node.id && node.id.type === 'ObjectPattern' &&
            node.init && node.init.type === 'CallExpression' &&
            node.init.callee && node.init.callee.name === 'require') {
            
            const arg = node.init.arguments[0];
            if (arg && arg.type === 'Literal' && dangerousModules.has(arg.value)) {
                for (const prop of node.id.properties) {
                    const imported = prop.key.name;
                    const local = prop.value.name;
                    if (dangerousFunctions.has(imported)) {
                        aliasedDangerous.set(local, { module: arg.value, function: imported });
                        addFinding('danger', 'js_ast_dangerous_function_import',
                            `Importing dangerous function: ${imported} from ${arg.value}`,
                            node, `{ ${imported} } from '${arg.value}'`);
                    }
                }
            }
        }
        
        // const s = require('child_process').exec
        if (node.type === 'VariableDeclarator' &&
            node.init && node.init.type === 'MemberExpression' &&
            node.init.object && node.init.object.type === 'CallExpression' &&
            node.init.object.callee && node.init.object.callee.name === 'require') {
            
            const arg = node.init.object.arguments[0];
            const prop = node.init.property;
            if (arg && arg.type === 'Literal' && dangerousModules.has(arg.value) &&
                prop && dangerousFunctions.has(prop.name)) {
                const varName = node.id.name;
                aliasedDangerous.set(varName, { module: arg.value, function: prop.name });
                addFinding('danger', 'js_ast_aliased_dangerous_function',
                    `Aliased dangerous function: ${varName} = require('${arg.value}').${prop.name}`,
                    node, `${varName} = require('${arg.value}').${prop.name}`);
            }
        }
        
        // === IMPORT ANALYSIS (ES6) ===
        
        // import { exec as e } from 'child_process'
        if (node.type === 'ImportDeclaration') {
            const source = node.source.value;
            if (dangerousModules.has(source)) {
                for (const spec of node.specifiers) {
                    if (spec.type === 'ImportSpecifier') {
                        const imported = spec.imported.name;
                        const local = spec.local.name;
                        if (dangerousFunctions.has(imported)) {
                            aliasedDangerous.set(local, { module: source, function: imported });
                            addFinding('danger', 'js_ast_import_dangerous',
                                `Importing dangerous function: ${imported} as ${local}`,
                                node, `import { ${imported} as ${local} } from '${source}'`);
                        }
                    } else if (spec.type === 'ImportDefaultSpecifier' || spec.type === 'ImportNamespaceSpecifier') {
                        aliasedDangerous.set(spec.local.name, { module: source, function: null });
                        addFinding('warn', 'js_ast_import_dangerous_module',
                            `Importing dangerous module: ${source}`, node, `import from '${source}'`);
                    }
                }
            }
        }
        
        // === DYNAMIC CODE EXECUTION ===
        
        // new Function(...)
        if (node.type === 'NewExpression' && 
            node.callee && node.callee.name === 'Function') {
            addFinding('danger', 'js_ast_function_constructor',
                'new Function() - dynamic code execution (eval equivalent)', node, 'new Function(...)');
        }
        
        // Function(...) without new
        if (node.type === 'CallExpression' &&
            node.callee && node.callee.name === 'Function' &&
            parent && parent.type !== 'NewExpression') {
            addFinding('danger', 'js_ast_function_call',
                'Function() call - dynamic code execution', node, 'Function(...)');
        }
        
        // global[x] / window[x] / globalThis[x]
        if (node.type === 'MemberExpression' &&
            node.computed &&
            node.object && ['global', 'window', 'globalThis', 'self'].includes(node.object.name)) {
            addFinding('warn', 'js_ast_dynamic_global_access',
                `Dynamic access to ${node.object.name}[] - may execute arbitrary code`,
                node, `${node.object.name}[<dynamic>]`);
        }
        
        // setTimeout/setInterval with string (not function)
        if (node.type === 'CallExpression' &&
            node.callee && ['setTimeout', 'setInterval'].includes(node.callee.name)) {
            const firstArg = node.arguments[0];
            if (firstArg && firstArg.type === 'Literal' && typeof firstArg.value === 'string') {
                addFinding('danger', 'js_ast_settimeout_string',
                    `${node.callee.name} with string argument - implicit eval`,
                    node, `${node.callee.name}("${firstArg.value.substring(0, 30)}...")`);
            }
        }
        
        // === PROTOTYPE POLLUTION ===
        
        // __proto__ access
        if (node.type === 'MemberExpression' &&
            node.property && node.property.name === '__proto__') {
            addFinding('danger', 'js_ast_proto_access',
                'Direct __proto__ access - prototype pollution risk', node, '.__proto__');
        }
        
        // obj['__proto__'] computed access
        if (node.type === 'MemberExpression' &&
            node.computed &&
            node.property && node.property.type === 'Literal' &&
            node.property.value === '__proto__') {
            addFinding('danger', 'js_ast_proto_computed',
                'Computed __proto__ access - prototype pollution', node, "['__proto__']");
        }
        
        // constructor.prototype
        if (node.type === 'MemberExpression' &&
            node.object && node.object.type === 'MemberExpression' &&
            node.object.property && node.object.property.name === 'constructor' &&
            node.property && node.property.name === 'prototype') {
            addFinding('danger', 'js_ast_constructor_prototype',
                'constructor.prototype access - prototype pollution', node, '.constructor.prototype');
        }
        
        // Object.setPrototypeOf
        if (node.type === 'CallExpression' &&
            node.callee && node.callee.type === 'MemberExpression' &&
            node.callee.object && node.callee.object.name === 'Object' &&
            node.callee.property && node.callee.property.name === 'setPrototypeOf') {
            addFinding('warn', 'js_ast_setprototypeof',
                'Object.setPrototypeOf - may modify prototype chain', node, 'Object.setPrototypeOf()');
        }
        
        // === ALIASED FUNCTION CALLS ===
        
        // Detect calls to aliased dangerous functions
        if (node.type === 'CallExpression' && node.callee) {
            const calleeName = node.callee.name;
            if (calleeName && aliasedDangerous.has(calleeName)) {
                const info = aliasedDangerous.get(calleeName);
                if (info.function) {
                    addFinding('danger', 'js_ast_aliased_call',
                        `Call to aliased dangerous function: ${calleeName} (${info.module}.${info.function})`,
                        node, `${calleeName}(...)`);
                }
            }
        }
        
        // Recurse
        for (const key of Object.keys(node)) {
            const child = node[key];
            if (Array.isArray(child)) {
                child.forEach(c => walk(c, node));
            } else if (child && typeof child === 'object' && child.type) {
                walk(child, node);
            }
        }
    }
    
    walk(ast, null);
    
    return { findings, aliasCount: aliasedDangerous.size };
}

// Read code from stdin
let code = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => code += chunk);
process.stdin.on('end', () => {
    const result = analyzeAST(code);
    console.log(JSON.stringify(result));
});
'''


def ensure_esprima_installed() -> bool:
    """Check if esprima is available, install if needed."""
    try:
        result = subprocess.run(
            ['node', '-e', "require('esprima')"],
            capture_output=True,
            timeout=10
        )
        if result.returncode == 0:
            return True
    except Exception:
        pass
    
    # Try to install esprima
    try:
        subprocess.run(
            ['npm', 'install', '-g', 'esprima'],
            capture_output=True,
            timeout=60
        )
        return True
    except Exception:
        return False


_esprima_available: Optional[bool] = None


def is_esprima_available() -> bool:
    """Check if esprima is available (cached)."""
    global _esprima_available
    if _esprima_available is None:
        _esprima_available = ensure_esprima_installed()
    return _esprima_available


def analyze_js_ast(content: str, filename: str) -> List[Finding]:
    """
    Analyze JavaScript code using AST parsing via Node.js/esprima.
    
    Catches patterns that regex misses:
    - Import aliasing
    - Dynamic requires
    - eval variants
    - Prototype pollution
    """
    findings = []
    
    # Skip if esprima not available
    if not is_esprima_available():
        return findings
    
    # Skip non-JS files
    if not filename.endswith(('.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx')):
        return findings
    
    # Skip very small files
    if len(content) < 50:
        return findings
    
    # Skip TypeScript for now (esprima doesn't support it well)
    if filename.endswith(('.ts', '.tsx')):
        # TypeScript - try parsing but expect failures
        pass
    
    try:
        # Write analyzer script to temp file
        with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f:
            f.write(JS_ANALYZER_CODE)
            analyzer_path = f.name
        
        try:
            # Run analyzer with code on stdin
            result = subprocess.run(
                ['node', analyzer_path],
                input=content,
                capture_output=True,
                text=True,
                timeout=30,
                env={'NODE_PATH': '/usr/lib/node_modules:/usr/local/lib/node_modules'}
            )
            
            if result.returncode != 0:
                # Parse error - not necessarily a problem
                return findings
            
            try:
                data = json.loads(result.stdout)
            except json.JSONDecodeError:
                return findings
            
            if 'error' in data and data['error']:
                # Parse failed, not a security issue
                return findings
            
            # Convert findings
            for f_data in data.get('findings', []):
                severity_map = {
                    'danger': Severity.DANGER,
                    'warn': Severity.WARN,
                    'info': Severity.INFO,
                }
                severity = severity_map.get(f_data.get('severity', 'warn'), Severity.WARN)
                
                findings.append(Finding(
                    severity=severity,
                    pattern_id=f_data.get('pattern_id', 'js_ast_unknown'),
                    description=f_data.get('description', 'JS AST finding'),
                    file=filename,
                    line=f_data.get('line', 0),
                    match=f_data.get('match', '')[:100],
                    context='',
                ))
        
        finally:
            Path(analyzer_path).unlink(missing_ok=True)
    
    except subprocess.TimeoutExpired:
        # Analysis took too long, skip
        pass
    except Exception as e:
        # Don't fail the whole scan for AST issues
        pass
    
    return findings
