import ast
import json
import os
import re
from typing import List

from bastion.analyzers.python_analyzer import PythonAnalyzer
from bastion.analyzers.js_analyzer import JavaScriptAnalyzer
from bastion.analyzers.skillmd_analyzer import SkillMDAnalyzer
from bastion.analyzers.base import BaseAnalyzer
from bastion.analyzers.dependency_analyzer import DependencyAnalyzer
from bastion.models import Capability, Evidence, ScanResult

ANALYZERS = {
    '.py': PythonAnalyzer,
    '.js': JavaScriptAnalyzer,
    '.ts': JavaScriptAnalyzer,
    '.jsx': JavaScriptAnalyzer,
    '.tsx': JavaScriptAnalyzer,
    '.md': SkillMDAnalyzer,
}

def get_analyzer(file_extension):
    return ANALYZERS.get(file_extension)


def analyze_plugin(plugin, timestamp: str) -> ScanResult:
    all_capabilities: List[Capability] = []
    dependencies: set = set()
    skills: list = []
    description: str = ""
    file_count: int = 0
    total_lines: int = 0

    if plugin.type == "folder":
        for root, dirs, files in os.walk(plugin.path):
            for fname in sorted(files):
                ext = os.path.splitext(fname)[1].lower()
                is_skill_md = (ext == '.md' and fname.upper() == 'SKILL.MD')
                if ext in ANALYZERS and (ext != '.md' or is_skill_md):
                    file_count += 1
                    fpath = os.path.join(root, fname)
                    analyzer_cls = ANALYZERS[ext]
                    analyzer = analyzer_cls()
                    all_capabilities.extend(analyzer.analyze(fpath))
                    all_capabilities.extend(_scan_for_hardcoded_secrets(fpath))
                    if ext != '.md':
                        _extract_metadata(fpath, ext, dependencies, skills, file_count == 1)
                    try:
                        with open(fpath, 'r', encoding='utf-8', errors='replace') as f:
                            total_lines += sum(1 for _ in f)
                    except Exception:
                        pass
                    if not description and file_count == 1:
                        description = _extract_description(fpath, ext)
        all_capabilities.extend(_detect_install_scripts(plugin.path, plugin.type))
        dep_analyzer = DependencyAnalyzer()
        all_capabilities.extend(dep_analyzer.analyze_plugin_dependencies(plugin.path, plugin.type))
    else:
        ext = os.path.splitext(plugin.path)[1].lower()
        file_count = 1
        if ext in ANALYZERS:
            analyzer_cls = ANALYZERS[ext]
            analyzer = analyzer_cls()
            all_capabilities.extend(analyzer.analyze(plugin.path))
            all_capabilities.extend(_scan_for_hardcoded_secrets(plugin.path))
            _extract_metadata(plugin.path, ext, dependencies, skills, True)
            try:
                with open(plugin.path, 'r', encoding='utf-8', errors='replace') as f:
                    total_lines += sum(1 for _ in f)
            except Exception:
                pass
            description = _extract_description(plugin.path, ext)
        all_capabilities.extend(_detect_install_scripts(plugin.path, plugin.type))
        dep_analyzer = DependencyAnalyzer()
        all_capabilities.extend(dep_analyzer.analyze_plugin_dependencies(plugin.path, plugin.type))

    privilege_cap = _compute_privilege_level(all_capabilities)
    if privilege_cap:
        all_capabilities.append(privilege_cap)

    severity_order = {"Low": 0, "Medium": 1, "High": 2, "Critical": 3}
    risk_level = "Low"
    for cap in all_capabilities:
        if severity_order.get(cap.severity, 0) > severity_order.get(risk_level, 0):
            risk_level = cap.severity

    return ScanResult(
        plugin_id=plugin.id,
        capabilities=all_capabilities,
        risk_level=risk_level,
        timestamp=timestamp,
        dependencies=sorted(dependencies),
        skills=skills,
        description=description,
        file_count=file_count,
        total_lines=total_lines,
    )


