from collections.abc import Callable
from enum import Enum

from backend.services.helpers.attachment_history_utils import history_has_snapshots
from backend.utils.logging_utils import get_logger

logger = get_logger(__name__)


class AttachmentForwardingPolicy(str, Enum):
    # Keep the original attachment representation. For screenshot-based documents this means
    # snapshots/images can still be forwarded to the target.
    FORWARD_TEXT_AND_SNAPSHOTS = "FORWARD_TEXT_AND_SNAPSHOTS"
    # Only forward extracted text before the completion is built.
    FORWARD_TEXT_ONLY = "FORWARD_TEXT_ONLY"
    # We would need to remove images, but no extracted-text fallback is available.
    CANT_FORWARD = "CANT_FORWARD"


def is_llm_multimodal(project, llm_id: str) -> bool:
    """
    Multimodal support is resolved using DSS canonical capability filtering:
    we call project.list_llms(purpose="IMAGE_INPUT") and consider the target
    multimodal only if its id is returned.
    DSS builds this list by applying IMAGE_INPUT eligibility to both provider
    LLMs (model capabilities) and Agents (their computed image-support
    settings/modes), so we rely on DSS instead of re-implementing that logic
    """
    if not llm_id:
        return False
    try:
        vision_ids = set()
        for llm in project.list_llms(purpose="IMAGE_INPUT", as_type="listitems"):
            if isinstance(llm, dict):
                lid = llm.get("id")
            else:
                lid = getattr(llm, "id", None)
            if lid:
                vision_ids.add(lid)
        return llm_id in vision_ids
    except Exception:
        logger.debug("Unable to list IMAGE_INPUT llms for multimodal check", exc_info=True)
        return False


def resolve_attachment_forwarding_policy(*, multimodal: bool, has_text_fallback: bool) -> AttachmentForwardingPolicy:
    """
    Return the forwarding policy for one target when snapshot/image attachments are present.

    Args:
        multimodal: Whether the target model/agent can consume image inputs directly.
        has_text_fallback: Whether extracted text is available as a fallback for snapshot-based documents.

    The decision matrix is deliberately simple:
    - multimodal target -> keep the original payload
    - non-multimodal target with extracted text available -> switch to text-only
    - non-multimodal target without extracted text -> block
    """
    if multimodal:
        return AttachmentForwardingPolicy.FORWARD_TEXT_AND_SNAPSHOTS
    if has_text_fallback:
        return AttachmentForwardingPolicy.FORWARD_TEXT_ONLY
    return AttachmentForwardingPolicy.CANT_FORWARD


def is_text_fallback_generation_required(
    *,
    sel_agents: list[dict],
    history: list[dict],
    base_llm_id: str,
    is_base_llm_multimodal: Callable[[], bool],
    is_target_multimodal: Callable[[str], bool],
) -> bool:
    """
    Return True when text fallback generation is required for the current turn.

    Generation is required only when history includes snapshots and at least one target
    is non-multimodal (base-llm mode or selected agents mode).
    """
    if not history_has_snapshots(history):
        logger.info("Skip fallback generation: no snapshots in history")
        return False

    # No agents to route to (LLM-direct mode, or agents mode with none selected/available):
    # check multimodality against the base LLM instead of agent targets.
    if not sel_agents:
        base_is_multimodal = is_base_llm_multimodal()
        logger.info(
            "Evaluate base llm fallback generation target=%s multimodal=%s",
            base_llm_id,
            base_is_multimodal,
        )
        # Base LLM can consume images, so generating extracted text would only add latency.
        if base_is_multimodal:
            logger.info("Skip fallback generation: base llm is multimodal")
            return False
        # Base LLM is not multimodal: materialize text fallbacks before routing.
        logger.info("Fallback generation required for non-multimodal base llm target=%s", base_llm_id)
        return True

    non_multimodal_targets: list[str] = []
    for sel in sel_agents:
        target_id = sel.get("agentId")
        if not target_id:
            continue
        # We collect all non-multimodal targets first so logs clearly show which targets forced
        # fallback generation for this turn.
        if not is_target_multimodal(target_id):
            non_multimodal_targets.append(target_id)

    # All selected targets are multimodal, so we keep the fast path and avoid extraction.
    if not non_multimodal_targets:
        logger.info("Skip fallback generation: all selected targets are multimodal")
        return False

    # At least one target cannot receive images. Materialize extracted text now,
    # for screenshot-based documents, after which the normal routing phase can decide between
    # FORWARD_TEXT_AND_SNAPSHOTS / FORWARD_TEXT_ONLY / CANT_FORWARD.
    logger.info("Fallback generation required for non-multimodal targets=%s", non_multimodal_targets)
    return True
