"""
Reverse Engineering - Binary analysis and decompilation tools

Provides tools for:
- Binary file analysis (PE, ELF, Mach-O)
- Disassembly (via radare2, objdump, Ghidra)
- Decompilation assistance
- String extraction and analysis
- Symbol resolution
- Control flow analysis

For CTF challenges, malware analysis, and security research.
"""

import json
import re
import shutil
import subprocess
import tempfile
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Any, Optional

from groknroll.mode2600.core import get_mode2600


class BinaryFormat(Enum):
    """Binary file formats."""

    PE = "pe"  # Windows
    ELF = "elf"  # Linux
    MACHO = "macho"  # macOS
    RAW = "raw"
    JAVA_CLASS = "java"
    DOTNET = "dotnet"
    PYTHON_PYC = "pyc"
    UNKNOWN = "unknown"


class Architecture(Enum):
    """CPU architectures."""

    X86 = "x86"
    X86_64 = "x86_64"
    ARM = "arm"
    ARM64 = "arm64"
    MIPS = "mips"
    RISCV = "riscv"
    POWERPC = "ppc"
    UNKNOWN = "unknown"


@dataclass
class BinaryInfo:
    """Basic binary file information."""

    path: Path
    format: BinaryFormat
    architecture: Architecture
    bits: int  # 32 or 64
    endianness: str  # "little" or "big"
    entry_point: Optional[int] = None
    sections: list[dict] = field(default_factory=list)
    imports: list[str] = field(default_factory=list)
    exports: list[str] = field(default_factory=list)
    libraries: list[str] = field(default_factory=list)
    compiler: Optional[str] = None
    stripped: bool = False
    pie: bool = False
    nx: bool = False
    canary: bool = False


@dataclass
class DisassemblyResult:
    """Disassembly output."""

    address: int
    bytes: bytes
    mnemonic: str
    operands: str
    comment: Optional[str] = None

    def __str__(self) -> str:
        hex_bytes = self.bytes.hex()
        return f"0x{self.address:08x}  {hex_bytes:<16}  {self.mnemonic:<8} {self.operands}"


@dataclass
class FunctionInfo:
    """Information about a function in the binary."""

    name: str
    address: int
    size: int
    calling_convention: Optional[str] = None
    parameters: list[str] = field(default_factory=list)
    local_vars: list[str] = field(default_factory=list)
    calls: list[str] = field(default_factory=list)
    called_by: list[str] = field(default_factory=list)
    disassembly: list[DisassemblyResult] = field(default_factory=list)


@dataclass
class StringResult:
    """Extracted string from binary."""

    value: str
    offset: int
    section: Optional[str] = None
    encoding: str = "ascii"
    entropy: float = 0.0


