"""Export formatters for content list results.

Provides various output formats for exporting content items:
- markdown: Markdown list or grouped headers
- xml: XML tags with content
- numbered: Numbered list
- plain: Plain text (content only)
- json: JSON array
"""

import json
from abc import ABC, abstractmethod
from typing import Any

from alloy_runtime_types.dtos.content import ContentPartItemResponse


class ContentExportFormatter(ABC):
    """Base class for content export formatters."""

    @abstractmethod
    def format(
        self, items: list[ContentPartItemResponse], group_by: str | None = None
    ) -> str:
        """Format content items for export.

        Args:
            items: List of content part items to format
            group_by: Optional tag prefix to group items by

        Returns:
            Formatted string ready for output
        """
        pass

    def _get_content(self, item: ContentPartItemResponse) -> str:
        """Extract content text from item.

        Args:
            item: Content part item

        Returns:
            Content as string (text or JSON-serialized structured content)
        """
        if item.content_text:
            return item.content_text
        if item.content_structured:
            return json.dumps(item.content_structured, ensure_ascii=False)
        return ""

    def _format_role_label(self, item: ContentPartItemResponse) -> str:
        """Return the explicit plain-text role label."""
        return item.message_role

    def _format_role_header_label(self, item: ContentPartItemResponse) -> str:
        """Return the title-cased role label for markdown headers."""
        return self._format_role_label(item).capitalize()

    def _format_labeled_content(self, item: ContentPartItemResponse) -> str:
        """Prefix content with its role label."""
        return f"[{self._format_role_label(item)}] {self._get_content(item)}"

    def _group_items(
        self, items: list[ContentPartItemResponse], group_by: str
    ) -> dict[str, list[ContentPartItemResponse]]:
        """Group items by tag path matching the group_by prefix.

        Examples:
            group_by="ads" groups by first subtag: ads.headline -> "headline"
            group_by="ads.headline" groups by exact match -> "ads.headline"

        Args:
            items: List of content items to group
            group_by: Tag prefix to group by

        Returns:
            Dictionary mapping group names to lists of items
        """
        groups: dict[str, list[ContentPartItemResponse]] = {}
        ungrouped: list[ContentPartItemResponse] = []

        for item in items:
            matched = False
            for tag in item.tags:
                if tag.tag_path.startswith(group_by):
                    # Extract the grouping key
                    remaining = tag.tag_path[len(group_by) :].lstrip(".")
                    if remaining:
                        # Use first component after prefix
                        key = remaining.split(".")[0]
                    else:
                        # Exact match - use full tag path
                        key = tag.tag_path

                    if key not in groups:
                        groups[key] = []
                    groups[key].append(item)
                    matched = True
                    break

            if not matched:
                ungrouped.append(item)

        if ungrouped:
            groups["(ungrouped)"] = ungrouped

        return groups


class MarkdownFormatter(ContentExportFormatter):
    """Format content as markdown with numbered headers.

    Output format (flat):
        ## Item 1

        <content>

        ## Item 2

        <content>

    Output format (grouped):
        ## group_name

        ### Item 1

        <content>

        ### Item 2

        <content>
    """

    def format(
        self, items: list[ContentPartItemResponse], group_by: str | None = None
    ) -> str:
        """Format items as markdown.

        Args:
            items: Content items to format
            group_by: Optional tag prefix for grouping

        Returns:
            Markdown formatted string
        """
        if group_by:
            return self._format_grouped(items, group_by)
        return self._format_flat(items)

    def _format_flat(self, items: list[ContentPartItemResponse]) -> str:
        """Format items with numbered headers."""
        sections: list[str] = []
        for i, item in enumerate(items, 1):
            content = self._get_content(item)
            sections.append(
                f"## {self._format_role_header_label(item)} Item {i}\n\n{content}"
            )
        return "\n\n".join(sections)

    def _format_grouped(
        self, items: list[ContentPartItemResponse], group_by: str
    ) -> str:
        """Format items grouped by tag prefix with nested headers."""
        groups = self._group_items(items, group_by)
        sections: list[str] = []
        for group_name, group_items in groups.items():
            group_sections: list[str] = [f"## {group_name}"]
            for i, item in enumerate(group_items, 1):
                content = self._get_content(item)
                group_sections.append(
                    f"### {self._format_role_header_label(item)} Item {i}\n\n{content}"
                )
            sections.append("\n\n".join(group_sections))
        return "\n\n".join(sections)


