"""Context bundle loader and converter for PydanticAI message history.

Spec source: vectl step spec-compliance-v2.context-bundle

Loads ContextBundle from file path, parses raw_tail,
converts to PydanticAI message_history format for AgentRunner.
"""

# @invar:allow file_size: Module focused on single responsibility (bundle loading)
# @shell_complexity: File I/O requires pathlib, json
# @invar:allow forbidden_import: Shell module needs pathlib for file operations
# @invar:allow shell_result: All functions return Result types

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

from pydantic import ValidationError
from returns.result import Failure, Result, Success

from anima.core.conversation import ContextBundle, RawTailEntry, SessionEvent
from anima.core.schemas import (
    AnimaError,
    InternalError,
    InvalidInputError,
    NotFoundError,
)


# @shell_complexity: File I/O with multiple error paths (not found, read error, parse error, validation error)
def load_context_bundle(bundle_path: str) -> Result[ContextBundle, AnimaError]:
    """Load ContextBundle from JSON file.

    Reads the JSON file at bundle_path and parses it as ContextBundle.
    Validates all fields according to ContextBundle schema.

    Args:
        bundle_path: Path to the ContextBundle JSON file.

    Returns:
        Success with ContextBundle, or Failure with:
            - NotFoundError if file does not exist
            - InvalidInputError if JSON is malformed or validation fails
            - InternalError if file read error

    Examples:
        >>> from pathlib import Path
        >>> import json
        >>> import tempfile
        >>> with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
        ...     _ = f.write(json.dumps({"bundle_id": "test_bundle"}))
        ...     path = f.name
        >>> result = load_context_bundle(path)
        >>> isinstance(result, Success)
        True
        >>> bundle = result.unwrap()
        >>> bundle.bundle_id
        'test_bundle'
        >>> Path(path).unlink()

        >>> # Non-existent file
        >>> result = load_context_bundle("/nonexistent/path.json")
        >>> isinstance(result, Failure)
        True
        >>> isinstance(result.failure(), NotFoundError)
        True
    """
    path = Path(bundle_path)

    if not path.exists():
        return Failure(NotFoundError(f"Context bundle file not found: {bundle_path}"))

    try:
        content = path.read_text()
    except OSError as e:
        return Failure(InternalError(f"Failed to read context bundle: {e}"))

    try:
        data = json.loads(content)
    except json.JSONDecodeError as e:
        return Failure(InvalidInputError(f"Invalid JSON in context bundle: {e}"))

    try:
        bundle = ContextBundle(**data)
    except ValidationError as e:
        return Failure(InvalidInputError(f"Context bundle validation failed: {e}"))

    return Success(bundle)


# @shell_complexity: Type conversion with multiple role branches (user/assistant/tool) and import handling
def convert_raw_tail_to_message_history(
    raw_tail: list[RawTailEntry],
) -> Result[list[Any], AnimaError]:
    """Convert RawTailEntry list to PydanticAI message_history format.

    Transforms anima's RawTailEntry format (internal) to PydanticAI's
    ModelRequest/ModelResponse format (external library).

    Conversion rules:
        - role="user" → ModelRequest(parts=[UserPromptPart(content=...)])
        - role="assistant" → ModelResponse(parts=[TextPart(content=...)])
        - role="tool" → skipped (tool responses handled by PydanticAI internally)

    Args:
        raw_tail: List of RawTailEntry objects from ContextBundle.

    Returns:
        Success with list of PydanticAI message objects, or Failure with:
            - InternalError if PydanticAI import failure or conversion error

    Examples:
        >>> entries = [
        ...     RawTailEntry(role="user", content="Hello"),
        ...     RawTailEntry(role="assistant", content="Hi there"),
        ... ]
        >>> result = convert_raw_tail_to_message_history(entries)
        >>> isinstance(result, Success)
        True
        >>> history = result.unwrap()
        >>> len(history)
        2
        >>> # First is ModelRequest with UserPromptPart
        >>> history[0].parts[0].content
        'Hello'
        >>> # Second is ModelResponse with TextPart
        >>> history[1].parts[0].content
        'Hi there'

        >>> # Tool messages are skipped
        >>> entries = [RawTailEntry(role="tool", content="result")]
        >>> result = convert_raw_tail_to_message_history(entries)
        >>> isinstance(result, Success)
        True
        >>> result.unwrap()
        []

        >>> # Invalid role (already rejected by RawTailEntry validation)
        >>> # RawTailEntry itself prevents invalid roles at construction time
    """
    from pydantic_ai.messages import ModelRequest, ModelResponse
    from pydantic_ai.messages import TextPart, UserPromptPart

    messages: list[Any] = []

    for entry in raw_tail:
        try:
            if entry.role == "user":
                # User message → ModelRequest with UserPromptPart
                msg = ModelRequest(parts=[UserPromptPart(content=entry.content)])
                messages.append(msg)
            elif entry.role == "assistant":
                # Assistant message → ModelResponse with TextPart
                # NOTE: speaker_id is logged but not used in PydanticAI format
                # (PydanticAI doesn't have a speaker_id concept)
                msg = ModelResponse(parts=[TextPart(content=entry.content)])
                messages.append(msg)
            elif entry.role == "tool":
                # Tool messages are internal and skipped
                # PydanticAI handles tool responses separately
                continue
            else:
                # This should never happen due to RawTailEntry validation
                # but handle defensively
                return Failure(
                    InternalError(f"Unexpected role in raw_tail: {entry.role}")
                )
        except Exception as e:
            return Failure(InternalError(f"Failed to convert message: {e}"))

    return Success(messages)


