"""
Subprocess Steganography Wrapper (v4.0.0)

Runs stegasoo operations in isolated subprocesses to prevent crashes
from taking down the Flask server.

CHANGES in v4.0.0:
- Added channel_key parameter to encode() and decode() methods
- Channel keys enable deployment/group isolation

Usage:
    from subprocess_stego import SubprocessStego

    stego = SubprocessStego()

    # Encode with channel key
    result = stego.encode(
        carrier_data=carrier_bytes,
        reference_data=ref_bytes,
        message="secret message",
        passphrase="my passphrase",
        pin="123456",
        embed_mode="dct",
        channel_key="auto",  # or "none", or explicit key
    )

    if result.success:
        stego_bytes = result.stego_data
        extension = result.extension
    else:
        error_message = result.error

    # Decode
    result = stego.decode(
        stego_data=stego_bytes,
        reference_data=ref_bytes,
        passphrase="my passphrase",
        pin="123456",
        channel_key="auto",
    )

    # Compare modes (capacity)
    result = stego.compare_modes(carrier_bytes)
"""

import base64
import json
import subprocess
import sys
import tempfile
import uuid
from dataclasses import dataclass
from pathlib import Path
from typing import Any

# Default timeout for operations (seconds)
DEFAULT_TIMEOUT = 120

# Path to worker script - adjust if needed
WORKER_SCRIPT = Path(__file__).parent / "stego_worker.py"


@dataclass
class EncodeResult:
    """Result from encode operation."""

    success: bool
    stego_data: bytes | None = None
    filename: str | None = None
    stats: dict[str, Any] | None = None
    # Channel info (v4.0.0)
    channel_mode: str | None = None
    channel_fingerprint: str | None = None
    error: str | None = None
    error_type: str | None = None


@dataclass
class DecodeResult:
    """Result from decode operation."""

    success: bool
    is_file: bool = False
    message: str | None = None
    file_data: bytes | None = None
    filename: str | None = None
    mime_type: str | None = None
    error: str | None = None
    error_type: str | None = None


@dataclass
class CompareResult:
    """Result from compare_modes operation."""

    success: bool
    width: int = 0
    height: int = 0
    lsb: dict[str, Any] | None = None
    dct: dict[str, Any] | None = None
    error: str | None = None


@dataclass
class CapacityResult:
    """Result from capacity check operation."""

    success: bool
    fits: bool = False
    payload_size: int = 0
    capacity: int = 0
    usage_percent: float = 0.0
    headroom: int = 0
    mode: str = ""
    error: str | None = None


@dataclass
class AudioEncodeResult:
    """Result from audio encode operation (v4.3.0)."""

    success: bool
    stego_data: bytes | None = None
    stats: dict[str, Any] | None = None
    channel_mode: str | None = None
    channel_fingerprint: str | None = None
    error: str | None = None
    error_type: str | None = None


@dataclass
class AudioInfoResult:
    """Result from audio info operation (v4.3.0)."""

    success: bool
    sample_rate: int = 0
    channels: int = 0
    duration_seconds: float = 0.0
    num_samples: int = 0
    format: str = ""
    bit_depth: int | None = None
    capacity_lsb: int = 0
    capacity_spread: int = 0
    error: str | None = None


@dataclass
class ChannelStatusResult:
    """Result from channel status check (v4.0.0)."""

    success: bool
    mode: str = "public"
    configured: bool = False
    fingerprint: str | None = None
    source: str | None = None
    key: str | None = None
    error: str | None = None


