from concurrent.futures import Future
import logging
from pathlib import Path
import re
import shutil
import subprocess
import sys
import time
from typing import (
    TYPE_CHECKING,
    Dict,
    List,
    Optional,
    Tuple,
    Union,
)
import uuid

from safety.utils.pyapp_utils import get_path, get_env

from safety_schemas.models.events import Event, EventType
from safety_schemas.models.events.types import ToolType
from safety_schemas.models.events.payloads import (
    CodebaseDetectionStatusPayload,
    CodebaseSetupCompletedPayload,
    CodebaseSetupResponseCreatedPayload,
    DependencyFile,
    FirewallConfiguredPayload,
    FirewallDisabledPayload,
    FirewallSetupCompletedPayload,
    FirewallSetupResponseCreatedPayload,
    InitExitStep,
    InitExitedPayload,
    InitScanCompletedPayload,
    PackageInstalledPayload,
    PackageUninstalledPayload,
    PackageUpdatedPayload,
    CommandExecutedPayload,
    ToolCommandExecutedPayload,
    CommandErrorPayload,
    AliasConfig,
    IndexConfig,
    ToolStatus,
    CommandParam,
    ProcessStatus,
    FirewallHeartbeatPayload,
    InitStartedPayload,
    AuthStartedPayload,
    AuthCompletedPayload,
)
import typer

from ..event_bus import EventBus
from ..types.base import InternalEventType, InternalPayload

from .creation import (
    create_event,
)
from .data import (
    clean_parameter,
    get_command_path,
    get_root_context,
    scrub_sensitive_value,
    translate_param_source,
)
from .conditions import conditional_emitter, should_emit_firewall_heartbeat


if TYPE_CHECKING:
    from safety.models import SafetyCLI, ToolResult
    from safety.cli_util import CustomContext
    from safety.init.types import FirewallConfigStatus
    from safety.tool.environment_diff import PackageLocation

logger = logging.getLogger(__name__)


@conditional_emitter
def send_and_flush(event_bus: "EventBus", event: Event) -> Optional[Future]:
    """
    Emit an event and immediately flush the event bus without closing it.

    Args:
        event_bus: The event bus to emit on
        event: The event to emit
    """
    future = event_bus.emit(event)

    # Create and emit flush event
    flush_payload = InternalPayload()
    flush_event = create_event(
        payload=flush_payload, event_type=InternalEventType.FLUSH_SECURITY_TRACES
    )

    # Emit flush event and wait for it to complete
    flush_future = event_bus.emit(flush_event)

    # Wait for both events to complete
    if future:
        try:
            future.result(timeout=0.5)
        except Exception:
            logger.error("Emit Failed %s (%s)", event.type, event.id)

    if flush_future:
        try:
            return flush_future.result(timeout=0.5)
        except Exception:
            logger.error("Flush Failed for event %s", event.id)

    return None


@conditional_emitter(conditions=[should_emit_firewall_heartbeat])
def emit_firewall_heartbeat(
    event_bus: "EventBus", ctx: Optional["CustomContext"], *, tools: List[ToolStatus]
):
    payload = FirewallHeartbeatPayload(tools=tools)
    event = create_event(payload=payload, event_type=EventType.FIREWALL_HEARTBEAT)

    event_bus.emit(event)


@conditional_emitter
def emit_firewall_disabled(
    event_bus: "EventBus",
    ctx: Optional["CustomContext"] = None,
    *,
    reason: Optional[str],
):
    payload = FirewallDisabledPayload(reason=reason)
    event = create_event(payload=payload, event_type=EventType.FIREWALL_DISABLED)

    event_bus.emit(event)


def status_to_tool_status(status: "FirewallConfigStatus") -> List[ToolStatus]:
    filtered_path = get_path()
    tools = []
    for tool_type, configs in status.items():
        alias_config = (
            configs["alias"] if isinstance(configs["alias"], AliasConfig) else None
        )
        index_config = (
            configs["index"] if isinstance(configs["index"], IndexConfig) else None
        )

        tool = tool_type.value
        command_path = shutil.which(tool, path=filtered_path)
        reachable = False
        version = "unknown"

        if command_path:
            args = [command_path, "--version"]
            result = subprocess.run(args, capture_output=True, text=True, env=get_env())

            if result.returncode == 0:
                output = result.stdout
                reachable = True

                # Extract version
                version_match = re.search(r"(\d+\.\d+(?:\.\d+)?)", output)
                if version_match:
                    version = version_match.group(1)
        else:
            command_path = tool

        tool = ToolStatus(
            type=tool_type,
            command_path=command_path,
            version=version,
            reachable=reachable,
            alias_config=alias_config,
            index_config=index_config,
        )
        tools.append(tool)

    return tools


