"""Pure markdown persona parsing helpers.

Spec source: vectl step `core-markdown-parser` requires parsing
markdown files with optional YAML frontmatter into PersonaSpec-compatible dicts.
"""

from deal import post, pre
from typing import Any
import yaml


# PersonaSpec fields that can be set via frontmatter
# (excludes computed/read-only fields like spec_version)
PERSONA_SPEC_FRONTMATTER_FIELDS = frozenset(
    {
        "id",
        "description",
        "prompt",
        "model",
        "model_params",
        "tools",
        "can_spawn",
        "spec_digest",
    }
)


@pre(
    lambda content, fallback_name: (
        isinstance(content, str)
        and isinstance(fallback_name, str)
        and len(fallback_name.strip()) > 0
    )
)
@post(lambda result: isinstance(result, dict))
@post(lambda result: "id" in result and "prompt" in result)
@post(lambda result: isinstance(result["id"], str) and len(result["id"]) > 0)
@post(lambda result: result["prompt"] is None or isinstance(result["prompt"], str))
def parse_markdown_persona(content: str, fallback_name: str) -> dict[str, Any]:
    """Parse markdown content with optional YAML frontmatter into persona fields.

    Detects optional YAML frontmatter delimited by ``---``. Extracts ``id``
    from frontmatter if present, otherwise uses ``fallback_name``. Uses markdown
    body (after frontmatter) as ``prompt``.

    Unknown frontmatter fields are filtered out to prevent PersonaSpec validation
    errors (PersonaSpec uses extra="forbid"). Only known PersonaSpec fields
    (id, model, can_spawn, etc.) are included in the returned dict.

    OpenCode compatibility: If frontmatter has a ``description`` field but no
    ``prompt`` and the body is empty, ``description`` is used as
    ``prompt``.

    Args:
        content: Raw markdown content, optionally with YAML frontmatter.
        fallback_name: ID to use if frontmatter lacks ``id`` field.

    Returns:
        A dict with ``id``, ``prompt``, and PersonaSpec fields from
        frontmatter, compatible with PersonaSpec construction.

    Examples:
        >>> # Scenario 1: Frontmatter has id
        >>> content1 = '''---
        ... id: alice
        ... model: gpt-4
        ... ---
        ... You are a helpful assistant.'''
        >>> result1 = parse_markdown_persona(content1, "fallback")
        >>> result1["id"]
        'alice'
        >>> result1["prompt"]
        'You are a helpful assistant.'

        >>> # Scenario 2: Frontmatter without id uses fallback
        >>> content2 = '''---
        ... model: gpt-4
        ... version: 1.0
        ... ---
        ... Be concise.'''
        >>> result2 = parse_markdown_persona(content2, "Bob")
        >>> result2["id"]
        'bob'
        >>> result2["prompt"]
        'Be concise.'
        >>> # Unknown 'version' field is filtered out
        >>> "version" in result2
        False

        >>> # Scenario 3: No frontmatter uses entire content
        >>> content3 = 'Just a plain prompt.'
        >>> result3 = parse_markdown_persona(content3, "Default")
        >>> result3["id"]
        'default'
        >>> result3["prompt"]
        'Just a plain prompt.'

        >>> # Scenario 4: Frontmatter-only with empty body
        >>> content4 = '''---
        ... id: eve
        ... ---
        ... '''
        >>> result4 = parse_markdown_persona(content4, "fallback")
        >>> result4["id"]
        'eve'
        >>> result4["prompt"]
        ''

        >>> # Scenario 5: OpenCode-style description (no body)
        >>> content5 = '''---
        ... model: gpt-4
        ... description: A helpful assistant
        ... ---
        ... '''
        >>> result5 = parse_markdown_persona(content5, "fallback")
        >>> "description" in result5
        False
        >>> result5["prompt"]
        'A helpful assistant'

        >>> # Scenario 6: Description with body (body takes precedence)
        >>> content6 = '''---
        ... model: gpt-4
        ... description: Ignored description
        ... ---
        ... Use this body.'''
        >>> result6 = parse_markdown_persona(content6, "fallback")
        >>> result6["prompt"]
        'Use this body.'

        >>> # Scenario 7: Non-mapping YAML (list) - treated as no frontmatter
        >>> content7 = '''---
        ... - item1
        ... - item2
        ... ---
        ... Just body content.'''
        >>> result7 = parse_markdown_persona(content7, "fallback")
        >>> result7["id"]
        'fallback'
        >>> result7["prompt"]
        '---\\n- item1\\n- item2\\n---\\nJust body content.'

        >>> # Scenario 8: UTF-8 BOM is stripped
        >>> content8 = '\\ufeff---\\nid: bom-agent\\nmodel: gpt-4\\n---\\nHello BOM.'
        >>> result8 = parse_markdown_persona(content8, "fallback")
        >>> result8["id"]
        'bom-agent'
        >>> result8["prompt"]
        'Hello BOM.'

        >>> # Invalid fallback_name raises contract error
        >>> parse_markdown_persona("content", "")  # doctest: +IGNORE_EXCEPTION_DETAIL
        Traceback (most recent call last):
        ...
        PreContractError: ...
    """
    # Normalize filename-derived fallback to PersonaSpec id style.
    fallback_id = fallback_name.strip().lower().replace("_", "-")

    # Strip UTF-8 BOM (common on Windows-saved files)
    if content.startswith("\ufeff"):
        content = content[1:]

    lines = content.split("\n")

    # Check for frontmatter: must start with ---
    if not lines or lines[0].strip() != "---":
        # No frontmatter: entire content is prompt
        return {
            "id": fallback_id,
            "prompt": content.strip(),
        }

    # Find the closing ---
    frontmatter_lines = []
    body_start_index = 1

    for i in range(1, len(lines)):
        if lines[i].strip() == "---":
            # End of frontmatter
            body_start_index = i + 1
            break
        frontmatter_lines.append(lines[i])
    else:
        # No closing --- found, treat entire content as body
        return {
            "id": fallback_id,
            "prompt": content.strip(),
        }

    # Parse frontmatter
    frontmatter_text = "\n".join(frontmatter_lines)
    try:
        frontmatter = yaml.safe_load(frontmatter_text) or {}
    except yaml.YAMLError:
        # Invalid YAML: treat as no frontmatter
        return {
            "id": fallback_id,
            "prompt": content.strip(),
        }

    # Normalize YAML output: YAML allows non-dict values (scalars, lists)
    # PersonaSpec requires a dict, so treat non-dict as empty frontmatter
    if not isinstance(frontmatter, dict):
        # Non-mapping YAML (list, scalar, etc.): treat entire content as body
        return {
            "id": fallback_id,
            "prompt": content.strip(),
        }

    # Extract id from frontmatter or use fallback
    # Backward compatibility: accept legacy `name` key when `id` is absent.
    persona_id = frontmatter.get("id", frontmatter.get("name", fallback_id))
    if not isinstance(persona_id, str) or len(persona_id.strip()) == 0:
        persona_id = fallback_id

    # Body is everything after the closing ---
    body = "\n".join(lines[body_start_index:])
    body_stripped = body.strip()

    # Filter frontmatter to only PersonaSpec-allowed fields
    # Unknown fields are ignored to prevent PersonaSpec validation errors
    filtered_frontmatter = {
        key: value
        for key, value in frontmatter.items()
        if key in PERSONA_SPEC_FRONTMATTER_FIELDS
    }

    # Handle OpenCode-style 'description' field as fallback for prompt
    # If prompt not in frontmatter but description is, use description
    # (only when body is empty)
    if "prompt" not in filtered_frontmatter and "description" in frontmatter:
        # 'description' is not a PersonaSpec field, but we treat it as prompt
        # for OpenCode compatibility
        description_value = frontmatter["description"]
        if isinstance(description_value, str) and not body_stripped:
            # Body is empty, so use description as prompt
            filtered_frontmatter["prompt"] = description_value

    # Merge filtered frontmatter fields with id and prompt
    # ID uses fallback if missing/empty, prompt uses body (if not empty)
    # or filtered_frontmatter (which may have description fallback)
    result = dict(filtered_frontmatter)
    result["id"] = persona_id.strip()
    # Only override prompt if body has content
    if body_stripped:
        result["prompt"] = body_stripped
    elif "prompt" not in result:
        # No body and no description fallback, use empty string
        result["prompt"] = ""
    return result


__all__ = ["parse_markdown_persona", "PERSONA_SPEC_FRONTMATTER_FIELDS"]