class XmlFormatter(ContentExportFormatter):
    """Format content as XML tags.

    Uses CDATA sections to preserve raw content without escaping,
    which is important when content itself contains XML (e.g., system prompts).
    """

    def format(
        self, items: list[ContentPartItemResponse], group_by: str | None = None
    ) -> str:
        """Format items as XML.

        Args:
            items: Content items to format
            group_by: Optional tag prefix for grouping

        Returns:
            XML formatted string
        """
        if group_by:
            return self._format_grouped(items, group_by)
        return self._format_flat(items)

    def _wrap_cdata(self, text: str) -> str:
        """Wrap content in CDATA section to preserve raw content.

        Handles the edge case where content contains ']]>' by splitting
        into multiple CDATA sections.
        """
        # Handle edge case: content contains CDATA end marker
        if "]]>" in text:
            # Split at ]]> and rejoin with separate CDATA sections
            text = text.replace("]]>", "]]]]><![CDATA[>")
        return f"<![CDATA[{text}]]>"

    def _safe_tag_name(self, name: str) -> str:
        """Convert a name to a valid XML tag name."""
        # Replace dots and other invalid chars with underscores
        safe = name.replace(".", "_").replace("-", "_").replace(" ", "_")
        # Ensure starts with letter or underscore
        if safe and not (safe[0].isalpha() or safe[0] == "_"):
            safe = "_" + safe
        return safe or "item"

    def _format_flat(self, items: list[ContentPartItemResponse]) -> str:
        """Format items as a flat XML list."""
        lines = ["<content_items>"]
        for item in items:
            content = self._get_content(item)
            # Use first tag as element name, or "item"
            tag_name = (
                self._safe_tag_name(item.tags[0].tag_path) if item.tags else "item"
            )
            role = self._format_role_label(item)
            lines.append(
                f'  <{tag_name} role="{role}">{self._wrap_cdata(content)}</{tag_name}>'
            )
        lines.append("</content_items>")
        return "\n".join(lines)

    def _format_grouped(
        self, items: list[ContentPartItemResponse], group_by: str
    ) -> str:
        """Format items grouped by tag prefix."""
        groups = self._group_items(items, group_by)
        lines = ["<content_items>"]
        for group_name, group_items in groups.items():
            safe_name = self._safe_tag_name(group_name)
            lines.append(f"  <{safe_name}>")
            for item in group_items:
                content = self._get_content(item)
                role = self._format_role_label(item)
                lines.append(
                    f'    <item role="{role}">{self._wrap_cdata(content)}</item>'
                )
            lines.append(f"  </{safe_name}>")
        lines.append("</content_items>")
        return "\n".join(lines)


class NumberedFormatter(ContentExportFormatter):
    """Format content as numbered list."""

    def format(
        self, items: list[ContentPartItemResponse], group_by: str | None = None
    ) -> str:
        """Format items as numbered list.

        Args:
            items: Content items to format
            group_by: Optional tag prefix for grouping

        Returns:
            Numbered list string
        """
        if group_by:
            return self._format_grouped(items, group_by)
        return self._format_flat(items)

    def _format_flat(self, items: list[ContentPartItemResponse]) -> str:
        """Format items as a flat numbered list."""
        lines: list[str] = []
        for i, item in enumerate(items, 1):
            content = self._format_labeled_content(item)
            # Handle multi-line content
            content_lines = content.split("\n")
            if len(content_lines) == 1:
                lines.append(f"{i}. {content}")
            else:
                lines.append(f"{i}. {content_lines[0]}")
                padding = " " * (len(str(i)) + 2)
                for continuation in content_lines[1:]:
                    lines.append(f"{padding}{continuation}")
        return "\n".join(lines)

    def _format_grouped(
        self, items: list[ContentPartItemResponse], group_by: str
    ) -> str:
        """Format items grouped by tag prefix."""
        groups = self._group_items(items, group_by)
        lines: list[str] = []
        for group_name, group_items in groups.items():
            lines.append(f"[{group_name}]")
            for i, item in enumerate(group_items, 1):
                content = self._format_labeled_content(item)
                content_lines = content.split("\n")
                if len(content_lines) == 1:
                    lines.append(f"  {i}. {content}")
                else:
                    lines.append(f"  {i}. {content_lines[0]}")
                    padding = " " * (len(str(i)) + 4)
                    for continuation in content_lines[1:]:
                        lines.append(f"{padding}{continuation}")
            lines.append("")
        return "\n".join(lines).rstrip()