class SubprocessStego:
    """
    Subprocess-isolated steganography operations.

    All operations run in a separate Python process. If jpeglib or scipy
    crashes, only the subprocess dies - Flask keeps running.
    """

    def __init__(
        self,
        worker_path: Path | None = None,
        python_executable: str | None = None,
        timeout: int = DEFAULT_TIMEOUT,
    ):
        """
        Initialize subprocess wrapper.

        Args:
            worker_path: Path to stego_worker.py (default: same directory)
            python_executable: Python interpreter to use (default: same as current)
            timeout: Default timeout in seconds
        """
        self.worker_path = worker_path or WORKER_SCRIPT
        self.python = python_executable or sys.executable
        self.timeout = timeout

        if not self.worker_path.exists():
            raise FileNotFoundError(f"Worker script not found: {self.worker_path}")

    def _run_worker(self, params: dict[str, Any], timeout: int | None = None) -> dict[str, Any]:
        """
        Run the worker subprocess with given parameters.

        Args:
            params: Dictionary of parameters (will be JSON-encoded)
            timeout: Operation timeout in seconds

        Returns:
            Dictionary with results from worker
        """
        timeout = timeout or self.timeout
        input_json = json.dumps(params)

        try:
            result = subprocess.run(
                [self.python, str(self.worker_path)],
                input=input_json,
                capture_output=True,
                text=True,
                timeout=timeout,
                cwd=str(self.worker_path.parent),
            )

            if result.returncode != 0:
                # Worker crashed
                return {
                    "success": False,
                    "error": f"Worker crashed (exit code {result.returncode})",
                    "stderr": result.stderr,
                }

            if not result.stdout.strip():
                return {
                    "success": False,
                    "error": "Worker returned empty output",
                    "stderr": result.stderr,
                }

            return json.loads(result.stdout)

        except subprocess.TimeoutExpired:
            return {
                "success": False,
                "error": f"Operation timed out after {timeout} seconds",
                "error_type": "TimeoutError",
            }
        except json.JSONDecodeError as e:
            return {
                "success": False,
                "error": f"Invalid JSON from worker: {e}",
                "raw_output": result.stdout if "result" in dir() else None,
            }
        except Exception as e:
            return {
                "success": False,
                "error": str(e),
                "error_type": type(e).__name__,
            }

    def encode(
        self,
        carrier_data: bytes,
        reference_data: bytes,
        message: str | None = None,
        file_data: bytes | None = None,
        file_name: str | None = None,
        file_mime: str | None = None,
        passphrase: str = "",
        pin: str | None = None,
        rsa_key_data: bytes | None = None,
        rsa_password: str | None = None,
        embed_mode: str = "lsb",
        dct_output_format: str = "png",
        dct_color_mode: str = "color",
        # Channel key (v4.0.0)
        channel_key: str | None = "auto",
        timeout: int | None = None,
        # Progress file (v4.1.2)
        progress_file: str | None = None,
    ) -> EncodeResult:
        """
        Encode a message or file into an image.

        Args:
            carrier_data: Carrier image bytes
            reference_data: Reference photo bytes
            message: Text message to encode (if not file)
            file_data: File bytes to encode (if not message)
            file_name: Original filename (for file payload)
            file_mime: MIME type (for file payload)
            passphrase: Encryption passphrase
            pin: Optional PIN
            rsa_key_data: Optional RSA key PEM bytes
            rsa_password: RSA key password if encrypted
            embed_mode: 'lsb' or 'dct'
            dct_output_format: 'png' or 'jpeg' (for DCT mode)
            dct_color_mode: 'grayscale' or 'color' (for DCT mode)
            channel_key: 'auto' (server config), 'none' (public), or explicit key (v4.0.0)
            timeout: Operation timeout in seconds

        Returns:
            EncodeResult with stego_data and extension on success
        """
        params = {
            "operation": "encode",
            "carrier_b64": base64.b64encode(carrier_data).decode("ascii"),
            "reference_b64": base64.b64encode(reference_data).decode("ascii"),
            "message": message,
            "passphrase": passphrase,
            "pin": pin,
            "embed_mode": embed_mode,
            "dct_output_format": dct_output_format,
            "dct_color_mode": dct_color_mode,
            "channel_key": channel_key,  # v4.0.0
            "progress_file": progress_file,  # v4.1.2
        }

        if file_data:
            params["file_b64"] = base64.b64encode(file_data).decode("ascii")
            params["file_name"] = file_name
            params["file_mime"] = file_mime

        if rsa_key_data:
            params["rsa_key_b64"] = base64.b64encode(rsa_key_data).decode("ascii")
            params["rsa_password"] = rsa_password

        result = self._run_worker(params, timeout)

        if result.get("success"):
            return EncodeResult(
                success=True,
                stego_data=base64.b64decode(result["stego_b64"]),
                filename=result.get("filename"),
                stats=result.get("stats"),
                channel_mode=result.get("channel_mode"),
                channel_fingerprint=result.get("channel_fingerprint"),
            )
        else:
            return EncodeResult(
                success=False,
                error=result.get("error", "Unknown error"),
                error_type=result.get("error_type"),
            )

    def decode(
        self,
        stego_data: bytes,
        reference_data: bytes,
        passphrase: str = "",
        pin: str | None = None,
        rsa_key_data: bytes | None = None,
        rsa_password: str | None = None,
        embed_mode: str = "auto",
        # Channel key (v4.0.0)
        channel_key: str | None = "auto",
        timeout: int | None = None,
        # Progress tracking (v4.1.5)
        progress_file: str | None = None,
    ) -> DecodeResult:
        """
        Decode a message or file from a stego image.

        Args:
            stego_data: Stego image bytes
            reference_data: Reference photo bytes
            passphrase: Decryption passphrase
            pin: Optional PIN
            rsa_key_data: Optional RSA key PEM bytes
            rsa_password: RSA key password if encrypted
            embed_mode: 'auto', 'lsb', or 'dct'
            channel_key: 'auto' (server config), 'none' (public), or explicit key (v4.0.0)
            timeout: Operation timeout in seconds
            progress_file: Path to write progress updates (v4.1.5)

        Returns:
            DecodeResult with message or file_data on success
        """
        params = {
            "operation": "decode",
            "stego_b64": base64.b64encode(stego_data).decode("ascii"),
            "reference_b64": base64.b64encode(reference_data).decode("ascii"),
            "passphrase": passphrase,
            "pin": pin,
            "embed_mode": embed_mode,
            "channel_key": channel_key,  # v4.0.0
            "progress_file": progress_file,  # v4.1.5
        }

        if rsa_key_data:
            params["rsa_key_b64"] = base64.b64encode(rsa_key_data).decode("ascii")
            params["rsa_password"] = rsa_password

        result = self._run_worker(params, timeout)

        if result.get("success"):
            if result.get("is_file"):
                return DecodeResult(
                    success=True,
                    is_file=True,
                    file_data=base64.b64decode(result["file_b64"]),
                    filename=result.get("filename"),
                    mime_type=result.get("mime_type"),
                )
            else:
                return DecodeResult(
                    success=True,
                    is_file=False,
                    message=result.get("message"),
                )
        else:
            return DecodeResult(
                success=False,
                error=result.get("error", "Unknown error"),
                error_type=result.get("error_type"),
            )

    def compare_modes(
        self,
        carrier_data: bytes,
        timeout: int | None = None,
    ) -> CompareResult:
        """
        Compare LSB and DCT capacity for a carrier image.

        Args:
            carrier_data: Carrier image bytes
            timeout: Operation timeout in seconds

        Returns:
            CompareResult with capacity information
        """
        params = {
            "operation": "compare",
            "carrier_b64": base64.b64encode(carrier_data).decode("ascii"),
        }

        result = self._run_worker(params, timeout)

        if result.get("success"):
            comparison = result.get("comparison", {})
            return CompareResult(
                success=True,
                width=comparison.get("width", 0),
                height=comparison.get("height", 0),
                lsb=comparison.get("lsb"),
                dct=comparison.get("dct"),
            )
        else:
            return CompareResult(
                success=False,
                error=result.get("error", "Unknown error"),
            )

    def check_capacity(
        self,
        carrier_data: bytes,
        payload_size: int,
        embed_mode: str = "lsb",
        timeout: int | None = None,
    ) -> CapacityResult:
        """
        Check if a payload will fit in the carrier.

        Args:
            carrier_data: Carrier image bytes
            payload_size: Size of payload in bytes
            embed_mode: 'lsb' or 'dct'
            timeout: Operation timeout in seconds

        Returns:
            CapacityResult with fit information
        """
        params = {
            "operation": "capacity",
            "carrier_b64": base64.b64encode(carrier_data).decode("ascii"),
            "payload_size": payload_size,
            "embed_mode": embed_mode,
        }

        result = self._run_worker(params, timeout)

        if result.get("success"):
            r = result.get("result", {})
            return CapacityResult(
                success=True,
                fits=r.get("fits", False),
                payload_size=r.get("payload_size", 0),
                capacity=r.get("capacity", 0),
                usage_percent=r.get("usage_percent", 0.0),
                headroom=r.get("headroom", 0),
                mode=r.get("mode", embed_mode),
            )
        else:
            return CapacityResult(
                success=False,
                error=result.get("error", "Unknown error"),
            )

    # =========================================================================
    # Audio Steganography (v4.3.0)
    # =========================================================================

    def encode_audio(
        self,
        carrier_data: bytes,
        reference_data: bytes,
        message: str | None = None,
        file_data: bytes | None = None,
        file_name: str | None = None,
        file_mime: str | None = None,
        passphrase: str = "",
        pin: str | None = None,
        rsa_key_data: bytes | None = None,
        rsa_password: str | None = None,
        embed_mode: str = "audio_lsb",
        channel_key: str | None = "auto",
        timeout: int | None = None,
        progress_file: str | None = None,
        chip_tier: int | None = None,
    ) -> AudioEncodeResult:
        """
        Encode a message or file into an audio carrier.

        Args:
            carrier_data: Carrier audio bytes (WAV, FLAC, MP3, etc.)
            reference_data: Reference photo bytes
            message: Text message to encode (if not file)
            file_data: File bytes to encode (if not message)
            file_name: Original filename (for file payload)
            file_mime: MIME type (for file payload)
            passphrase: Encryption passphrase
            pin: Optional PIN
            rsa_key_data: Optional RSA key PEM bytes
            rsa_password: RSA key password if encrypted
            embed_mode: 'audio_lsb' or 'audio_spread'
            channel_key: 'auto', 'none', or explicit key
            timeout: Operation timeout (default 300s for audio)
            progress_file: Path to write progress updates

        Returns:
            AudioEncodeResult with stego audio data on success
        """
        params = {
            "operation": "encode_audio",
            "carrier_b64": base64.b64encode(carrier_data).decode("ascii"),
            "reference_b64": base64.b64encode(reference_data).decode("ascii"),
            "message": message,
            "passphrase": passphrase,
            "pin": pin,
            "embed_mode": embed_mode,
            "channel_key": channel_key,
            "progress_file": progress_file,
            "chip_tier": chip_tier,
        }

        if file_data:
            params["file_b64"] = base64.b64encode(file_data).decode("ascii")
            params["file_name"] = file_name
            params["file_mime"] = file_mime

        if rsa_key_data:
            params["rsa_key_b64"] = base64.b64encode(rsa_key_data).decode("ascii")
            params["rsa_password"] = rsa_password

        # Audio operations can be slower (especially spread spectrum)
        result = self._run_worker(params, timeout or 300)

        if result.get("success"):
            return AudioEncodeResult(
                success=True,
                stego_data=base64.b64decode(result["stego_b64"]),
                stats=result.get("stats"),
                channel_mode=result.get("channel_mode"),
                channel_fingerprint=result.get("channel_fingerprint"),
            )
        else:
            return AudioEncodeResult(
                success=False,
                error=result.get("error", "Unknown error"),
                error_type=result.get("error_type"),
            )

    def decode_audio(
        self,
        stego_data: bytes,
        reference_data: bytes,
        passphrase: str = "",
        pin: str | None = None,
        rsa_key_data: bytes | None = None,
        rsa_password: str | None = None,
        embed_mode: str = "audio_auto",
        channel_key: str | None = "auto",
        timeout: int | None = None,
        progress_file: str | None = None,
    ) -> DecodeResult:
        """
        Decode a message or file from stego audio.

        Args:
            stego_data: Stego audio bytes
            reference_data: Reference photo bytes
            passphrase: Decryption passphrase
            pin: Optional PIN
            rsa_key_data: Optional RSA key PEM bytes
            rsa_password: RSA key password if encrypted
            embed_mode: 'audio_auto', 'audio_lsb', or 'audio_spread'
            channel_key: 'auto', 'none', or explicit key
            timeout: Operation timeout (default 300s for audio)
            progress_file: Path to write progress updates

        Returns:
            DecodeResult with message or file_data on success
        """
        params = {
            "operation": "decode_audio",
            "stego_b64": base64.b64encode(stego_data).decode("ascii"),
            "reference_b64": base64.b64encode(reference_data).decode("ascii"),
            "passphrase": passphrase,
            "pin": pin,
            "embed_mode": embed_mode,
            "channel_key": channel_key,
            "progress_file": progress_file,
        }

        if rsa_key_data:
            params["rsa_key_b64"] = base64.b64encode(rsa_key_data).decode("ascii")
            params["rsa_password"] = rsa_password

        result = self._run_worker(params, timeout or 300)

        if result.get("success"):
            if result.get("is_file"):
                return DecodeResult(
                    success=True,
                    is_file=True,
                    file_data=base64.b64decode(result["file_b64"]),
                    filename=result.get("filename"),
                    mime_type=result.get("mime_type"),
                )
            else:
                return DecodeResult(
                    success=True,
                    is_file=False,
                    message=result.get("message"),
                )
        else:
            return DecodeResult(
                success=False,
                error=result.get("error", "Unknown error"),
                error_type=result.get("error_type"),
            )

    def audio_info(
        self,
        audio_data: bytes,
        timeout: int | None = None,
    ) -> AudioInfoResult:
        """
        Get audio file information and steganographic capacity.

        Args:
            audio_data: Audio file bytes
            timeout: Operation timeout in seconds

        Returns:
            AudioInfoResult with metadata and capacity info
        """
        params = {
            "operation": "audio_info",
            "audio_b64": base64.b64encode(audio_data).decode("ascii"),
        }

        result = self._run_worker(params, timeout)

        if result.get("success"):
            info = result.get("info", {})
            return AudioInfoResult(
                success=True,
                sample_rate=info.get("sample_rate", 0),
                channels=info.get("channels", 0),
                duration_seconds=info.get("duration_seconds", 0.0),
                num_samples=info.get("num_samples", 0),
                format=info.get("format", ""),
                bit_depth=info.get("bit_depth"),
                capacity_lsb=info.get("capacity_lsb", 0),
                capacity_spread=info.get("capacity_spread", 0),
            )
        else:
            return AudioInfoResult(
                success=False,
                error=result.get("error", "Unknown error"),
            )

    def get_channel_status(
        self,
        reveal: bool = False,
        timeout: int | None = None,
    ) -> ChannelStatusResult:
        """
        Get current channel key status (v4.0.0).

        Args:
            reveal: Include full key in response
            timeout: Operation timeout in seconds

        Returns:
            ChannelStatusResult with channel info
        """
        params = {
            "operation": "channel_status",
            "reveal": reveal,
        }

        result = self._run_worker(params, timeout)

        if result.get("success"):
            status = result.get("status", {})
            return ChannelStatusResult(
                success=True,
                mode=status.get("mode", "public"),
                configured=status.get("configured", False),
                fingerprint=status.get("fingerprint"),
                source=status.get("source"),
                key=status.get("key") if reveal else None,
            )
        else:
            return ChannelStatusResult(
                success=False,
                error=result.get("error", "Unknown error"),
            )


# Convenience function for quick usage
_default_stego: SubprocessStego | None = None


def get_subprocess_stego() -> SubprocessStego:
    """Get or create default SubprocessStego instance."""
    global _default_stego
    if _default_stego is None:
        _default_stego = SubprocessStego()
    return _default_stego


# =============================================================================
# Progress File Utilities (v4.1.2)
# =============================================================================


def generate_job_id() -> str:
    """Generate a unique job ID for tracking encode/decode operations."""
    return str(uuid.uuid4())[:8]


def get_progress_file_path(job_id: str) -> str:
    """Get the progress file path for a job ID."""
    return str(Path(tempfile.gettempdir()) / f"stegasoo_progress_{job_id}.json")


def read_progress(job_id: str) -> dict | None:
    """
    Read progress from file for a job ID.

    Returns:
        Progress dict with current, total, percent, phase, or None if not found
    """
    progress_file = get_progress_file_path(job_id)
    try:
        with open(progress_file) as f:
            return json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        return None


def cleanup_progress_file(job_id: str) -> None:
    """Remove progress file for a completed job."""
    progress_file = get_progress_file_path(job_id)
    try:
        Path(progress_file).unlink(missing_ok=True)
    except Exception:
        pass
