"""
AIPTX Beast Mode - Exploit Chain Executor
==========================================

Executes planned exploit chains step by step with automatic fallback,
retry logic, and state management.

Key Features:
- Step-by-step execution with condition checking
- Automatic fallback to alternative techniques
- Retry logic with exponential backoff
- State persistence between steps
- Real-time progress callbacks
- Abort and resume capabilities
"""

from __future__ import annotations

import asyncio
import logging
import time
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Callable, Optional, Protocol

from aipt_v2.exploitation.chain_builder import (
    AttackChain,
    ChainStep,
    ChainStatus,
    StepStatus,
)
from aipt_v2.exploitation.chain_templates import StepType

logger = logging.getLogger(__name__)


class TechniqueHandler(Protocol):
    """Protocol for technique execution handlers."""

    async def execute(
        self,
        technique: str,
        context: dict[str, Any],
        step: ChainStep,
    ) -> dict[str, Any]:
        """
        Execute a technique and return results.

        Args:
            technique: Technique name to execute
            context: Current chain context
            step: The step being executed

        Returns:
            dict with keys:
                - success: bool
                - data: dict of produced outputs
                - error: str if failed
        """
        ...


@dataclass
class ExecutionResult:
    """Result of executing a chain or step."""
    success: bool
    chain_id: str
    status: ChainStatus
    steps_completed: int
    steps_total: int
    final_context: dict[str, Any]
    error: str | None = None
    execution_time: float = 0.0
    step_results: list[dict[str, Any]] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return {
            "success": self.success,
            "chain_id": self.chain_id,
            "status": self.status.value,
            "steps_completed": self.steps_completed,
            "steps_total": self.steps_total,
            "final_context": self.final_context,
            "error": self.error,
            "execution_time": self.execution_time,
            "step_results": self.step_results,
        }


