"""
cortex/core/injection_slots.py — Injection Slot Manager (Issue #363).

Manages the assignment of compiled context bundles to specific injection
positions within the LLM prompt. Each bundle type maps to a deterministic
slot position based on its role in the conversation.

Slot Positions:
    SYSTEM_PREFIX  — boot context (identity, continuity)
    SYSTEM_SUFFIX  — open_loops, task, procedures (work context)
    USER_PREFIX    — recent_history, defaults (conversational context)
    USER_SUFFIX    — evidence (supporting details)

Architecture:
    InjectionSlotManager receives a CompiledContext from ContextCompiler,
    assigns each bundle to its designated slot position, renders the bundles
    into injectable text, and returns a dict of SlotPosition → rendered text.

Dependencies:
    - cortex.core.context_compiler (CompiledContext, BundleType, Bundle)

Dependents:
    - cortex.api.plugin.CortexPlugin (consumes rendered slots for injection)
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional

from cortex.core.context_compiler import Bundle, BundleType, CompiledContext

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Slot Positions
# ---------------------------------------------------------------------------

class SlotPosition(str, Enum):
    """Injection positions within the LLM prompt.

    Ordered from earliest to latest in the prompt:
        SYSTEM_PREFIX → SYSTEM_SUFFIX → USER_PREFIX → USER_SUFFIX
    """

    SYSTEM_PREFIX = "system_prefix"
    SYSTEM_SUFFIX = "system_suffix"
    USER_PREFIX = "user_prefix"
    USER_SUFFIX = "user_suffix"

    @property
    def order(self) -> int:
        """Ordering index for deterministic slot rendering."""
        _order = {
            SlotPosition.SYSTEM_PREFIX: 0,
            SlotPosition.SYSTEM_SUFFIX: 1,
            SlotPosition.USER_PREFIX: 2,
            SlotPosition.USER_SUFFIX: 3,
        }
        return _order[self]

    @classmethod
    def by_order(cls) -> List[SlotPosition]:
        """Return positions in prompt order."""
        return sorted(cls, key=lambda s: s.order)


# ---------------------------------------------------------------------------
# Bundle → Slot mapping
# ---------------------------------------------------------------------------

# Deterministic mapping from BundleType to SlotPosition.
BUNDLE_SLOT_MAP: Dict[BundleType, SlotPosition] = {
    BundleType.BOOT: SlotPosition.SYSTEM_PREFIX,
    BundleType.OPEN_LOOPS: SlotPosition.SYSTEM_SUFFIX,
    BundleType.TASK: SlotPosition.SYSTEM_SUFFIX,
    BundleType.PROCEDURES: SlotPosition.SYSTEM_SUFFIX,
    BundleType.RECENT_HISTORY: SlotPosition.USER_PREFIX,
    BundleType.DEFAULTS: SlotPosition.USER_PREFIX,
    BundleType.EVIDENCE: SlotPosition.USER_SUFFIX,
}


# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------

@dataclass
class InjectionAssignment:
    """A bundle assigned to a specific injection slot.

    Attributes:
        slot_position: Where in the prompt this bundle should be injected.
        bundle: The compiled bundle being injected.
        rendered_text: The formatted text ready for injection.
        token_count: Token count of the rendered text.
    """

    slot_position: SlotPosition
    bundle: Bundle
    rendered_text: str
    token_count: int


# ---------------------------------------------------------------------------
# Config duck-type
# ---------------------------------------------------------------------------

class _ConfigProtocol:
    """Structural type expected from CortexConfig for injection slots."""
    context_compiler_enabled: bool


# ---------------------------------------------------------------------------
# Injection Slot Manager
# ---------------------------------------------------------------------------

class InjectionSlotManager:
    """Assign compiled bundles to injection slots and render injectable text.

    The manager takes a CompiledContext from ContextCompiler, maps each bundle
    to its designated slot position, formats the bundle content into injectable
    text blocks, and produces a dict of SlotPosition → text for the plugin
    to inject into the prompt.

    Args:
        config: CortexConfig (optional, uses context_compiler.* fields).
    """

    # Section header templates for bundle types
    _SECTION_HEADERS: Dict[BundleType, str] = {
        BundleType.BOOT: "## Session Context",
        BundleType.OPEN_LOOPS: "## Open Loops",
        BundleType.TASK: "## Task Context",
        BundleType.PROCEDURES: "## Procedures",
        BundleType.RECENT_HISTORY: "## Recent History",
        BundleType.DEFAULTS: "## Known Facts & Preferences",
        BundleType.EVIDENCE: "## Supporting Evidence",
    }

    def __init__(self, config: Optional[_ConfigProtocol] = None) -> None:
        self._enabled = True
        if config is not None and hasattr(config, "context_compiler_enabled"):
            self._enabled = config.context_compiler_enabled

    def assign_slots(
        self,
        compiled: CompiledContext,
    ) -> List[InjectionAssignment]:
        """Assign each bundle in compiled context to its injection slot.

        Args:
            compiled: CompiledContext from ContextCompiler.

        Returns:
            List of InjectionAssignment objects, ordered by slot position
            then bundle priority. Empty bundles are excluded.
        """
        assignments: List[InjectionAssignment] = []

        for bundle in compiled.bundles:
            if not bundle.items or bundle.token_count <= 0:
                continue

            slot = BUNDLE_SLOT_MAP.get(bundle.bundle_type)
            if slot is None:
                logger.warning(
                    "No slot mapping for bundle type %s — skipping",
                    bundle.bundle_type.value,
                )
                continue

            rendered = self._render_bundle(bundle)
            if not rendered:
                continue

            assignments.append(InjectionAssignment(
                slot_position=slot,
                bundle=bundle,
                rendered_text=rendered,
                token_count=bundle.token_count,
            ))

        # Sort by slot order, then by bundle priority within each slot
        assignments.sort(
            key=lambda a: (a.slot_position.order, a.bundle.bundle_type.priority)
        )

        return assignments

    def render(
        self,
        assignments: List[InjectionAssignment],
    ) -> Dict[SlotPosition, str]:
        """Render assignments into a dict of slot position → injectable text.

        Multiple bundles assigned to the same slot are concatenated with
        double newlines between sections.

        Args:
            assignments: List of InjectionAssignment from assign_slots().

        Returns:
            Dict mapping SlotPosition → rendered text. Only positions with
            content are included. Positions are in deterministic prompt order.
        """
        slot_texts: Dict[SlotPosition, List[str]] = {}

        for assignment in assignments:
            if not assignment.rendered_text:
                continue
            if assignment.slot_position not in slot_texts:
                slot_texts[assignment.slot_position] = []
            slot_texts[assignment.slot_position].append(assignment.rendered_text)

        result: Dict[SlotPosition, str] = {}
        for position in SlotPosition.by_order():
            if position in slot_texts:
                result[position] = "\n\n".join(slot_texts[position])

        return result

    # ------------------------------------------------------------------
    # Rendering helpers
    # ------------------------------------------------------------------

    def _render_bundle(self, bundle: Bundle) -> str:
        """Render a single bundle into formatted text.

        Format:
            ## [Section Header]
            - item content 1
            - item content 2
        """
        if not bundle.items:
            return ""

        header = self._SECTION_HEADERS.get(
            bundle.bundle_type,
            f"## {bundle.bundle_type.value.replace('_', ' ').title()}",
        )

        lines = [header]
        for item in bundle.items:
            content = item.get("content", "")
            if content:
                lines.append(f"- {content}")

        if len(lines) <= 1:
            return ""

        return "\n".join(lines)