class BinaryAnalyzer:
    """
    Binary file analyzer for reverse engineering.

    Integrates with:
    - radare2 (r2) - Primary analysis engine
    - objdump - Fallback disassembler
    - file - Format identification
    - strings - String extraction
    - readelf/objdump - Section analysis

    Usage:
        analyzer = BinaryAnalyzer()
        info = analyzer.analyze("/path/to/binary")
        print(info.format, info.architecture)

        # Disassemble function
        disasm = analyzer.disassemble_function("main")

        # Find strings
        strings = analyzer.extract_strings(min_length=8)
    """

    def __init__(self):
        """Initialize binary analyzer."""
        self.mode2600 = get_mode2600()
        self._current_binary: Optional[Path] = None
        self._binary_info: Optional[BinaryInfo] = None

    def analyze(self, binary_path: str | Path) -> BinaryInfo:
        """
        Analyze a binary file.

        Args:
            binary_path: Path to binary file

        Returns:
            BinaryInfo with file details
        """
        context = self.mode2600.require_context()

        path = Path(binary_path)
        if not path.exists():
            raise FileNotFoundError(f"Binary not found: {path}")

        self._current_binary = path

        # Log operation
        self.mode2600.log_operation(
            "analyze_binary",
            "binary_analyzer",
            {"path": str(path)},
            f"Analyzing binary: {path.name}",
            True,
        )

        # Get basic info
        format_type = self._identify_format(path)
        arch, bits, endian = self._identify_architecture(path)

        info = BinaryInfo(
            path=path,
            format=format_type,
            architecture=arch,
            bits=bits,
            endianness=endian,
        )

        # Get detailed info based on format
        if format_type == BinaryFormat.ELF:
            self._analyze_elf(path, info)
        elif format_type == BinaryFormat.PE:
            self._analyze_pe(path, info)
        elif format_type == BinaryFormat.MACHO:
            self._analyze_macho(path, info)

        # Try radare2 for additional info
        if shutil.which("r2"):
            self._analyze_r2(path, info)

        self._binary_info = info
        return info

    def _identify_format(self, path: Path) -> BinaryFormat:
        """Identify binary format using file command."""
        try:
            result = subprocess.run(
                ["file", "-b", str(path)],
                capture_output=True,
                text=True,
            )

            output = result.stdout.lower()

            if "elf" in output:
                return BinaryFormat.ELF
            elif "pe32" in output or "executable" in output and "windows" in output:
                return BinaryFormat.PE
            elif "mach-o" in output:
                return BinaryFormat.MACHO
            elif "java" in output or "class" in output:
                return BinaryFormat.JAVA_CLASS
            elif ".net" in output or "mono" in output:
                return BinaryFormat.DOTNET
            elif "python" in output and "byte" in output:
                return BinaryFormat.PYTHON_PYC

        except Exception:
            pass

        # Check magic bytes
        try:
            with open(path, "rb") as f:
                magic = f.read(4)

            if magic[:4] == b"\x7fELF":
                return BinaryFormat.ELF
            elif magic[:2] == b"MZ":
                return BinaryFormat.PE
            elif magic[:4] in [b"\xfe\xed\xfa\xce", b"\xfe\xed\xfa\xcf",
                               b"\xce\xfa\xed\xfe", b"\xcf\xfa\xed\xfe"]:
                return BinaryFormat.MACHO
            elif magic[:4] == b"\xca\xfe\xba\xbe":
                return BinaryFormat.JAVA_CLASS

        except Exception:
            pass

        return BinaryFormat.UNKNOWN

    def _identify_architecture(self, path: Path) -> tuple[Architecture, int, str]:
        """Identify CPU architecture."""
        try:
            result = subprocess.run(
                ["file", "-b", str(path)],
                capture_output=True,
                text=True,
            )

            output = result.stdout.lower()

            # Bits
            bits = 64 if "64-bit" in output or "x86-64" in output else 32

            # Endianness
            endian = "big" if "msb" in output or "big endian" in output else "little"

            # Architecture
            if "x86-64" in output or "x86_64" in output:
                return Architecture.X86_64, 64, endian
            elif "x86" in output or "i386" in output or "i686" in output:
                return Architecture.X86, 32, endian
            elif "aarch64" in output or "arm64" in output:
                return Architecture.ARM64, 64, endian
            elif "arm" in output:
                return Architecture.ARM, 32, endian
            elif "mips" in output:
                return Architecture.MIPS, bits, endian
            elif "powerpc" in output or "ppc" in output:
                return Architecture.POWERPC, bits, endian
            elif "riscv" in output:
                return Architecture.RISCV, bits, endian

        except Exception:
            pass

        return Architecture.UNKNOWN, 0, "little"

    def _analyze_elf(self, path: Path, info: BinaryInfo) -> None:
        """Analyze ELF binary."""
        try:
            # Get sections
            result = subprocess.run(
                ["readelf", "-S", str(path)],
                capture_output=True,
                text=True,
            )

            for line in result.stdout.split("\n"):
                match = re.search(r"\[\s*\d+\]\s+(\.\w+)", line)
                if match:
                    info.sections.append({"name": match.group(1)})

            # Get imports/libraries
            result = subprocess.run(
                ["readelf", "-d", str(path)],
                capture_output=True,
                text=True,
            )

            for line in result.stdout.split("\n"):
                if "NEEDED" in line:
                    match = re.search(r"\[(.+)\]", line)
                    if match:
                        info.libraries.append(match.group(1))

            # Check security features
            result = subprocess.run(
                ["readelf", "-l", str(path)],
                capture_output=True,
                text=True,
            )

            info.nx = "GNU_STACK" in result.stdout and "RWE" not in result.stdout
            info.pie = "INTERP" in result.stdout

            # Check for symbols
            result = subprocess.run(
                ["readelf", "-s", str(path)],
                capture_output=True,
                text=True,
            )

            info.stripped = "no symbols" in result.stdout.lower()

        except Exception:
            pass

    def _analyze_pe(self, path: Path, info: BinaryInfo) -> None:
        """Analyze PE binary (Windows)."""
        # Would require pefile or similar
        pass

    def _analyze_macho(self, path: Path, info: BinaryInfo) -> None:
        """Analyze Mach-O binary (macOS)."""
        try:
            result = subprocess.run(
                ["otool", "-L", str(path)],
                capture_output=True,
                text=True,
            )

            for line in result.stdout.split("\n")[1:]:
                lib = line.strip().split()[0] if line.strip() else ""
                if lib:
                    info.libraries.append(lib)

        except Exception:
            pass

    def _analyze_r2(self, path: Path, info: BinaryInfo) -> None:
        """Analyze with radare2."""
        try:
            # Get info in JSON
            result = subprocess.run(
                ["r2", "-q", "-c", "iIj", str(path)],
                capture_output=True,
                text=True,
                timeout=30,
            )

            if result.stdout:
                r2_info = json.loads(result.stdout)
                if "bin" in r2_info:
                    bin_info = r2_info["bin"]
                    info.entry_point = bin_info.get("baddr", 0)
                    info.canary = bin_info.get("canary", False)
                    info.nx = bin_info.get("nx", False)
                    info.pie = bin_info.get("pic", False)
                    info.stripped = bin_info.get("stripped", False)

            # Get imports
            result = subprocess.run(
                ["r2", "-q", "-c", "iij", str(path)],
                capture_output=True,
                text=True,
                timeout=30,
            )

            if result.stdout:
                imports = json.loads(result.stdout)
                info.imports = [i.get("name", "") for i in imports if i.get("name")]

            # Get exports
            result = subprocess.run(
                ["r2", "-q", "-c", "iEj", str(path)],
                capture_output=True,
                text=True,
                timeout=30,
            )

            if result.stdout:
                exports = json.loads(result.stdout)
                info.exports = [e.get("name", "") for e in exports if e.get("name")]

        except Exception:
            pass

    def disassemble(
        self,
        address: int = 0,
        length: int = 100,
        function: Optional[str] = None,
    ) -> list[DisassemblyResult]:
        """
        Disassemble code at address or function.

        Args:
            address: Start address (0 for entry point)
            length: Number of instructions
            function: Function name (overrides address)

        Returns:
            List of DisassemblyResult
        """
        if not self._current_binary:
            raise ValueError("No binary loaded. Call analyze() first.")

        results = []

        # Try radare2 first
        if shutil.which("r2"):
            results = self._disassemble_r2(address, length, function)

        # Fallback to objdump
        if not results and shutil.which("objdump"):
            results = self._disassemble_objdump(address, length)

        return results

    def _disassemble_r2(
        self,
        address: int,
        length: int,
        function: Optional[str],
    ) -> list[DisassemblyResult]:
        """Disassemble using radare2."""
        results = []

        try:
            if function:
                cmd = f"aaa; s sym.{function}; pdf"
            else:
                cmd = f"aaa; s {address}; pd {length}"

            result = subprocess.run(
                ["r2", "-q", "-c", cmd, str(self._current_binary)],
                capture_output=True,
                text=True,
                timeout=60,
            )

            for line in result.stdout.split("\n"):
                # Parse r2 output: 0x00001234      4883ec08       sub rsp, 8
                match = re.match(
                    r"\s*(0x[0-9a-f]+)\s+([0-9a-f]+)\s+(\w+)\s*(.*)",
                    line,
                    re.IGNORECASE,
                )
                if match:
                    addr = int(match.group(1), 16)
                    byte_str = match.group(2)
                    mnemonic = match.group(3)
                    operands = match.group(4).strip()

                    results.append(DisassemblyResult(
                        address=addr,
                        bytes=bytes.fromhex(byte_str),
                        mnemonic=mnemonic,
                        operands=operands,
                    ))

        except Exception:
            pass

        return results

    def _disassemble_objdump(self, address: int, length: int) -> list[DisassemblyResult]:
        """Disassemble using objdump."""
        results = []

        try:
            result = subprocess.run(
                ["objdump", "-d", str(self._current_binary)],
                capture_output=True,
                text=True,
            )

            for line in result.stdout.split("\n"):
                # Parse objdump: 1234:	48 83 ec 08          	sub    $0x8,%rsp
                match = re.match(
                    r"\s*([0-9a-f]+):\s+([0-9a-f ]+)\s+(\w+)\s*(.*)",
                    line,
                    re.IGNORECASE,
                )
                if match:
                    addr = int(match.group(1), 16)
                    byte_str = match.group(2).replace(" ", "")
                    mnemonic = match.group(3)
                    operands = match.group(4).strip()

                    results.append(DisassemblyResult(
                        address=addr,
                        bytes=bytes.fromhex(byte_str) if byte_str else b"",
                        mnemonic=mnemonic,
                        operands=operands,
                    ))

                    if len(results) >= length:
                        break

        except Exception:
            pass

        return results

    def extract_strings(
        self,
        min_length: int = 4,
        encoding: str = "auto",
    ) -> list[StringResult]:
        """
        Extract strings from binary.

        Args:
            min_length: Minimum string length
            encoding: "ascii", "unicode", or "auto"

        Returns:
            List of StringResult
        """
        if not self._current_binary:
            raise ValueError("No binary loaded. Call analyze() first.")

        results = []

        try:
            cmd = ["strings", "-n", str(min_length)]

            if encoding == "unicode" or encoding == "auto":
                # Run both ASCII and Unicode
                result_ascii = subprocess.run(
                    cmd + [str(self._current_binary)],
                    capture_output=True,
                    text=True,
                )

                result_unicode = subprocess.run(
                    cmd + ["-e", "l", str(self._current_binary)],
                    capture_output=True,
                    text=True,
                )

                all_strings = set(result_ascii.stdout.split("\n"))
                all_strings.update(result_unicode.stdout.split("\n"))
            else:
                result = subprocess.run(
                    cmd + [str(self._current_binary)],
                    capture_output=True,
                    text=True,
                )
                all_strings = set(result.stdout.split("\n"))

            for s in all_strings:
                if s and len(s) >= min_length:
                    results.append(StringResult(
                        value=s,
                        offset=0,  # Would need more complex parsing for offset
                        entropy=self._calculate_entropy(s),
                    ))

            # Sort by entropy (interesting strings first)
            results.sort(key=lambda x: x.entropy, reverse=True)

        except Exception:
            pass

        return results

    def _calculate_entropy(self, s: str) -> float:
        """Calculate Shannon entropy of string."""
        import math
        from collections import Counter

        if not s:
            return 0.0

        counts = Counter(s)
        length = len(s)

        entropy = 0.0
        for count in counts.values():
            p = count / length
            entropy -= p * math.log2(p)

        return entropy

    def find_functions(self) -> list[FunctionInfo]:
        """Find functions in binary."""
        if not self._current_binary:
            raise ValueError("No binary loaded. Call analyze() first.")

        functions = []

        if shutil.which("r2"):
            try:
                result = subprocess.run(
                    ["r2", "-q", "-c", "aaa; aflj", str(self._current_binary)],
                    capture_output=True,
                    text=True,
                    timeout=120,
                )

                if result.stdout:
                    funcs = json.loads(result.stdout)
                    for f in funcs:
                        functions.append(FunctionInfo(
                            name=f.get("name", "unknown"),
                            address=f.get("offset", 0),
                            size=f.get("size", 0),
                        ))

            except Exception:
                pass

        return functions

    def get_cross_references(self, address: int) -> dict[str, list[int]]:
        """Get cross-references to/from address."""
        xrefs = {"to": [], "from": []}

        if not self._current_binary or not shutil.which("r2"):
            return xrefs

        try:
            # Refs to this address
            result = subprocess.run(
                ["r2", "-q", "-c", f"aaa; axtj {address}", str(self._current_binary)],
                capture_output=True,
                text=True,
                timeout=60,
            )

            if result.stdout:
                refs = json.loads(result.stdout)
                xrefs["to"] = [r.get("from", 0) for r in refs]

            # Refs from this address
            result = subprocess.run(
                ["r2", "-q", "-c", f"aaa; axfj {address}", str(self._current_binary)],
                capture_output=True,
                text=True,
                timeout=60,
            )

            if result.stdout:
                refs = json.loads(result.stdout)
                xrefs["from"] = [r.get("at", 0) for r in refs]

        except Exception:
            pass

        return xrefs