class DefaultTechniqueHandler:
    """
    Default technique handler that dispatches to specific implementations.

    This is a placeholder that should be replaced with actual technique
    implementations that interact with the target.
    """

    def __init__(self):
        """Initialize with technique mappings."""
        # Map technique names to handler methods
        self._handlers: dict[str, Callable] = {
            # SQLi techniques
            "error_based": self._sqli_error_based,
            "union_based": self._sqli_union_based,
            "blind_boolean": self._sqli_blind_boolean,
            "blind_time": self._sqli_blind_time,
            "into_outfile": self._sqli_file_write,
            "into_dumpfile": self._sqli_file_write,
            # SSRF techniques
            "dns_callback": self._ssrf_callback,
            "http_callback": self._ssrf_callback,
            "aws_metadata": self._ssrf_cloud_metadata,
            # Upload techniques
            "double_extension": self._upload_bypass,
            "null_byte": self._upload_bypass,
            "mime_spoof": self._upload_bypass,
            # XSS techniques
            "alert_test": self._xss_test,
            "cookie_theft": self._xss_cookie_steal,
            # Command execution
            "http_request": self._execute_webshell,
            "command_injection": self._execute_command,
            # Generic
            "default": self._generic_technique,
        }

    async def execute(
        self,
        technique: str,
        context: dict[str, Any],
        step: ChainStep,
    ) -> dict[str, Any]:
        """Execute a technique."""
        handler = self._handlers.get(technique, self._handlers["default"])
        try:
            return await handler(technique, context, step)
        except Exception as e:
            logger.error(f"Technique {technique} failed: {e}")
            return {"success": False, "data": {}, "error": str(e)}

    async def _generic_technique(
        self,
        technique: str,
        context: dict[str, Any],
        step: ChainStep,
    ) -> dict[str, Any]:
        """Generic technique placeholder."""
        logger.info(f"Executing technique: {technique}")
        # In real implementation, this would dispatch to actual exploit code
        return {
            "success": True,
            "data": {output: f"{output}_value" for output in step.produces},
            "error": None,
        }

    async def _sqli_error_based(
        self,
        technique: str,
        context: dict[str, Any],
        step: ChainStep,
    ) -> dict[str, Any]:
        """Error-based SQL injection technique."""
        # Placeholder for actual SQLi implementation
        return {
            "success": True,
            "data": {
                "dbms_type": "mysql",
                "injection_type": "error_based",
            },
            "error": None,
        }

    async def _sqli_union_based(
        self,
        technique: str,
        context: dict[str, Any],
        step: ChainStep,
    ) -> dict[str, Any]:
        """Union-based SQL injection technique."""
        return {
            "success": True,
            "data": {
                "dbms_type": context.get("dbms_type", "mysql"),
                "injection_type": "union_based",
                "column_count": 5,
            },
            "error": None,
        }

    async def _sqli_blind_boolean(
        self,
        technique: str,
        context: dict[str, Any],
        step: ChainStep,
    ) -> dict[str, Any]:
        """Boolean blind SQL injection technique."""
        return {
            "success": True,
            "data": {
                "dbms_type": "mysql",
                "injection_type": "blind_boolean",
            },
            "error": None,
        }

    async def _sqli_blind_time(
        self,
        technique: str,
        context: dict[str, Any],
        step: ChainStep,
    ) -> dict[str, Any]:
        """Time-based blind SQL injection technique."""
        return {
            "success": True,
            "data": {
                "dbms_type": "mysql",
                "injection_type": "blind_time",
            },
            "error": None,
        }

    async def _sqli_file_write(
        self,
        technique: str,
        context: dict[str, Any],
        step: ChainStep,
    ) -> dict[str, Any]:
        """SQL injection file write technique."""
        return {
            "success": True,
            "data": {
                "webshell_path": "/var/www/html/shell.php",
                "webshell_url": f"{context.get('target', '')}/shell.php",
            },
            "error": None,
        }

    async def _ssrf_callback(
        self,
        technique: str,
        context: dict[str, Any],
        step: ChainStep,
    ) -> dict[str, Any]:
        """SSRF callback confirmation technique."""
        return {
            "success": True,
            "data": {
                "ssrf_confirmed": True,
                "protocol_support": ["http", "https", "file"],
            },
            "error": None,
        }

    async def _ssrf_cloud_metadata(
        self,
        technique: str,
        context: dict[str, Any],
        step: ChainStep,
    ) -> dict[str, Any]:
        """SSRF cloud metadata access technique."""
        return {
            "success": True,
            "data": {
                "cloud_credentials": {"access_key": "AKIA***", "secret_key": "***"},
                "iam_role": "ec2-role",
            },
            "error": None,
        }

    async def _upload_bypass(
        self,
        technique: str,
        context: dict[str, Any],
        step: ChainStep,
    ) -> dict[str, Any]:
        """File upload bypass technique."""
        return {
            "success": True,
            "data": {
                "bypass_method": technique,
                "uploaded_file": "shell.php.jpg",
            },
            "error": None,
        }

    async def _xss_test(
        self,
        technique: str,
        context: dict[str, Any],
        step: ChainStep,
    ) -> dict[str, Any]:
        """XSS confirmation technique."""
        return {
            "success": True,
            "data": {
                "xss_type": "reflected",
                "context": "html_body",
                "encoding_required": False,
            },
            "error": None,
        }

    async def _xss_cookie_steal(
        self,
        technique: str,
        context: dict[str, Any],
        step: ChainStep,
    ) -> dict[str, Any]:
        """XSS cookie theft technique."""
        return {
            "success": True,
            "data": {
                "session_token": "abc123...",
                "auth_cookies": {"session": "abc123"},
            },
            "error": None,
        }

    async def _execute_webshell(
        self,
        technique: str,
        context: dict[str, Any],
        step: ChainStep,
    ) -> dict[str, Any]:
        """Execute command via webshell."""
        return {
            "success": True,
            "data": {
                "command_output": "uid=33(www-data) gid=33(www-data)",
                "os_type": "linux",
                "current_privileges": "www-data",
            },
            "error": None,
        }

    async def _execute_command(
        self,
        technique: str,
        context: dict[str, Any],
        step: ChainStep,
    ) -> dict[str, Any]:
        """Direct command execution technique."""
        return {
            "success": True,
            "data": {
                "command_output": "command executed",
                "exit_code": 0,
            },
            "error": None,
        }