def load_message_history_from_context_bundle(
    context_bundle_ref: str | None,
) -> Result[list[Any], AnimaError]:
    """Load context bundle and convert to message_history for AgentRunner.

    This is the main entry point for AgentRunner: given a context_bundle_ref
    path (from JobSpec), load and convert to PydanticAI message_history format.

    If context_bundle_ref is None, returns empty list (no history).

    Args:
        context_bundle_ref: Path to context bundle JSON file, or None.

    Returns:
        Success with message_history list, or Failure with error.

    Examples:
        >>> import tempfile
        >>> import json
        >>> from pathlib import Path
        >>> # Test with None (no context)
        >>> result = load_message_history_from_context_bundle(None)
        >>> isinstance(result, Success)
        True
        >>> result.unwrap()
        []
        >>> # Test with valid bundle
        >>> bundle_data = {
        ...     "bundle_id": "test",
        ...     "raw_tail": [
        ...         {"role": "user", "content": "Hello"},
        ...         {"role": "assistant", "content": "Hi"},
        ...     ]
        ... }
        >>> with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
        ...     _ = f.write(json.dumps(bundle_data))
        ...     path = f.name
        >>> result = load_message_history_from_context_bundle(path)
        >>> isinstance(result, Success)
        True
        >>> history = result.unwrap()
        >>> len(history)
        2
        >>> Path(path).unlink()
    """
    if context_bundle_ref is None:
        return Success([])

    # Load the bundle
    bundle_result = load_context_bundle(context_bundle_ref)
    if isinstance(bundle_result, Failure):
        return bundle_result

    bundle = bundle_result.unwrap()

    # No raw_tail → empty history
    if bundle.raw_tail is None or len(bundle.raw_tail) == 0:
        return Success([])

    # Convert raw_tail to message_history
    return convert_raw_tail_to_message_history(bundle.raw_tail)


# @shell_complexity: Log parsing with multiple event types
# @invar:allow shell_result: Pure transformation, returns list directly
# @shell_orchestration: Log transformation for context bundle export
def extract_raw_tail_from_logs(log_entries: list[SessionEvent]) -> list[RawTailEntry]:
    """Extract RawTailEntry list from agent log entries.

    Reads agent log.jsonl and extracts conversation messages:
    - "interact_message" events → user messages
    - "response" events → assistant messages

    Events are ordered by timestamp to preserve conversation order.

    Args:
        log_entries: List of log entry dicts from read_log().

    Returns:
        List of RawTailEntry in chronological order.

    Examples:
        >>> logs = [
        ...     {"ts": "2026-01-01T00:00:00Z", "event": "interact_message", "details": {"message": "Hello"}},
        ...     {"ts": "2026-01-01T00:00:01Z", "event": "response", "details": {"response_text": "Hi"}},
        ... ]
        >>> entries = extract_raw_tail_from_logs(logs)
        >>> len(entries)
        2
        >>> entries[0].role
        'user'
        >>> entries[0].content
        'Hello'
        >>> entries[1].role
        'assistant'
        >>> entries[1].content
        'Hi'

        >>> # Non-relevant events are skipped
        >>> logs = [
        ...     {"ts": "2026-01-01T00:00:00Z", "event": "created", "details": {}},
        ...     {"ts": "2026-01-01T00:00:01Z", "event": "interact_message", "details": {"message": "Test"}},
        ... ]
        >>> entries = extract_raw_tail_from_logs(logs)
        >>> len(entries)
        1
        >>> entries[0].content
        'Test'
    """
    entries: list[RawTailEntry] = []

    for entry in log_entries:
        event = entry.get("event")
        details_obj = entry.get("details", {})
        details = details_obj if isinstance(details_obj, dict) else {}

        if event == "interact_message":
            # User message from interact
            message = details.get("message", "")
            if message:
                entries.append(RawTailEntry(role="user", content=message))

        elif event == "response":
            # Assistant response
            response_text = details.get("response_text", "")
            if response_text:
                entries.append(RawTailEntry(role="assistant", content=response_text))

    return entries


# @shell_complexity: ContextBundle creation and file I/O
def export_context_bundle(
    bundle_id: str,
    inherited_from: str,
    raw_tail: list[RawTailEntry],
    output_path: Path,
) -> Result[None, AnimaError]:
    """Export ContextBundle to JSON file for child agent.

    Creates a ContextBundle with the provided raw_tail (conversation history)
    and writes it to the specified path.

    Args:
        bundle_id: Unique identifier for this bundle (e.g., "ctx_xxx").
        inherited_from: Parent instance_id who provided this context.
        raw_tail: List of conversation entries to include.
        output_path: Path to write the ContextBundle JSON file.

    Returns:
        Success(None) or Failure(InternalError) on I/O error.

    Examples:
        >>> import tempfile
        >>> from pathlib import Path
        >>> entries = [RawTailEntry(role="user", content="Hello")]
        >>> tmpdir = tempfile.mkdtemp()
        >>> output = Path(tmpdir) / "context_bundle.json"
        >>> result = export_context_bundle("ctx_test", "parent_123", entries, output)
        >>> isinstance(result, Success)
        True
        >>> output.exists()
        True
        >>> import json
        >>> data = json.loads(output.read_text())
        >>> data["bundle_id"]
        'ctx_test'
        >>> data["inherited_from"]
        'parent_123'
    """
    bundle = ContextBundle(
        bundle_id=bundle_id,
        inherited_from=inherited_from,
        raw_tail=raw_tail if raw_tail else None,
    )

    try:
        # Ensure parent directory exists
        output_path.parent.mkdir(parents=True, exist_ok=True)

        # Atomic write via temp file
        temp_path = output_path.with_suffix(".tmp")
        temp_path.write_text(bundle.model_dump_json(indent=2))
        temp_path.rename(output_path)

        return Success(None)
    except OSError as e:
        return Failure(InternalError(f"Failed to export context bundle: {e}"))