STDLIB_MODULES = {
    'os', 'sys', 're', 'json', 'math', 'time', 'datetime', 'collections',
    'itertools', 'functools', 'pathlib', 'typing', 'abc', 'io', 'hashlib',
    'uuid', 'logging', 'copy', 'string', 'textwrap', 'enum', 'dataclasses',
    'contextlib', 'ast', 'inspect', 'unittest', 'argparse', 'configparser',
    'csv', 'sqlite3', 'threading', 'multiprocessing', 'queue', 'struct',
    'base64', 'hmac', 'secrets', 'tempfile', 'shutil', 'glob', 'fnmatch',
    'stat', 'fileinput', 'pickle', 'shelve', 'marshal', 'dbm', 'gzip',
    'bz2', 'lzma', 'zipfile', 'tarfile', 'zlib', 'pprint', 'dis',
    'traceback', 'warnings', 'weakref', 'types', 'operator', 'codecs',
    'unicodedata', 'locale', 'gettext', 'bisect', 'heapq', 'array',
    'decimal', 'fractions', 'random', 'statistics', 'cmath',
    'socket', 'ssl', 'select', 'signal', 'subprocess', 'ctypes',
    'http', 'urllib', 'email', 'html', 'xml', 'webbrowser',
    '__future__', 'builtins', 'importlib', 'pkgutil', 'platform',
}

NODE_BUILTINS = {
    'fs', 'path', 'os', 'http', 'https', 'url', 'util', 'crypto',
    'stream', 'events', 'child_process', 'cluster', 'net', 'dns',
    'tls', 'zlib', 'buffer', 'querystring', 'assert', 'readline',
}


def _extract_metadata(file_path: str, ext: str, dependencies: set, skills: list, is_first: bool):
    try:
        with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
            source = f.read()
    except Exception:
        return

    if ext == '.py':
        try:
            tree = ast.parse(source, filename=file_path)
        except SyntaxError:
            return

        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    top = alias.name.split('.')[0]
                    if top not in STDLIB_MODULES and not top.startswith('_'):
                        dependencies.add(top)
            elif isinstance(node, ast.ImportFrom):
                if node.module:
                    top = node.module.split('.')[0]
                    if top not in STDLIB_MODULES and not top.startswith('_') and node.level == 0:
                        dependencies.add(top)
            elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                if not node.name.startswith('_') and node.col_offset == 0:
                    skills.append(node.name)
            elif isinstance(node, ast.ClassDef):
                if not node.name.startswith('_') and node.col_offset == 0:
                    skills.append(node.name)
    else:
        for line in source.splitlines():
            m = re.search(r'''require\s*\(\s*['"]([^'"./][^'"]*)['"]\s*\)''', line)
            if m:
                pkg = m.group(1).split('/')[0]
                if pkg not in NODE_BUILTINS:
                    dependencies.add(pkg)
            m = re.search(r'''from\s+['"]([^'"./][^'"]*)['"]\s*''', line)
            if m:
                pkg = m.group(1).split('/')[0]
                if pkg not in NODE_BUILTINS:
                    dependencies.add(pkg)
            m = re.match(r'export\s+(?:default\s+)?(?:async\s+)?(?:function|class)\s+(\w+)', line)
            if m:
                skills.append(m.group(1))
            m = re.match(r'module\.exports\s*=\s*(\w+)', line)
            if m:
                skills.append(m.group(1))


def _extract_description(file_path: str, ext: str) -> str:
    try:
        with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
            source = f.read()
    except Exception:
        return ""

    if ext == '.py':
        try:
            tree = ast.parse(source, filename=file_path)
            docstring = ast.get_docstring(tree)
            if docstring:
                first_line = docstring.strip().split('\n')[0]
                return first_line[:200]
        except SyntaxError:
            pass
    else:
        lines = source.splitlines()
        for line in lines[:10]:
            stripped = line.strip()
            if stripped.startswith('//') and len(stripped) > 4:
                return stripped[2:].strip()[:200]
            if stripped.startswith('/*') or stripped.startswith('/**'):
                comment = stripped.lstrip('/* ').rstrip('*/').strip()
                if comment:
                    return comment[:200]
            if stripped and not stripped.startswith(('import', 'from', 'require', "'use", '"use', 'const', 'let', 'var', '//')):
                break

    return ""