@conditional_emitter
def emit_firewall_configured(
    event_bus: "EventBus",
    ctx: Optional["CustomContext"] = None,
    *,
    status: "FirewallConfigStatus",
):
    tools = status_to_tool_status(status)

    payload = FirewallConfiguredPayload(tools=tools)

    event = create_event(payload=payload, event_type=EventType.FIREWALL_CONFIGURED)

    event_bus.emit(event)


@conditional_emitter
def emit_diff_operations(
    event_bus: "EventBus",
    ctx: "CustomContext",
    *,
    added: Dict["PackageLocation", str],
    removed: Dict["PackageLocation", str],
    updated: Dict["PackageLocation", Tuple[str, str]],
    tool_path: Optional[str],
    by_tool: ToolType,
):
    obj: "SafetyCLI" = ctx.obj
    correlation_id = obj.correlation_id

    kwargs = {
        "tool_path": tool_path,
        "tool": by_tool,
    }

    if (added or removed or updated) and not correlation_id:
        correlation_id = obj.correlation_id = str(uuid.uuid4())

    def emit_package_event(event_bus, correlation_id, payload, event_type):
        event = create_event(
            payload=payload,
            event_type=event_type,
            correlation_id=correlation_id,
        )
        event_bus.emit(event)

    for package, version in added.items():
        emit_package_event(
            event_bus,
            correlation_id,
            PackageInstalledPayload(
                package_name=package.name,
                location=package.location,
                version=version,
                **kwargs,
            ),
            EventType.PACKAGE_INSTALLED,
        )

    for package, version in removed.items():
        emit_package_event(
            event_bus,
            correlation_id,
            PackageUninstalledPayload(
                package_name=package.name,
                location=package.location,
                version=version,
                **kwargs,
            ),
            EventType.PACKAGE_UNINSTALLED,
        )

    for package, (previous_version, current_version) in updated.items():
        emit_package_event(
            event_bus,
            correlation_id,
            PackageUpdatedPayload(
                package_name=package.name,
                location=package.location,
                previous_version=previous_version,
                current_version=current_version,
                **kwargs,
            ),
            EventType.PACKAGE_UPDATED,
        )


@conditional_emitter
def emit_tool_command_executed(
    event_bus: "EventBus", ctx: "CustomContext", *, tool: ToolType, result: "ToolResult"
) -> None:
    correlation_id = ctx.obj.correlation_id

    if not correlation_id:
        correlation_id = ctx.obj.correlation_id = str(uuid.uuid4())

    process = result.process

    payload = ToolCommandExecutedPayload(
        tool=tool,
        tool_path=result.tool_path,
        raw_command=[clean_parameter("", arg) for arg in process.args],
        duration_ms=result.duration_ms,
        status=ProcessStatus(
            stdout=process.stdout, stderr=process.stderr, return_code=process.returncode
        ),
    )

    # Scrub after binary coercion to str
    if payload.status.stdout:
        payload.status.stdout = scrub_sensitive_value(payload.status.stdout)
    if payload.status.stderr:
        payload.status.stderr = scrub_sensitive_value(payload.status.stderr)

    event = create_event(
        correlation_id=correlation_id,
        payload=payload,
        event_type=EventType.TOOL_COMMAND_EXECUTED,
    )

    event_bus.emit(event)


@conditional_emitter
def emit_command_executed(
    event_bus: "EventBus", ctx: "CustomContext", *, returned_code: int
) -> None:
    root_context = get_root_context(ctx)
    NA = ""

    started_at = getattr(root_context, "started_at", None) if root_context else None
    if started_at is not None:
        duration_ms = int((time.monotonic() - started_at) * 1000)
    else:
        duration_ms = 1

    command_name = ctx.command.name if ctx.command.name is not None else NA
    raw_command = [clean_parameter("", arg) for arg in sys.argv]

    params: List[CommandParam] = []

    for idx, param in enumerate(ctx.command.params):
        param_name = param.name if param.name is not None else NA
        param_value = ctx.params.get(param_name)

        # Scrub the parameter value if sensitive
        scrubbed_value = clean_parameter(param_name, param_value)

        # Determine parameter source using Click's API
        click_source = ctx.get_parameter_source(param_name)
        source = translate_param_source(click_source)

        display_name = param_name if param_name else None

        params.append(
            CommandParam(
                position=idx, name=display_name, value=scrubbed_value, source=source
            )
        )

    payload = CommandExecutedPayload(
        command_name=command_name,
        command_path=get_command_path(ctx),
        raw_command=raw_command,
        parameters=params,
        duration_ms=duration_ms,
        status=ProcessStatus(
            return_code=returned_code,
        ),
    )

    event = create_event(
        correlation_id=ctx.obj.correlation_id,
        payload=payload,
        event_type=EventType.COMMAND_EXECUTED,
    )

    try:
        if future := event_bus.emit(event):
            future.result(timeout=0.5)
    except Exception:
        logger.error("Emit Failed %s (%s)", event.type, event.id)


