"""Plain-text conversation parser with boundary detection.

Recognises several common formats people use when logging LLM conversations:

1. Role-prefixed lines (most common):
   ``User: Hello``
   ``Assistant: Hi there``
   ``System: You are a helpful assistant.``

2. Markdown-bold prefixes:
   ``**User:** Hello``
   ``**Assistant:** Hi there``

3. Angle-bracket role tags:
   ``[User] Hello``
   ``[Assistant] Hi there``

4. All-caps role labels:
   ``USER: Hello``
   ``ASSISTANT: Hi``

5. Conversation separator lines (``---``, ``===``, blank lines) between
   separate conversations, enabling multi-conversation files.

Each contiguous block of turns without a separator becomes one conversation.
Multi-line turn content (indented or blank-line-separated within a role block)
is supported — a new role label starts a new turn.
"""

from __future__ import annotations

import re
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Iterator, TextIO

from canari_forensics.models import ConversationTurn


_ROLE_ALIASES: dict[str, str] = {
    "user": "user",
    "human": "user",
    "you": "user",
    "question": "user",
    "q": "user",
    "assistant": "assistant",
    "ai": "assistant",
    "bot": "assistant",
    "answer": "assistant",
    "a": "assistant",
    "llm": "assistant",
    "response": "assistant",
    "system": "system",
    "sys": "system",
    "instruction": "system",
    "tool": "tool",
    "function": "tool",
}

_ROLE_PREFIX_RE = re.compile(
    r"^(?:\*\*)?(?:\[)?([A-Za-z][A-Za-z0-9 _\-]{0,20})(?:\]|\*\*)?[ \t]*:[ \t]*(.*)",
    re.IGNORECASE,
)

_SEPARATOR_RE = re.compile(r"^(?:-{3,}|={3,}|#{3,}|\*{3,})[ \t]*$")


def _normalise_role(raw: str) -> str | None:
    key = raw.lower().strip()
    return _ROLE_ALIASES.get(key)


def _is_separator(line: str) -> bool:
    return bool(_SEPARATOR_RE.match(line.strip()))


class PlainTextParser:
    """Parser for plain-text LLM conversation logs."""

    def parse_file(self, path: str | Path) -> Iterator[ConversationTurn]:
        with open(path, encoding="utf-8", errors="replace") as f:
            yield from self.parse_stream(f)

    def parse_directory(self, path: str | Path) -> Iterator[ConversationTurn]:
        for file_path in sorted(Path(path).rglob("*.txt")):
            yield from self.parse_file(file_path)
        for file_path in sorted(Path(path).rglob("*.md")):
            yield from self.parse_file(file_path)
        for file_path in sorted(Path(path).rglob("*.log")):
            yield from self.parse_file(file_path)

    def parse_stream(self, stream: TextIO) -> Iterator[ConversationTurn]:
        now = datetime.now(timezone.utc)
        lines = stream.read().splitlines()
        yield from self._parse_lines(lines, now)

    def _parse_lines(
        self, lines: list[str], now: datetime
    ) -> Iterator[ConversationTurn]:
        # Segment into conversations on separator lines
        conversations: list[list[str]] = []
        current: list[str] = []
        for line in lines:
            if _is_separator(line):
                if any(l.strip() for l in current):
                    conversations.append(current)
                current = []
            else:
                current.append(line)
        if any(l.strip() for l in current):
            conversations.append(current)

        if not conversations:
            conversations = [lines]

        for conv_lines in conversations:
            yield from self._parse_conversation(conv_lines, now)

    def _parse_conversation(
        self, lines: list[str], now: datetime
    ) -> Iterator[ConversationTurn]:
        conv_id = str(uuid.uuid4())
        turns: list[tuple[str, list[str]]] = []  # (role, content_lines)

        current_role: str | None = None
        current_content: list[str] = []

        for line in lines:
            m = _ROLE_PREFIX_RE.match(line)
            if m:
                raw_role = m.group(1)
                rest = m.group(2).strip()
                role = _normalise_role(raw_role)
                if role is not None:
                    # Flush previous turn
                    if current_role is not None:
                        content = " ".join(current_content).strip()
                        if content:
                            turns.append((current_role, current_content[:]))
                    current_role = role
                    current_content = [rest] if rest else []
                    continue

            # Continuation line for current turn
            if current_role is not None:
                current_content.append(line)

        # Flush last turn
        if current_role is not None:
            content = " ".join(current_content).strip()
            if content:
                turns.append((current_role, current_content[:]))

        for idx, (role, content_lines) in enumerate(turns):
            content = "\n".join(content_lines).strip()
            if not content:
                continue
            yield ConversationTurn(
                conversation_id=conv_id,
                turn_index=idx,
                role=role,
                content=content,
                timestamp=now,
                metadata={"source_line_count": len(content_lines)},
                source_format="plain",
            )