DANGEROUS_COMBOS = {
    frozenset({"system_execution", "network_access"}): (
        "Critical", "Plugin can execute system commands AND access the network — could download and run malicious code"
    ),
    frozenset({"system_execution", "secret_access"}): (
        "Critical", "Plugin can execute system commands AND access secrets — could exfiltrate credentials"
    ),
    frozenset({"system_execution", "network_access", "secret_access"}): (
        "Critical", "Plugin has full system access: exec + network + secrets — effectively unrestricted"
    ),
    frozenset({"system_execution", "network_access", "file_system_access"}): (
        "Critical", "Plugin can execute commands, access network, and read/write files — effectively root-level access"
    ),
    frozenset({"network_access", "secret_access"}): (
        "High", "Plugin can access secrets AND the network — could exfiltrate credentials to external servers"
    ),
    frozenset({"network_access", "file_system_access"}): (
        "High", "Plugin can access the network AND file system — could exfiltrate local files"
    ),
    frozenset({"dynamic_code", "network_access"}): (
        "Critical", "Plugin uses dynamic code execution AND network access — could fetch and execute remote payloads"
    ),
    frozenset({"sandbox_bypass", "system_execution"}): (
        "Critical", "Plugin bypasses sandbox AND executes system commands — no safety controls"
    ),
    frozenset({"sandbox_bypass", "network_access"}): (
        "Critical", "Plugin bypasses sandbox AND accesses the network — unrestricted external communication"
    ),
}


def _compute_privilege_level(capabilities: List[Capability]) -> Capability | None:
    categories = {cap.category for cap in capabilities}

    info_categories = {"install_risk", "hardcoded_secret", "obfuscation", "privilege_escalation"}
    threat_categories = categories - info_categories

    if len(threat_categories) < 2:
        return None

    best_match = None
    best_severity = "Medium"
    best_description = ""
    severity_order = {"Low": 0, "Medium": 1, "High": 2, "Critical": 3}

    for combo, (severity, description) in DANGEROUS_COMBOS.items():
        if combo.issubset(threat_categories):
            if severity_order.get(severity, 0) > severity_order.get(best_severity, 0) or (
                severity == best_severity and (best_match is None or len(combo) > len(best_match))
            ):
                best_match = combo
                best_severity = severity
                best_description = description

    if best_match is None:
        if len(threat_categories) >= 3:
            best_severity = "High"
            best_description = f"Plugin combines {len(threat_categories)} capability types: {', '.join(sorted(threat_categories))} — review for over-privileged access"
        elif len(threat_categories) == 2:
            best_severity = "Medium"
            best_description = f"Plugin combines capabilities: {', '.join(sorted(threat_categories))}"
        else:
            return None

    evidence_entries = []
    for cap in capabilities:
        if cap.category in threat_categories and cap.evidence:
            ev = cap.evidence[0]
            evidence_entries.append(Evidence(
                file=ev.file,
                line=ev.line,
                code_snippet=f"[{cap.category}] {ev.code_snippet[:150]}",
                pattern_name=f"privilege_{cap.category}",
            ))

    return Capability(
        category="privilege_escalation",
        description=best_description,
        evidence=evidence_entries[:5],
        severity=best_severity,
    )


INSTALL_SCRIPT_PATTERNS = re.compile(r'(?:install|setup|bootstrap)', re.IGNORECASE)