@conditional_emitter
def emit_command_error(
    event_bus: "EventBus",
    ctx: "CustomContext",
    *,
    message: str,
    traceback: Optional[str] = None,
) -> None:
    """
    Emit a CommandErrorEvent with sensitive data scrubbed.
    """
    # Get command name from context if available
    command_name = getattr(ctx, "command", None)
    if command_name and command_name.name:
        command_name = command_name.name

    scrub_traceback = None
    if traceback:
        scrub_traceback = scrub_sensitive_value(traceback)

    command_path = get_command_path(ctx)
    raw_command = [scrub_sensitive_value(arg) for arg in sys.argv]

    payload = CommandErrorPayload(
        command_name=command_name,
        raw_command=raw_command,
        command_path=command_path,
        error_message=scrub_sensitive_value(message),
        stacktrace=scrub_traceback,
    )

    event = create_event(
        payload=payload,
        event_type=EventType.COMMAND_ERROR,
    )

    event_bus.emit(event)


def emit_init_started(
    event_bus: "EventBus", ctx: Union["CustomContext", typer.Context]
) -> None:
    """
    Emit an InitStartedEvent and store it as a pending event in SafetyCLI object.

    Args:
        event_bus: The event bus to emit on
        ctx: The Click context containing the SafetyCLI object
    """
    obj: "SafetyCLI" = ctx.obj

    if not obj.correlation_id:
        obj.correlation_id = str(uuid.uuid4())

    payload = InitStartedPayload()
    event = create_event(
        correlation_id=obj.correlation_id,
        payload=payload,
        event_type=EventType.INIT_STARTED,
    )

    if not send_and_flush(event_bus, event):
        # Store as pending event
        obj.pending_events.append(event)


def emit_auth_started(event_bus: "EventBus", ctx: "CustomContext") -> None:
    """
    Emit an AuthStartedEvent and store it as a pending event in SafetyCLI object.

    Args:
        event_bus: The event bus to emit on
        ctx: The Click context containing the SafetyCLI object
    """
    obj: "SafetyCLI" = ctx.obj

    if not obj.correlation_id:
        obj.correlation_id = str(uuid.uuid4())

    payload = AuthStartedPayload()
    event = create_event(
        correlation_id=obj.correlation_id,
        payload=payload,
        event_type=EventType.AUTH_STARTED,
    )

    if not send_and_flush(event_bus, event):
        # Store as pending event
        obj.pending_events.append(event)


@conditional_emitter
def emit_auth_completed(
    event_bus: "EventBus",
    ctx: "CustomContext",
    *,
    success: bool = True,
    error_message: Optional[str] = None,
) -> None:
    """
    Emit an AuthCompletedEvent and submit all pending events together.

    Args:
        event_bus: The event bus to emit on
        ctx: The Click context containing the SafetyCLI object
        success: Whether authentication was successful
        error_message: Optional error message if authentication failed
    """
    obj: "SafetyCLI" = ctx.obj

    if not obj.correlation_id:
        obj.correlation_id = str(uuid.uuid4())

    payload = AuthCompletedPayload(success=success, error_message=error_message)

    event = create_event(
        correlation_id=obj.correlation_id,
        payload=payload,
        event_type=EventType.AUTH_COMPLETED,
    )

    for pending_event in obj.pending_events:
        event_bus.emit(pending_event)

    obj.pending_events.clear()

    # Emit auth completed event and flush
    send_and_flush(event_bus, event)


@conditional_emitter
def emit_firewall_setup_response_created(
    event_bus: "EventBus",
    ctx: Union["CustomContext", typer.Context],
    *,
    user_consent_requested: bool,
    user_consent: Optional[bool] = None,
) -> None:
    obj: "SafetyCLI" = ctx.obj

    if not obj.correlation_id:
        obj.correlation_id = str(uuid.uuid4())

    payload = FirewallSetupResponseCreatedPayload(
        user_consent_requested=user_consent_requested, user_consent=user_consent
    )

    event = create_event(
        correlation_id=obj.correlation_id,
        payload=payload,
        event_type=EventType.FIREWALL_SETUP_RESPONSE_CREATED,
    )

    # Emit and flush
    send_and_flush(event_bus, event)