class ChainExecutor:
    """
    Executes exploit chains with automatic fallback and retry logic.

    The executor manages the state of chain execution, handles failures
    with fallback techniques, and provides progress callbacks.
    """

    def __init__(
        self,
        technique_handler: TechniqueHandler | None = None,
        max_retries: int = 2,
        step_timeout: float = 120.0,
        on_step_complete: Callable[[ChainStep, dict], None] | None = None,
        on_chain_complete: Callable[[AttackChain, ExecutionResult], None] | None = None,
    ):
        """
        Initialize the chain executor.

        Args:
            technique_handler: Handler for executing techniques
            max_retries: Maximum retries per step
            step_timeout: Timeout for each step in seconds
            on_step_complete: Callback when a step completes
            on_chain_complete: Callback when the chain completes
        """
        self.technique_handler = technique_handler or DefaultTechniqueHandler()
        self.max_retries = max_retries
        self.step_timeout = step_timeout
        self.on_step_complete = on_step_complete
        self.on_chain_complete = on_chain_complete
        self._abort_requested = False

    async def execute_chain(
        self,
        chain: AttackChain,
        dry_run: bool = False,
    ) -> ExecutionResult:
        """
        Execute a complete exploit chain.

        Args:
            chain: The chain to execute
            dry_run: If True, simulate without actual exploitation

        Returns:
            ExecutionResult with final state
        """
        start_time = time.time()
        chain.status = ChainStatus.IN_PROGRESS
        chain.started_at = datetime.now(timezone.utc).isoformat()
        step_results = []
        self._abort_requested = False

        logger.info(f"Starting chain execution: {chain.name} ({chain.chain_id})")

        try:
            while chain.current_step_index < len(chain.steps):
                if self._abort_requested:
                    chain.status = ChainStatus.ABORTED
                    break

                step = chain.current_step()
                if not step:
                    break

                # Check conditions
                if not chain.check_step_conditions(step):
                    logger.warning(
                        f"Step '{step.name}' conditions not met. "
                        f"Required: {step.required_conditions}, "
                        f"Available: {chain.get_available_conditions()}"
                    )
                    step.status = StepStatus.SKIPPED
                    step.error = "Required conditions not met"
                    step_results.append(step.to_dict())
                    chain.advance_step()
                    continue

                # Execute the step
                step_result = await self._execute_step(step, chain.context, dry_run)
                step_results.append(step_result)

                if step.status == StepStatus.SUCCESS:
                    # Merge step outputs into chain context
                    chain.context.update(step.output)
                    logger.info(f"Step '{step.name}' completed successfully")
                else:
                    # Step failed even after retries
                    chain.status = ChainStatus.STEP_FAILED
                    logger.error(f"Step '{step.name}' failed: {step.error}")
                    break

                # Callback
                if self.on_step_complete:
                    self.on_step_complete(step, chain.context)

                chain.advance_step()

            # Determine final status
            if chain.status == ChainStatus.IN_PROGRESS:
                chain.status = ChainStatus.COMPLETED
                chain.completed_at = datetime.now(timezone.utc).isoformat()

            execution_time = time.time() - start_time

            result = ExecutionResult(
                success=chain.status == ChainStatus.COMPLETED,
                chain_id=chain.chain_id,
                status=chain.status,
                steps_completed=sum(1 for s in chain.steps if s.status == StepStatus.SUCCESS),
                steps_total=len(chain.steps),
                final_context=chain.context,
                error=chain.steps[chain.current_step_index - 1].error if chain.status == ChainStatus.STEP_FAILED else None,
                execution_time=execution_time,
                step_results=step_results,
            )

            if self.on_chain_complete:
                self.on_chain_complete(chain, result)

            logger.info(
                f"Chain execution finished: {chain.status.value} "
                f"({result.steps_completed}/{result.steps_total} steps) "
                f"in {execution_time:.2f}s"
            )

            return result

        except Exception as e:
            logger.exception(f"Chain execution error: {e}")
            chain.status = ChainStatus.ABORTED
            return ExecutionResult(
                success=False,
                chain_id=chain.chain_id,
                status=ChainStatus.ABORTED,
                steps_completed=sum(1 for s in chain.steps if s.status == StepStatus.SUCCESS),
                steps_total=len(chain.steps),
                final_context=chain.context,
                error=str(e),
                execution_time=time.time() - start_time,
                step_results=step_results,
            )

    async def _execute_step(
        self,
        step: ChainStep,
        context: dict[str, Any],
        dry_run: bool = False,
    ) -> dict[str, Any]:
        """
        Execute a single step with retry and fallback logic.

        Args:
            step: The step to execute
            context: Current chain context
            dry_run: If True, simulate without actual execution

        Returns:
            Step result dict
        """
        step.status = StepStatus.IN_PROGRESS
        step.started_at = datetime.now(timezone.utc).isoformat()

        # Try primary techniques first
        all_techniques = step.techniques + step.fallback_techniques

        for technique in all_techniques:
            if self._abort_requested:
                step.status = StepStatus.FAILED
                step.error = "Execution aborted"
                break

            step.selected_technique = technique
            step.retry_count = 0

            while step.retry_count <= self.max_retries:
                try:
                    logger.debug(f"Trying technique '{technique}' (attempt {step.retry_count + 1})")

                    if dry_run:
                        # Simulate success in dry run
                        result = {
                            "success": True,
                            "data": {output: f"[DRY_RUN] {output}" for output in step.produces},
                            "error": None,
                        }
                    else:
                        # Execute with timeout
                        result = await asyncio.wait_for(
                            self.technique_handler.execute(technique, context, step),
                            timeout=self.step_timeout,
                        )

                    if result.get("success"):
                        step.status = StepStatus.SUCCESS
                        step.output = result.get("data", {})
                        step.completed_at = datetime.now(timezone.utc).isoformat()
                        return step.to_dict()

                    # Technique failed, try next
                    step.error = result.get("error", "Unknown error")
                    logger.debug(f"Technique '{technique}' failed: {step.error}")
                    break  # Don't retry on logical failure, try next technique

                except asyncio.TimeoutError:
                    step.error = f"Technique '{technique}' timed out"
                    logger.warning(step.error)
                    step.retry_count += 1

                except Exception as e:
                    step.error = str(e)
                    logger.warning(f"Technique '{technique}' error: {e}")
                    step.retry_count += 1

                # Exponential backoff between retries
                if step.retry_count <= self.max_retries:
                    await asyncio.sleep(2 ** step.retry_count)

        # All techniques exhausted
        step.status = StepStatus.FAILED
        step.completed_at = datetime.now(timezone.utc).isoformat()
        return step.to_dict()

    def abort(self):
        """Request abort of chain execution."""
        self._abort_requested = True
        logger.info("Chain execution abort requested")

    def reset_abort(self):
        """Reset abort flag for new execution."""
        self._abort_requested = False


async def execute_chain(
    chain: AttackChain,
    dry_run: bool = False,
) -> ExecutionResult:
    """Convenience function to execute a chain."""
    executor = ChainExecutor()
    return await executor.execute_chain(chain, dry_run)


__all__ = [
    "TechniqueHandler",
    "DefaultTechniqueHandler",
    "ExecutionResult",
    "ChainExecutor",
    "execute_chain",
]