SCRIPT_CONTENT_PATTERNS = [
    (re.compile(r'curl\s+.*\|\s*(?:ba)?sh', re.IGNORECASE), "Critical", "curl_pipe_bash"),
    (re.compile(r'wget\s+.*\|\s*(?:ba)?sh', re.IGNORECASE), "Critical", "wget_pipe_sh"),
    (re.compile(r'curl\s+.*\|\s*bash', re.IGNORECASE), "Critical", "curl_pipe_bash_2"),
    (re.compile(r'wget\s+.*\|\s*bash', re.IGNORECASE), "Critical", "wget_pipe_bash"),
    (re.compile(r'\bsudo\b'), "High", "sudo_usage"),
    (re.compile(r'pip\s+install\s+(?:https?://|git\+)', re.IGNORECASE), "High", "pip_install_url"),
    (re.compile(r'npm\s+install\s+(?:https?://|git\+|git://)', re.IGNORECASE), "High", "npm_install_git"),
    (re.compile(r'(?:curl|wget)\s+.*https?://', re.IGNORECASE), "High", "external_download"),
    (re.compile(r'chmod\s+\+x\b'), "Medium", "chmod_executable"),
    (re.compile(r'chmod\s+777\b'), "Medium", "chmod_777"),
]


def _detect_install_scripts(plugin_path: str, plugin_type: str) -> List[Capability]:
    if plugin_type != "folder":
        return []

    capabilities: dict = {}
    detected_scripts: List[str] = []
    severity_order = {"Low": 0, "Medium": 1, "High": 2, "Critical": 3}

    for root, dirs, files in os.walk(plugin_path):
        rel_root = os.path.relpath(root, plugin_path)
        is_root = (rel_root == '.')

        for fname in files:
            fpath = os.path.join(root, fname)
            lower_fname = fname.lower()

            if lower_fname.endswith('.sh') and INSTALL_SCRIPT_PATTERNS.search(lower_fname):
                detected_scripts.append(fpath)

            if is_root and lower_fname in ('setup.py', 'helper.py'):
                detected_scripts.append(fpath)

            if lower_fname == 'makefile':
                try:
                    with open(fpath, 'r', encoding='utf-8', errors='replace') as f:
                        content = f.read()
                    if re.search(r'^install\s*:', content, re.MULTILINE):
                        detected_scripts.append(fpath)
                except Exception:
                    pass

            if lower_fname == 'package.json' and is_root:
                try:
                    with open(fpath, 'r', encoding='utf-8', errors='replace') as f:
                        pkg = json.loads(f.read())
                    scripts = pkg.get('scripts', {})
                    if isinstance(scripts, dict):
                        for key in ('postinstall', 'preinstall', 'install'):
                            if key in scripts:
                                evidence = Evidence(
                                    file=fpath,
                                    line=0,
                                    code_snippet=f'scripts.{key}: {scripts[key]}',
                                    pattern_name=f'package_json_{key}',
                                )
                                if 'install_risk' not in capabilities:
                                    capabilities['install_risk'] = Capability(
                                        category='install_risk',
                                        description='Potentially dangerous install/setup script detected',
                                        evidence=[evidence],
                                        severity='High',
                                    )
                                else:
                                    capabilities['install_risk'].evidence.append(evidence)
                except Exception:
                    pass

    for script_path in detected_scripts:
        try:
            with open(script_path, 'r', encoding='utf-8', errors='replace') as f:
                content = f.read()
        except Exception:
            content = ""

        matched_any = False
        lines = content.splitlines()

        for pattern, severity, name in SCRIPT_CONTENT_PATTERNS:
            for i, line in enumerate(lines, 1):
                if pattern.search(line):
                    evidence = Evidence(
                        file=script_path,
                        line=i,
                        code_snippet=line.rstrip()[:200],
                        pattern_name=name,
                    )
                    if 'install_risk' not in capabilities:
                        capabilities['install_risk'] = Capability(
                            category='install_risk',
                            description='Potentially dangerous install/setup script detected',
                            evidence=[evidence],
                            severity=severity,
                        )
                    else:
                        capabilities['install_risk'].evidence.append(evidence)
                        if severity_order.get(severity, 0) > severity_order.get(capabilities['install_risk'].severity, 0):
                            capabilities['install_risk'].severity = severity
                    matched_any = True
                    break

        if not matched_any:
            evidence = Evidence(
                file=script_path,
                line=0,
                code_snippet=f'Install/setup script detected: {os.path.basename(script_path)}',
                pattern_name='install_script_present',
            )
            if 'install_risk' not in capabilities:
                capabilities['install_risk'] = Capability(
                    category='install_risk',
                    description='Potentially dangerous install/setup script detected',
                    evidence=[evidence],
                    severity='Medium',
                )
            else:
                capabilities['install_risk'].evidence.append(evidence)

    return list(capabilities.values())


