"""
Authenticode Signing Module

Handles Microsoft Authenticode signing for VBA projects and Office documents.
Provides certificate management, timestamping, and signature validation.

Cross-Platform Support:
- Windows: Uses signtool.exe from Windows SDK
- macOS/Linux: Uses osslsigncode (https://github.com/mtrojnar/osslsigncode)

Install osslsigncode:
- macOS: brew install osslsigncode
- Ubuntu/Debian: sudo apt install osslsigncode
- From source: https://github.com/mtrojnar/osslsigncode
"""

import logging
from pathlib import Path
from typing import Dict, List, Optional, Any
import subprocess
import shutil
import os
import platform

logger = logging.getLogger(__name__)


class SigningBackend:
    """Abstract signing backend interface."""

    SIGNTOOL = "signtool"
    OSSLSIGNCODE = "osslsigncode"


class AuthenticodeSigner:
    """Microsoft Authenticode signing for VBA projects and Office documents.

    Provides comprehensive Authenticode signing capabilities including:
    - VBA project binary signing
    - Office document signing (.docx, .xlsx, .pptx)
    - Certificate management and validation
    - Timestamping for signature longevity
    - Cross-platform support (Windows signtool + macOS/Linux osslsigncode)
    """

    # Common timestamp servers
    TIMESTAMP_SERVERS = {
        "digicert": "http://timestamp.digicert.com",
        "globalsign": "http://timestamp.globalsign.com/tsa/r6advanced1",
        "comodo": "http://timestamp.comodoca.com/authenticode",
        "verisign": "http://timestamp.verisign.com/scripts/timstamp.dll",
        "sectigo": "http://timestamp.sectigo.com",
    }

    # RFC 3161 timestamp servers (for osslsigncode -ts flag)
    RFC3161_TIMESTAMP_SERVERS = {
        "digicert": "http://timestamp.digicert.com",
        "globalsign": "http://rfc3161timestamp.globalsign.com/advanced",
        "sectigo": "http://timestamp.sectigo.com",
        "comodo": "http://timestamp.comodoca.com/rfc3161",
        "entrust": "http://timestamp.entrust.net/TSS/RFC3161sha2TS",
    }

    def __init__(
        self,
        signtool_path: Optional[str] = None,
        osslsigncode_path: Optional[str] = None,
        default_timestamp_server: str = "digicert",
        prefer_osslsigncode: bool = False,
    ):
        """Initialize Authenticode signer.

        Args:
            signtool_path: Path to signtool.exe (Windows SDK tool)
            osslsigncode_path: Path to osslsigncode binary
            default_timestamp_server: Default timestamp server ('digicert', 'globalsign', etc.)
            prefer_osslsigncode: Prefer osslsigncode even on Windows (useful for CI/CD)
        """
        self._is_windows = os.name == "nt"
        self._is_macos = platform.system() == "Darwin"
        self._is_linux = platform.system() == "Linux"

        # Find available signing tools
        self.signtool_path = signtool_path or self._find_signtool()
        self.osslsigncode_path = osslsigncode_path or self._find_osslsigncode()

        self.cert_store = CertificateStore()
        self.default_timestamp_server = default_timestamp_server
        self.prefer_osslsigncode = prefer_osslsigncode

        # Determine active backend
        self._backend = self._determine_backend()

        if not self._backend:
            logger.warning(
                "No signing backend available. Install osslsigncode (cross-platform) "
                "or use Windows with signtool.exe"
            )

    def _find_signtool(self) -> Optional[str]:
        """Find signtool.exe in common locations (Windows only)."""
        if not self._is_windows:
            return None

        common_paths = [
            r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe",
            r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64\signtool.exe",
            r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64\signtool.exe",
            r"C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\signtool.exe",
            r"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1\Bin\signtool.exe",
        ]

        for path in common_paths:
            if Path(path).exists():
                return path

        # Check PATH
        try:
            result = subprocess.run(
                ["where", "signtool"], capture_output=True, text=True
            )
            if result.returncode == 0:
                return result.stdout.strip().split("\n")[0]
        except Exception:
            pass

        return None

    def _find_osslsigncode(self) -> Optional[str]:
        """Find osslsigncode in common locations (cross-platform)."""
        # Check common installation paths
        common_paths = []

        if self._is_macos:
            common_paths = [
                "/usr/local/bin/osslsigncode",
                "/opt/homebrew/bin/osslsigncode",
            ]
        elif self._is_linux:
            common_paths = [
                "/usr/bin/osslsigncode",
                "/usr/local/bin/osslsigncode",
            ]
        elif self._is_windows:
            common_paths = [
                r"C:\osslsigncode\osslsigncode.exe",
                r"C:\Program Files\osslsigncode\osslsigncode.exe",
            ]

        for path in common_paths:
            if Path(path).exists():
                return path

        # Check PATH using 'which' (Unix) or 'where' (Windows)
        try:
            cmd = (
                ["where", "osslsigncode"]
                if self._is_windows
                else ["which", "osslsigncode"]
            )
            result = subprocess.run(cmd, capture_output=True, text=True)
            if result.returncode == 0:
                return result.stdout.strip().split("\n")[0]
        except Exception:
            pass

        return None

    def _determine_backend(self) -> Optional[str]:
        """Determine which signing backend to use."""
        if self.prefer_osslsigncode and self.osslsigncode_path:
            return SigningBackend.OSSLSIGNCODE

        if self._is_windows and self.signtool_path:
            return SigningBackend.SIGNTOOL

        if self.osslsigncode_path:
            return SigningBackend.OSSLSIGNCODE

        return None

    @property
    def is_available(self) -> bool:
        """Check if signing is available."""
        return self._backend is not None

    @property
    def backend_name(self) -> Optional[str]:
        """Get the name of the active signing backend."""
        return self._backend

    def sign_file(
        self,
        file_path: Path,
        certificate_path: Path,
        certificate_password: Optional[str] = None,
        timestamp_url: Optional[str] = None,
        description: Optional[str] = None,
        url: Optional[str] = None,
        hash_algorithm: str = "sha256",
    ) -> bool:
        """Sign a file with Authenticode certificate (cross-platform).

        Args:
            file_path: Path to file to sign
            certificate_path: Path to .pfx/.p12 certificate file
            certificate_password: Certificate password (if required)
            timestamp_url: Timestamp server URL or server name ('digicert', 'globalsign', etc.)
            description: Description of the signed content
            url: URL associated with the signer
            hash_algorithm: Hash algorithm ('sha256', 'sha384', 'sha512')

        Returns:
            True if signing successful
        """
        if not self._backend:
            logger.error("No signing backend available")
            return False

        if not file_path.exists():
            logger.error(f"File not found: {file_path}")
            return False

        if not certificate_path.exists():
            logger.error(f"Certificate not found: {certificate_path}")
            return False

        # Resolve timestamp server
        resolved_timestamp = self._resolve_timestamp_url(timestamp_url)

        if self._backend == SigningBackend.OSSLSIGNCODE:
            return self._sign_with_osslsigncode(
                file_path,
                certificate_path,
                certificate_password,
                resolved_timestamp,
                description,
                url,
                hash_algorithm,
            )
        else:
            return self._sign_with_signtool(
                file_path,
                certificate_path,
                certificate_password,
                resolved_timestamp,
                description,
                url,
                hash_algorithm,
            )

    def _resolve_timestamp_url(self, timestamp_url: Optional[str]) -> str:
        """Resolve timestamp server name to URL."""
        if timestamp_url and timestamp_url in self.TIMESTAMP_SERVERS:
            return self.TIMESTAMP_SERVERS[timestamp_url]
        elif timestamp_url:
            return timestamp_url
        else:
            return self.TIMESTAMP_SERVERS[self.default_timestamp_server]

    def _resolve_rfc3161_timestamp_url(self, timestamp_url: Optional[str]) -> str:
        """Resolve RFC 3161 timestamp server name to URL."""
        if timestamp_url and timestamp_url in self.RFC3161_TIMESTAMP_SERVERS:
            return self.RFC3161_TIMESTAMP_SERVERS[timestamp_url]
        elif timestamp_url:
            return timestamp_url
        else:
            return self.RFC3161_TIMESTAMP_SERVERS.get(
                self.default_timestamp_server,
                self.RFC3161_TIMESTAMP_SERVERS["digicert"],
            )

    def _sign_with_osslsigncode(
        self,
        file_path: Path,
        certificate_path: Path,
        certificate_password: Optional[str],
        timestamp_url: str,
        description: Optional[str],
        url: Optional[str],
        hash_algorithm: str,
    ) -> bool:
        """Sign using osslsigncode (cross-platform)."""
        try:
            # Use RFC 3161 timestamp for osslsigncode
            rfc3161_url = self._resolve_rfc3161_timestamp_url(
                self.default_timestamp_server
            )

            # Create temporary output file
            output_path = (
                file_path.parent / f"{file_path.stem}_signed{file_path.suffix}"
            )

            cmd = [
                self.osslsigncode_path,
                "sign",
                "-pkcs12",
                str(certificate_path),
                "-h",
                hash_algorithm,
                "-ts",
                rfc3161_url,  # RFC 3161 timestamp
                "-in",
                str(file_path),
                "-out",
                str(output_path),
            ]

            # Add password if provided
            if certificate_password:
                cmd.extend(["-pass", certificate_password])

            # Add description and URL if provided
            if description:
                cmd.extend(["-n", description])
            if url:
                cmd.extend(["-i", url])

            logger.info(f"Signing with osslsigncode: {file_path}")
            safe_cmd = [
                ("***" if i > 0 and cmd[i - 1] == "-pass" else arg)
                for i, arg in enumerate(cmd)
            ]
            logger.debug(f"Command: {' '.join(safe_cmd)}")

            result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)

            if result.returncode == 0 and output_path.exists():
                # Replace original with signed file
                shutil.move(str(output_path), str(file_path))
                logger.info(f"Successfully signed with osslsigncode: {file_path}")
                return True
            else:
                logger.error(f"osslsigncode signing failed: {result.stderr}")
                if output_path.exists():
                    output_path.unlink()
                return False

        except Exception as e:
            logger.error(f"osslsigncode signing error: {e}")
            return False

    def _sign_with_signtool(
        self,
        file_path: Path,
        certificate_path: Path,
        certificate_password: Optional[str],
        timestamp_url: str,
        description: Optional[str],
        url: Optional[str],
        hash_algorithm: str,
    ) -> bool:
        """Sign using Windows signtool.exe."""
        try:
            cmd = [
                self.signtool_path,
                "sign",
                "/f",
                str(certificate_path),
                "/t",
                timestamp_url,
                "/fd",
                hash_algorithm.upper(),
                "/v",
            ]

            if certificate_password:
                cmd.extend(["/p", certificate_password])

            if description:
                cmd.extend(["/d", description])
            if url:
                cmd.extend(["/du", url])

            cmd.append(str(file_path))

            logger.info(f"Signing with signtool: {file_path}")
            safe_cmd = [
                ("***" if i > 0 and cmd[i - 1] == "/p" else arg)
                for i, arg in enumerate(cmd)
            ]
            logger.debug(f"Command: {' '.join(safe_cmd)}")

            result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)

            if result.returncode == 0:
                logger.info(f"Successfully signed with signtool: {file_path}")
                return True
            else:
                logger.error(f"signtool signing failed: {result.stderr}")
                return False

        except Exception as e:
            logger.error(f"signtool signing error: {e}")
            return False

    def sign_vba_project(
        self,
        vba_project_bin: Path,
        certificate_path: Path,
        certificate_password: Optional[str] = None,
        timestamp_url: Optional[str] = None,
        description: Optional[str] = None,
        url: Optional[str] = None,
    ) -> bool:
        """Sign a VBA project binary with Authenticode certificate.

        Args:
            vba_project_bin: Path to vbaProject.bin file
            certificate_path: Path to .pfx/.p12 certificate file
            certificate_password: Certificate password (if required)
            timestamp_url: Timestamp server URL or server name
            description: Description of the signed content
            url: URL associated with the signer

        Returns:
            True if signing successful
        """
        return self.sign_file(
            vba_project_bin,
            certificate_path,
            certificate_password,
            timestamp_url,
            description or "VBA Project",
            url,
        )

    def sign_office_document(
        self,
        document_path: Path,
        certificate_path: Path,
        certificate_password: Optional[str] = None,
        timestamp_url: Optional[str] = None,
        description: Optional[str] = None,
        url: Optional[str] = None,
    ) -> bool:
        """Sign an Office document with Authenticode certificate.

        Args:
            document_path: Path to Office document
            certificate_path: Path to .pfx/.p12 certificate file
            certificate_password: Certificate password (if required)
            timestamp_url: Timestamp server URL or server name
            description: Description of the signed content
            url: URL associated with the signer

        Returns:
            True if signing successful
        """
        valid_extensions = {
            ".docx",
            ".docm",
            ".xlsx",
            ".xlsm",
            ".pptx",
            ".pptm",
            ".potx",
            ".potm",
            ".xltx",
            ".xltm",
            ".dotx",
            ".dotm",
        }
        if document_path.suffix.lower() not in valid_extensions:
            logger.error(f"Unsupported file type for signing: {document_path.suffix}")
            return False

        return self.sign_file(
            document_path,
            certificate_path,
            certificate_password,
            timestamp_url,
            description or "Office Document",
            url,
        )

    def get_signing_recommendations(self) -> Dict[str, Any]:
        """Get recommendations for Authenticode signing setup.

        Returns:
            Dict with setup recommendations and requirements
        """
        recommendations = {
            "platform": platform.system(),
            "available_backends": [],
            "active_backend": self._backend,
            "requirements": [],
            "installation": [],
            "setup_steps": [],
            "best_practices": [],
        }

        # Check available backends
        if self.signtool_path:
            recommendations["available_backends"].append("signtool")
        if self.osslsigncode_path:
            recommendations["available_backends"].append("osslsigncode")

        # Platform-specific installation instructions
        if self._is_macos:
            recommendations["installation"] = [
                "Install osslsigncode via Homebrew:",
                "  brew install osslsigncode",
                "",
                "Or build from source:",
                "  git clone https://github.com/mtrojnar/osslsigncode.git",
                "  cd osslsigncode && mkdir build && cd build",
                "  cmake -S .. && cmake --build .",
                "  sudo cmake --install .",
            ]
        elif self._is_linux:
            recommendations["installation"] = [
                "Install osslsigncode via package manager:",
                "  Ubuntu/Debian: sudo apt install osslsigncode",
                "  Fedora: sudo dnf install osslsigncode",
                "  Arch: sudo pacman -S osslsigncode",
                "",
                "Or build from source:",
                "  sudo apt install cmake libssl-dev libcurl4-openssl-dev",
                "  git clone https://github.com/mtrojnar/osslsigncode.git",
                "  cd osslsigncode && mkdir build && cd build",
                "  cmake -S .. && cmake --build .",
                "  sudo cmake --install .",
            ]
        elif self._is_windows:
            recommendations["installation"] = [
                "Option 1: Windows SDK (includes signtool.exe)",
                "  Download: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/",
                "",
                "Option 2: osslsigncode (cross-platform, works in CI/CD)",
                "  Download: https://github.com/mtrojnar/osslsigncode/releases",
                "  Or build with CMake and Visual Studio",
            ]

        recommendations["requirements"] = [
            {
                "type": "certificate",
                "name": "Code Signing Certificate",
                "format": "PKCS#12 (.pfx/.p12)",
                "providers": ["DigiCert", "GlobalSign", "Sectigo", "Entrust", "Comodo"],
                "purpose": "Authenticode certificate for signing Office macros and documents",
            }
        ]

        recommendations["setup_steps"] = [
            "1. Obtain a code signing certificate from a trusted CA",
            "2. Export certificate as .pfx/.p12 file with private key",
            "3. Install osslsigncode (macOS/Linux) or signtool (Windows)",
            "4. Configure timestamp server for signature longevity",
            "5. Test signing with a sample file",
            "6. Store certificate securely (consider HSM for production)",
        ]

        recommendations["best_practices"] = [
            "Use SHA256 or stronger hash algorithms",
            "Always timestamp signatures to prevent expiration issues",
            "Store certificates securely, use hardware security modules when possible",
            "Use different certificates for development and production",
            "Regularly renew certificates before expiration",
            "Validate signatures after signing to ensure integrity",
            "Use RFC 3161 timestamps for better compatibility",
        ]

        return recommendations


class CertificateStore:
    """Manages code signing certificates for Authenticode.

    Provides certificate discovery, validation, and management for
    enterprise code signing workflows.
    """

    def __init__(self):
        self.certificates: Dict[str, Dict[str, Any]] = {}
        self._windows_cert_stores = ["My", "Root", "CA", "Trust"]

    def add_certificate(
        self, name: str, cert_path: str, password: Optional[str] = None
    ):
        """Add a certificate file to the store."""
        cert_path_obj = Path(cert_path)
        self.certificates[name] = {
            "path": cert_path_obj,
            "password": password,
            "type": "file",
            "valid": cert_path_obj.exists(),
        }

    def list_certificates(self) -> List[str]:
        """List available certificates."""
        return list(self.certificates.keys())


def get_signer_status() -> Dict[str, Any]:
    """Get current signer status and availability.

    Returns:
        Dict with signer status information
    """
    signer = AuthenticodeSigner()
    return {
        "available": signer.is_available,
        "backend": signer.backend_name,
        "signtool_path": signer.signtool_path,
        "osslsigncode_path": signer.osslsigncode_path,
        "platform": platform.system(),
        "recommendations": signer.get_signing_recommendations(),
    }