class Decompiler:
    """
    Decompilation assistance using Ghidra headless or RetDec.

    Provides pseudo-code generation from binaries for analysis.
    """

    def __init__(self):
        """Initialize decompiler."""
        self.mode2600 = get_mode2600()

    def decompile(
        self,
        binary_path: str | Path,
        function: Optional[str] = None,
        output_format: str = "c",
    ) -> str:
        """
        Decompile binary to pseudo-code.

        Args:
            binary_path: Path to binary
            function: Specific function to decompile (None for all)
            output_format: "c" or "pseudo"

        Returns:
            Decompiled source code
        """
        context = self.mode2600.require_context()

        path = Path(binary_path)

        # Log operation
        self.mode2600.log_operation(
            "decompile",
            "decompiler",
            {"path": str(path), "function": function},
            f"Decompiling {path.name}",
            True,
        )

        # Try Ghidra headless
        if shutil.which("analyzeHeadless"):
            return self._decompile_ghidra(path, function)

        # Try RetDec
        if shutil.which("retdec-decompiler"):
            return self._decompile_retdec(path)

        # Try r2 with r2dec plugin
        if shutil.which("r2"):
            return self._decompile_r2(path, function)

        return "# No decompiler available (install Ghidra, RetDec, or r2dec)"

    def _decompile_ghidra(self, path: Path, function: Optional[str]) -> str:
        """Decompile using Ghidra headless."""
        # Would require Ghidra installation and script setup
        return "# Ghidra decompilation not implemented yet"

    def _decompile_retdec(self, path: Path) -> str:
        """Decompile using RetDec."""
        try:
            with tempfile.TemporaryDirectory() as tmpdir:
                output_file = Path(tmpdir) / "output.c"

                result = subprocess.run(
                    [
                        "retdec-decompiler",
                        "-o", str(output_file),
                        str(path),
                    ],
                    capture_output=True,
                    text=True,
                    timeout=300,
                )

                if output_file.exists():
                    return output_file.read_text()

        except Exception as e:
            return f"# RetDec error: {e}"

        return "# RetDec decompilation failed"

    def _decompile_r2(self, path: Path, function: Optional[str]) -> str:
        """Decompile using r2 with r2dec/pdc."""
        try:
            if function:
                cmd = f"aaa; s sym.{function}; pdc"
            else:
                cmd = "aaa; pdc @@ sym.*"

            result = subprocess.run(
                ["r2", "-q", "-c", cmd, str(path)],
                capture_output=True,
                text=True,
                timeout=120,
            )

            return result.stdout or "# No output from r2dec"

        except Exception as e:
            return f"# r2dec error: {e}"


# Convenience function


def analyze_binary(path: str | Path) -> BinaryInfo:
    """Analyze a binary file."""
    return BinaryAnalyzer().analyze(path)