class PlainFormatter(ContentExportFormatter):
    """Format content as plain text (content only, one per line)."""

    def format(
        self, items: list[ContentPartItemResponse], group_by: str | None = None
    ) -> str:
        """Format items as plain text.

        Grouping adds blank lines between groups but no headers.

        Args:
            items: Content items to format
            group_by: Optional tag prefix for grouping

        Returns:
            Plain text string with content only
        """
        if group_by:
            return self._format_grouped(items, group_by)
        return self._format_flat(items)

    def _format_flat(self, items: list[ContentPartItemResponse]) -> str:
        """Format items as plain text."""
        lines: list[str] = []
        for item in items:
            content = self._format_labeled_content(item)
            lines.append(content)
        return "\n".join(lines)

    def _format_grouped(
        self, items: list[ContentPartItemResponse], group_by: str
    ) -> str:
        """Format items grouped by tag prefix with blank line separators."""
        groups = self._group_items(items, group_by)
        sections: list[str] = []
        for group_items in groups.values():
            section_lines: list[str] = []
            for item in group_items:
                content = self._format_labeled_content(item)
                section_lines.append(content)
            sections.append("\n".join(section_lines))
        return "\n\n".join(sections)


class JsonExportFormatter(ContentExportFormatter):
    """Format content as JSON array."""

    def format(
        self, items: list[ContentPartItemResponse], group_by: str | None = None
    ) -> str:
        """Format items as JSON.

        Args:
            items: Content items to format
            group_by: Optional tag prefix for grouping

        Returns:
            JSON string (array or object if grouped)
        """
        if group_by:
            return self._format_grouped(items, group_by)
        return self._format_flat(items)

    def _item_to_dict(self, item: ContentPartItemResponse) -> dict[str, Any]:
        """Convert content item to dictionary for JSON serialization."""
        return {
            "id": str(item.id),
            "message_role": item.message_role,
            "content_type": item.content_type_id,
            "content": item.content_text or item.content_structured,
            "tags": [t.tag_path for t in item.tags],
            "created_at": item.created_at.isoformat() if item.created_at else None,
        }

    def _format_flat(self, items: list[ContentPartItemResponse]) -> str:
        """Format items as JSON array."""
        data = [self._item_to_dict(item) for item in items]
        return json.dumps(data, indent=2, ensure_ascii=False)

    def _format_grouped(
        self, items: list[ContentPartItemResponse], group_by: str
    ) -> str:
        """Format items as JSON object with groups."""
        groups = self._group_items(items, group_by)
        data = {
            group_name: [self._item_to_dict(item) for item in group_items]
            for group_name, group_items in groups.items()
        }
        return json.dumps(data, indent=2, ensure_ascii=False)


# Registry of available export formatters
EXPORT_FORMATTERS: dict[str, type[ContentExportFormatter]] = {
    "markdown": MarkdownFormatter,
    "xml": XmlFormatter,
    "numbered": NumberedFormatter,
    "plain": PlainFormatter,
    "json": JsonExportFormatter,
}


def get_export_formatter(format_name: str) -> ContentExportFormatter | None:
    """Get an export formatter instance by name.

    Args:
        format_name: Name of the format (markdown, xml, numbered, plain, json)

    Returns:
        Formatter instance or None if format not found
    """
    formatter_cls = EXPORT_FORMATTERS.get(format_name)
    if formatter_cls:
        return formatter_cls()
    return None


def get_available_formats() -> list[str]:
    """Get list of available export format names."""
    return list(EXPORT_FORMATTERS.keys())