@conditional_emitter
def emit_codebase_setup_response_created(
    event_bus: "EventBus",
    ctx: Union["CustomContext", typer.Context],
    *,
    user_consent_requested: bool,
    user_consent: Optional[bool] = None,
) -> None:
    obj: "SafetyCLI" = ctx.obj

    if not obj.correlation_id:
        obj.correlation_id = str(uuid.uuid4())

    payload = CodebaseSetupResponseCreatedPayload(
        user_consent_requested=user_consent_requested, user_consent=user_consent
    )

    event = create_event(
        correlation_id=obj.correlation_id,
        payload=payload,
        event_type=EventType.CODEBASE_SETUP_RESPONSE_CREATED,
    )

    # Emit and flush
    send_and_flush(event_bus, event)


@conditional_emitter
def emit_codebase_detection_status(
    event_bus: "EventBus",
    ctx: Union["CustomContext", typer.Context],
    *,
    detected: bool,
    detected_files: Optional[List[Path]] = None,
) -> None:
    obj: "SafetyCLI" = ctx.obj

    if not obj.correlation_id:
        obj.correlation_id = str(uuid.uuid4())

    payload = CodebaseDetectionStatusPayload(
        detected=detected,
        dependency_files=[
            DependencyFile(file_path=str(file)) for file in detected_files
        ]
        if detected_files
        else None,
    )

    event = create_event(
        correlation_id=obj.correlation_id,
        payload=payload,
        event_type=EventType.CODEBASE_DETECTION_STATUS,
    )

    # Emit and flush
    send_and_flush(event_bus, event)


@conditional_emitter
def emit_init_scan_completed(
    event_bus: "EventBus",
    ctx: Union["CustomContext", typer.Context],
    *,
    scan_id: Optional[str],
) -> None:
    obj: "SafetyCLI" = ctx.obj

    if not obj.correlation_id:
        obj.correlation_id = str(uuid.uuid4())

    payload = InitScanCompletedPayload(scan_id=scan_id)

    event = create_event(
        correlation_id=obj.correlation_id,
        payload=payload,
        event_type=EventType.INIT_SCAN_COMPLETED,
    )

    # Emit and flush
    send_and_flush(event_bus, event)


@conditional_emitter
def emit_codebase_setup_completed(
    event_bus: "EventBus",
    ctx: Union["CustomContext", typer.Context],
    *,
    is_created: bool,
    codebase_id: Optional[str] = None,
) -> None:
    obj: "SafetyCLI" = ctx.obj

    if not obj.correlation_id:
        obj.correlation_id = str(uuid.uuid4())

    payload = CodebaseSetupCompletedPayload(
        is_created=is_created, codebase_id=codebase_id
    )

    event = create_event(
        correlation_id=obj.correlation_id,
        payload=payload,
        event_type=EventType.CODEBASE_SETUP_COMPLETED,
    )

    # Emit and flush
    send_and_flush(event_bus, event)


@conditional_emitter
def emit_firewall_setup_completed(
    event_bus: "EventBus",
    ctx: "CustomContext",
    *,
    status: "FirewallConfigStatus",
) -> None:
    obj: "SafetyCLI" = ctx.obj

    if not obj.correlation_id:
        obj.correlation_id = str(uuid.uuid4())

    tools = status_to_tool_status(status)

    payload = FirewallSetupCompletedPayload(
        tools=tools,
    )

    event = create_event(
        correlation_id=obj.correlation_id,
        payload=payload,
        event_type=EventType.FIREWALL_SETUP_COMPLETED,
    )

    # Emit and flush
    send_and_flush(event_bus, event)


@conditional_emitter
def emit_init_exited(
    event_bus: "EventBus",
    ctx: Union["CustomContext", typer.Context],
    *,
    exit_step: InitExitStep,
) -> None:
    obj: "SafetyCLI" = ctx.obj

    if not obj.correlation_id:
        obj.correlation_id = str(uuid.uuid4())

    payload = InitExitedPayload(exit_step=exit_step)

    event = create_event(
        correlation_id=obj.correlation_id,
        payload=payload,
        event_type=EventType.INIT_EXITED,
    )

    # Emit and flush
    send_and_flush(event_bus, event)
