"""Block-based message parsing helpers for rendered prompt bodies."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Literal

from dotpromptz.errors import MessageBlockParseError
from dotpromptz.image import infer_image_mime, resolve_image_value
from dotpromptz.logging import get_logger
from dotpromptz.typing import MediaContent, MediaPart, Message, Part, Role, TextPart

logger = get_logger(__name__)

_BLOCK_PREFIX = ':::'


class BlockParseError(ValueError):
    """Raised when block syntax is invalid."""

    def __init__(self, *, code: str, message: str, lineno: int, raw_line: str | None = None) -> None:
        """Build a structured block parsing error.

        Args:
            code: Stable error code.
            message: Human-readable message.
            lineno: 1-based line number where the error originates.
            raw_line: Optional original source line.
        """
        super().__init__(f'[{code}] line {lineno}: {message}')
        self.code = code
        self.lineno = lineno
        self.raw_line = raw_line


@dataclass(frozen=True)
class BlockToken:
    """A parsed ``:::`` block token."""

    kind: Literal['role', 'image']
    lineno: int
    raw_line: str
    role: Role | None = None


def is_block_line(line: str) -> bool:
    """Return whether *line* is a top-level block line."""
    return line.startswith(_BLOCK_PREFIX)


def parse_block_line(line: str, lineno: int) -> BlockToken | None:
    """Parse a single ``:::`` block line.

    Args:
        line: Source line.
        lineno: 1-based source line number.

    Returns:
        A parsed ``BlockToken`` when *line* is a block line, otherwise ``None``.

    Raises:
        BlockParseError: If *line* starts with ``:::`` but has invalid syntax.
    """
    stripped = line.rstrip('\r\n')
    if not is_block_line(stripped):
        return None

    rest = stripped[len(_BLOCK_PREFIX) :]
    parts = rest.split()
    if not parts:
        raise BlockParseError(
            code='block_bad_format',
            message='Empty block directive.',
            lineno=lineno,
            raw_line=stripped,
        )

    head = parts[0]
    if len(parts) > 1:
        if head in {'system', 'user', 'model', 'image'}:
            raise BlockParseError(
                code='block_bad_format',
                message='Block directives do not accept inline arguments.',
                lineno=lineno,
                raw_line=stripped,
            )
        raise BlockParseError(
            code='block_unknown',
            message=f'Unknown block directive: {head!r}.',
            lineno=lineno,
            raw_line=stripped,
        )

    if head == 'system':
        return BlockToken(kind='role', lineno=lineno, raw_line=stripped, role=Role.SYSTEM)
    if head == 'user':
        return BlockToken(kind='role', lineno=lineno, raw_line=stripped, role=Role.USER)
    if head == 'model':
        return BlockToken(kind='role', lineno=lineno, raw_line=stripped, role=Role.MODEL)
    if head == 'image':
        return BlockToken(kind='image', lineno=lineno, raw_line=stripped)

    raise BlockParseError(
        code='block_unknown',
        message=f'Unknown block directive: {head!r}.',
        lineno=lineno,
        raw_line=stripped,
    )


def build_image_part(value: str, mime_type: str) -> MediaPart:
    """Build a ``MediaPart`` from resolved image value and MIME type."""
    return MediaPart(media=MediaContent(url=value, content_type=mime_type))


def _to_message_block_error(exc: BlockParseError) -> MessageBlockParseError:
    """Convert block parsing errors to domain message block errors."""
    return MessageBlockParseError(
        str(exc),
        code=exc.code,
        lineno=exc.lineno,
        raw_line=exc.raw_line,
    )


def parse_blocks_to_messages(rendered_string: str) -> list[Message]:
    """Parse rendered text into messages using ``:::`` blocks."""
    if not rendered_string.strip():
        return []

    lines = rendered_string.splitlines(keepends=True)
    has_block = any(is_block_line(line.rstrip('\r\n')) for line in lines)
    if not has_block:
        return [Message(role=Role.USER, content=[TextPart(text=rendered_string)])]

    messages: list[Message] = []
    current_role: Role | None = None
    current_parts: list[Part] = []
    text_chunks: list[tuple[int, str]] = []

    def _flush_text() -> None:
        nonlocal text_chunks
        if not text_chunks:
            return

        text = ''.join(chunk for _, chunk in text_chunks)
        if current_role is None:
            first_non_empty = next(((ln, raw) for ln, raw in text_chunks if raw.strip()), None)
            if first_non_empty is not None:
                ln, raw = first_non_empty
                raise _to_message_block_error(
                    BlockParseError(
                        code='block_leading_text',
                        message='Text before the first block is not allowed when block syntax is used.',
                        lineno=ln,
                        raw_line=raw.rstrip('\r\n'),
                    )
                )
            text_chunks = []
            return

        current_parts.append(TextPart(text=text))
        text_chunks = []

    def _flush_message() -> None:
        nonlocal current_parts
        _flush_text()
        if current_role is not None and current_parts:
            messages.append(Message(role=current_role, content=current_parts))
            current_parts = []

    idx = 0
    while idx < len(lines):
        raw_line = lines[idx]
        token: BlockToken | None
        try:
            token = parse_block_line(raw_line.rstrip('\r\n'), idx + 1)
        except BlockParseError as exc:
            raise _to_message_block_error(exc) from exc

        if token is None:
            text_chunks.append((idx + 1, raw_line))
            idx += 1
            continue

        if token.kind == 'role':
            _flush_message()
            current_role = token.role
            idx += 1
            continue

        if current_role is None:
            raise _to_message_block_error(
                BlockParseError(
                    code='image_missing_role',
                    message=':::image must appear after a role block.',
                    lineno=token.lineno,
                    raw_line=token.raw_line,
                )
            )

        _flush_text()
        resolved = resolve_image_value(lines, idx, is_block_line=is_block_line)
        if resolved is None:
            raise _to_message_block_error(
                BlockParseError(
                    code='image_missing_value',
                    message=':::image requires a path or URL on a following non-empty line.',
                    lineno=idx + 1,
                    raw_line=raw_line.rstrip('\r\n'),
                )
            )
        image_value, value_idx = resolved
        mime_type, used_default = infer_image_mime(image_value)
        if used_default:
            logger.warning(
                'Could not infer image MIME type from path/URL suffix; defaulting to image/png.',
                line=value_idx + 1,
                value=image_value,
            )
        current_parts.append(build_image_part(image_value, mime_type))
        idx = value_idx + 1

    _flush_message()
    return messages
