"""
VBA Compilation Module

Compiles VBA source files (.bas) into binary vbaProject.bin format
using the vbaProject-Compiler library by Beakerboy (https://github.com/Beakerboy/MS-OVBA).

This enables TokenMoulds to build complete VBA projects from source code,
providing better security and version control than binary-only macros.

Credits:
- vbaProject-Compiler v0.0.1 by Beakerboy
- OLE file format implementation based on MS-OVBA specification
- VBA module parsing and binary generation
"""

import logging
from pathlib import Path
from typing import Dict, List, Optional, Any

logger = logging.getLogger(__name__)


class VbaCompiler:
    """VBA source code compiler using vbaProject-Compiler library."""

    def __init__(self, vba_compiler_path: Optional[Path] = None):
        """Initialize VBA compiler.

        Args:
            vba_compiler_path: Path to vbaProject-Compiler library (optional)
        """
        self.vba_compiler_path = vba_compiler_path
        self._vba_project_compiler = None

    def compile_project(
        self,
        source_dir: Path,
        output_path: Path,
        project_config: Optional[Dict[str, Any]] = None,
    ) -> bool:
        """Compile VBA source files into vbaProject.bin using Beakerboy's vbaProject-Compiler.

        Args:
            source_dir: Directory containing .bas, .cls, .frm files
            output_path: Where to write the compiled vbaProject.bin
            project_config: VBA project configuration

        Returns:
            True if compilation successful
        """
        try:
            # Load the Beakerboy vbaProject-Compiler library
            if self._vba_project_compiler is None:
                self._load_compiler()

            # Validate source directory
            if not source_dir.exists():
                logger.error(f"VBA source directory not found: {source_dir}")
                return False

            # Find all VBA source files
            vba_files = self._find_vba_files(source_dir)
            if not vba_files:
                logger.warning(f"No VBA source files found in: {source_dir}")
                return False

            logger.info(
                f"Found VBA files: {sum(len(files) for files in vba_files.values())} total"
            )

            # Create VBA project using Beakerboy's library
            project = self._create_vba_project(vba_files, project_config or {})

            # Compile to binary format using Beakerboy's write_file method
            return self._compile_to_binary(project, output_path)

        except ImportError:
            logger.error(
                "vbaProject-Compiler library not available. Install with: pip install git+https://github.com/Beakerboy/MS-OVBA.git"
            )
            return False
        except Exception as e:
            logger.error(f"VBA compilation failed: {e}")
            return False

    def _load_compiler(self):
        """Load the TokenMoulds VBA Compiler (enhanced fork of Beakerboy's library)."""
        try:
            # Try our enhanced fork first
            try:
                from vbaProject_enhanced import VbaProject  # type: ignore[import-not-found]

                logger.info("Loaded TokenMoulds VBA Compiler (enhanced fork)")
            except ImportError:
                # Fall back to original Beakerboy library
                from vbaProjectCompiler.vbaProject import VbaProject  # type: ignore[import-not-found]

                logger.warning(
                    "Using original Beakerboy vbaProject-Compiler v0.0.1 (limited features)"
                )

            self._vba_project_compiler = {
                "VbaProject": VbaProject,
            }

        except ImportError as e:
            raise ImportError(
                f"TokenMoulds VBA Compiler not available: {e}. Install with: pip install tokenmoulds-vba-compiler"
            )

    def _find_vba_files(self, source_dir: Path) -> Dict[str, List[Path]]:
        """Find all VBA source files in directory.

        Returns:
            Dict mapping file types to lists of paths
        """
        vba_files = {
            "modules": [],  # .bas files
            "classes": [],  # .cls files
            "forms": [],  # .frm files
            "sheets": [],  # Excel sheet modules
        }

        for file_path in source_dir.rglob("*"):
            if file_path.is_file():
                suffix = file_path.suffix.lower()
                if suffix == ".bas":
                    vba_files["modules"].append(file_path)
                elif suffix == ".cls":
                    vba_files["classes"].append(file_path)
                elif suffix == ".frm":
                    vba_files["forms"].append(file_path)

        return vba_files

    def _create_vba_project(
        self, vba_files: Dict[str, List[Path]], config: Dict[str, Any]
    ) -> Any:
        """Create a VBA project from source files using Beakerboy's VbaProject."""
        if self._vba_project_compiler is None:
            raise RuntimeError("VBA compiler not loaded")
        VbaProject = self._vba_project_compiler["VbaProject"]

        project = VbaProject()

        # Configure project properties using Beakerboy's API
        if "name" in config:
            project.setProjectId(config["name"])
        if "protection" in config:
            project.set_password(config["protection"].encode("utf-8"))

        # Add default OLE Automation reference (most VBA projects need this)
        self._add_default_references(project)

        # Add modules using Beakerboy's addModule method
        total_modules = 0

        for module_path in vba_files["modules"]:
            module = self._create_module_from_file(module_path, "standard")
            if module:
                project.addModule(module)
                total_modules += 1
                logger.debug(f"Added standard module: {module_path.name}")

        for class_path in vba_files["classes"]:
            cls_module = self._create_module_from_file(class_path, "class")
            if cls_module:
                project.addModule(cls_module)
                total_modules += 1
                logger.debug(f"Added class module: {class_path.name}")

        for form_path in vba_files["forms"]:
            form_module = self._create_module_from_file(form_path, "form")
            if form_module:
                project.addModule(form_module)
                total_modules += 1
                logger.debug(f"Added form module: {form_path.name}")

        # Add custom references from config
        if "references" in config:
            self._add_custom_references(project, config["references"])

        logger.info(
            f"Created VBA project with {total_modules} modules using Beakerboy vbaProject-Compiler"
        )
        return project

    def _create_module_from_file(
        self, file_path: Path, module_type: str
    ) -> Optional[Any]:
        """Create a VBA module from a source file.

        Uses TokenMoulds enhanced module classes with fallbacks to basic implementation.
        """
        try:
            content = file_path.read_text(encoding="utf-8", errors="ignore")
            module_name = file_path.stem

            # Try our enhanced module classes first
            try:
                if module_type == "standard":
                    from modules.std_module import StdModule  # type: ignore[import-not-found]

                    module = StdModule(module_name)
                elif module_type == "class":
                    from modules.class_module import ClassModule  # type: ignore[import-not-found]

                    module = ClassModule(module_name)
                elif module_type == "form":
                    from modules.form_module import FormModule  # type: ignore[import-not-found]

                    module = FormModule(module_name)
                elif module_type == "document":
                    from modules.doc_module import DocModule  # type: ignore[import-not-found]

                    module = DocModule(module_name)
                else:
                    logger.warning(f"Unknown module type: {module_type}")
                    return None

                # Set content using our API
                module.setContent(content)
                logger.debug(
                    f"Created {module_type} module '{module_name}' using TokenMoulds classes"
                )

            except ImportError:
                # Fall back to basic module structure
                logger.debug(
                    "TokenMoulds module classes not available, using basic module structure"
                )
                module = self._create_basic_module(module_name, content, module_type)

            return module

        except Exception as e:
            logger.error(f"Failed to create module from {file_path}: {e}")
            return None

    def _create_basic_module(self, name: str, content: str, module_type: str) -> Any:
        """Create a basic module object when Beakerboy classes aren't available."""

        class BasicModule:
            def __init__(self, name, content, module_type):
                self.name = name
                self.content = content
                self.type = module_type

        return BasicModule(name, content, module_type)

    def _add_default_references(self, project: Any):
        """Add default VBA references (OLE Automation, etc.) using Beakerboy's API."""
        try:
            # Try to use Beakerboy's reference classes
            try:
                from vbaProjectCompiler.reference import LibidReference, ReferenceRecord  # type: ignore[import-not-found]

                # OLE Automation reference (almost always needed)
                libid_ref = LibidReference(
                    "windows",
                    "{00020430-0000-0000-C000-000000000046}",
                    "2.0",
                    "0",
                    "C:\\Windows\\System32\\stdole2.tlb",
                    "OLE Automation",
                )
                ole_ref = ReferenceRecord("cp1252", "stdole", libid_ref)
                project.addReference(ole_ref)
                logger.debug("Added OLE Automation reference")

            except ImportError:
                logger.warning(
                    "Beakerboy reference classes not available, skipping default references"
                )

        except Exception as e:
            logger.warning(f"Failed to add default references: {e}")

    def _add_custom_references(self, project: Any, references: List[Dict[str, Any]]):
        """Add custom references from configuration using Beakerboy's API."""
        try:
            # Try to use Beakerboy's reference classes
            try:
                from vbaProjectCompiler.reference import LibidReference, ReferenceRecord  # type: ignore[import-not-found]

                for ref_config in references:
                    libid_ref = LibidReference(
                        ref_config.get("locale", "windows"),
                        ref_config["guid"],
                        ref_config.get("major_version", "1"),
                        ref_config.get("minor_version", "0"),
                        ref_config.get("path", ""),
                        ref_config.get("description", ""),
                    )
                    ref_record = ReferenceRecord(
                        ref_config.get("codepage", "cp1252"),
                        ref_config["name"],
                        libid_ref,
                    )
                    project.addReference(ref_record)
                    logger.debug(f"Added custom reference: {ref_config['name']}")

            except ImportError:
                logger.warning(
                    "Beakerboy reference classes not available, skipping custom references"
                )

        except Exception as e:
            logger.warning(f"Failed to add custom references: {e}")

    def _compile_to_binary(self, project: Any, output_path: Path) -> bool:
        """Compile VBA project to binary vbaProject.bin format.

        Uses Beakerboy's vbaProject-Compiler where possible, with fallbacks
        for API limitations in v0.0.1.
        """
        try:
            # Ensure output directory exists
            output_path.parent.mkdir(parents=True, exist_ok=True)

            # Try Beakerboy's write_file method
            try:
                import inspect

                if hasattr(project, "write_file"):
                    sig = inspect.signature(project.write_file)
                    if sig.parameters:
                        project.write_file(str(output_path))
                    else:
                        raise AttributeError("write_file takes no arguments")
                else:
                    raise AttributeError("write_file method not available")

            except Exception as e:
                logger.warning(
                    f"Beakerboy write_file failed ({e}), using fallback binary generation"
                )
                # Fallback: create a basic binary structure
                self._create_fallback_binary(project, output_path)

            # Verify the file was created and has content
            if output_path.exists() and output_path.stat().st_size > 0:
                logger.info(
                    f"Successfully compiled VBA project to: {output_path} ({output_path.stat().st_size} bytes)"
                )
                logger.info(
                    "Note: Using enhanced binary format with Beakerboy library integration"
                )
                return True
            else:
                logger.error(
                    f"VBA compilation produced empty or missing file: {output_path}"
                )
                return False

        except Exception as e:
            logger.error(f"VBA binary compilation failed: {e}")
            return False

    def _create_fallback_binary(self, project: Any, output_path: Path) -> None:
        """Fallback when Beakerboy's write_file isn't available.

        Raises RuntimeError instead of creating an invalid OLE structure
        that no Office application can open.
        """
        raise RuntimeError(
            "VBA compilation failed: Beakerboy write_file not available and "
            "no valid fallback exists. Install a compatible vbaProject-Compiler version."
        )

    def validate_compiled_project(self, vba_project_bin: Path) -> Dict[str, Any]:
        """Validate a compiled VBA project binary.

        Checks that the file exists, has non-zero size, and starts with the
        OLE/CFB signature expected by Office applications.

        Args:
            vba_project_bin: Path to vbaProject.bin file

        Returns:
            Dict with validation results
        """
        result: Dict[str, Any] = {"valid": False, "size": 0, "errors": []}

        if not vba_project_bin.exists():
            result["errors"].append("vbaProject.bin file not found")
            return result

        size = vba_project_bin.stat().st_size
        result["size"] = size

        if size < 512:
            result["errors"].append(f"File too small for valid OLE/CFB ({size} bytes)")
            return result

        with open(vba_project_bin, "rb") as f:
            signature = f.read(8)

        expected = b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1"
        if signature != expected:
            result["errors"].append("Invalid OLE/CFB signature")
            return result

        result["valid"] = True
        return result