SECRET_SCAN_PATTERNS = [
    (re.compile(r'AKIA[0-9A-Z]{16}'), 'aws_access_key'),
    (re.compile(r'(?i)aws.{0,20}(?:secret|key).{0,20}[\'"][0-9a-zA-Z/+]{40}[\'"]'), 'aws_secret_key'),
    (re.compile(r'sk-[a-zA-Z0-9]{20,}'), 'openai_api_key'),
    (re.compile(r'(?:sk|pk)_(?:test|live)_[a-zA-Z0-9]{20,}'), 'stripe_api_key'),
    (re.compile(r'gh[ps]_[a-zA-Z0-9]{36,}'), 'github_token'),
    (re.compile(r'github_pat_[a-zA-Z0-9_]{20,}'), 'github_pat'),
    (re.compile(r'(?i)(?:api_key|apikey|api_secret|access_token)\s*=\s*[\'"][a-zA-Z0-9_\-]{20,}[\'"]'), 'generic_api_key'),
    (re.compile(r'(?i)(?:password|passwd|pwd)\s*=\s*[\'"][^\'"]{8,}[\'"]'), 'hardcoded_password'),
    (re.compile(r'-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----'), 'private_key'),
    (re.compile(r'xox[bporas]-[a-zA-Z0-9-]+'), 'slack_token'),
    (re.compile(r'(?i)(?:secret|token|credential)\s*[:=]\s*[\'"][a-zA-Z0-9_\-/+=]{20,}[\'"]'), 'generic_secret'),
]

PLACEHOLDER_VALUES = re.compile(
    r'^(?:x{3,}|your[-_]?key[-_]?here|TODO|REPLACE_ME|changeme|CHANGE_ME|insert[-_]?here|placeholder|example|dummy|test)$',
    re.IGNORECASE,
)

TEST_PATH_PATTERN = re.compile(r'(?:^|/)(?:test|spec|mock|fixture)(?:s)?(?:/|$)', re.IGNORECASE)


def _scan_for_hardcoded_secrets(file_path: str) -> List[Capability]:
    if TEST_PATH_PATTERN.search(file_path):
        return []

    try:
        with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
            lines = f.readlines()
    except Exception:
        return []

    capabilities: dict = {}

    for i, line in enumerate(lines, 1):
        stripped = line.strip()

        if stripped.startswith('#') or stripped.startswith('//') or stripped.startswith('/*') or stripped.startswith('*'):
            continue

        if 'os.environ' in stripped or 'process.env' in stripped or 'os.getenv' in stripped:
            continue

        for pattern, name in SECRET_SCAN_PATTERNS:
            match = pattern.search(stripped)
            if not match:
                continue

            matched_value = match.group(0)
            value_match = re.search(r'[\'"]([^\'"]+)[\'"]', matched_value)
            if value_match and PLACEHOLDER_VALUES.search(value_match.group(1)):
                continue

            if not value_match and name in ('generic_api_key', 'hardcoded_password', 'generic_secret'):
                inner = re.search(r'=\s*[\'"]([^\'"]+)[\'"]', matched_value)
                if inner and PLACEHOLDER_VALUES.search(inner.group(1)):
                    continue

            evidence = Evidence(
                file=file_path,
                line=i,
                code_snippet=stripped[:200],
                pattern_name=name,
            )
            if 'hardcoded_secret' not in capabilities:
                capabilities['hardcoded_secret'] = Capability(
                    category='hardcoded_secret',
                    description='Hardcoded secret or credential detected',
                    evidence=[evidence],
                    severity='Critical',
                )
            else:
                capabilities['hardcoded_secret'].evidence.append(evidence)

    return list(capabilities.values())
