"""
Rules engine for Lattice.

Core layer - pure logic for rule manipulation.
No I/O operations.

Reference: RFC-002 §2.3, §2.4
"""

from __future__ import annotations

import deal

from lattice.core.types.rule import Rule


# =============================================================================
# Token Counting
# =============================================================================


@deal.post(lambda result: result >= 0)
def count_tokens_approximate(text: str) -> int:
    """Approximate token count using simple heuristic.

    Uses word-based estimation: ~1.3 tokens per word on average.
    For accurate counting, use tiktoken in Shell layer.

    >>> count_tokens_approximate("Hello world")
    3
    >>> count_tokens_approximate("")
    0
    """
    if not text:
        return 0
    words = len(text.split())
    return int(words * 1.3) + 1


@deal.post(lambda result: result >= 0)
def count_rule_tokens(rule: Rule) -> int:
    """Count tokens in a rule (content only).

    >>> from lattice.core.types.rule import Rule
    >>> r = Rule(file_path="test.md", title="Test", content="Hello world")
    >>> count_rule_tokens(r)
    3
    """
    return count_tokens_approximate(rule.content)


# =============================================================================
# Promoted Tag Parsing
# =============================================================================


PROMOTED_TAG = "<!-- lattice:promoted -->"


# Spec: parse_tags — split into is_promoted() and strip_promoted_tag() for clarity.
@deal.post(lambda result: isinstance(result, bool))
def is_promoted(content: str) -> bool:
    """Check if rule content has promoted tag.

    >>> is_promoted("# Rule\\n<!-- lattice:promoted -->\\nContent")
    True
    >>> is_promoted("# Rule\\nContent")
    False
    """
    return PROMOTED_TAG in content


@deal.post(lambda result: isinstance(result, str))
def strip_promoted_tag(content: str) -> str:
    """Remove promoted tag from content.

    >>> strip_promoted_tag("# Rule\\n<!-- lattice:promoted -->\\nContent")
    '# Rule\\n\\nContent'
    >>> strip_promoted_tag("# Rule\\nContent")
    '# Rule\\nContent'
    """
    return content.replace(PROMOTED_TAG, "").strip()


# =============================================================================
# Rule Merging
# =============================================================================


@deal.post(lambda result: isinstance(result, list))
@deal.post(lambda result: all(isinstance(r, Rule) for r in result))
def merge_rules(
    global_rules: list[Rule],
    project_rules: list[Rule],
) -> list[Rule]:
    """Merge global and project rules with overlay resolution.

    Project rules with matching file_path override global rules.
    Rules marked as promoted are excluded from project rules.

    >>> from lattice.core.types.rule import Rule
    >>> g = Rule(file_path="conventions.md", title="Global", content="Global rule")
    >>> p = Rule(file_path="conventions.md", title="Project", content="Project override")
    >>> merged = merge_rules([g], [p])
    >>> len(merged)
    1
    >>> merged[0].title
    'Project'
    """
    # Build override map from project rules
    project_overrides: dict[str, Rule] = {}
    for rule in project_rules:
        if not rule.is_promoted:
            project_overrides[rule.file_path] = rule

    # Start with non-overridden global rules
    result: list[Rule] = []
    overridden_files: set[str] = set()

    for rule in global_rules:
        if rule.file_path in project_overrides:
            # Mark as overridden, don't include global version
            overridden_files.add(rule.file_path)
        else:
            result.append(rule)

    # Add project rules (including overrides)
    for rule in project_rules:
        if not rule.is_promoted:
            result.append(rule)

    return result


@deal.post(lambda result: result >= 0)
def total_token_count(rules: list[Rule]) -> int:
    """Calculate total token count for a list of rules.

    >>> from lattice.core.types.rule import Rule
    >>> rules = [
    ...     Rule(file_path="a.md", title="A", content="Hello world"),
    ...     Rule(file_path="b.md", title="B", content="Foo bar"),
    ... ]
    >>> total_token_count(rules)
    6
    """
    return sum(count_rule_tokens(r) for r in rules)


# =============================================================================
# Instinct Assembly
# =============================================================================


INSTINCT_HEADER = """## Lattice — Active Instincts
Follow these rules in all responses. They are synthesized from project history.

"""


@deal.post(lambda result: isinstance(result, str))
def assemble_instincts(
    rules: list[Rule],
    global_rule_names: set[str] | None = None,
    capture_active: bool = True,
) -> str:
    """Assemble rules into a single instincts string.

    Groups rules by source (global vs project) and formats for injection.

    Args:
        rules: List of Rule objects to format.
        global_rule_names: Set of rule file paths that are global rules.
        capture_active: If False, omits log_turn from tool guidance.

    >>> from lattice.core.types.rule import Rule
    >>> rules = [
    ...     Rule(file_path="global.md", title="Global", content="Be helpful"),
    ...     Rule(file_path="local.md", title="Local", content="Use TDD"),
    ... ]
    >>> result = assemble_instincts(rules, global_rule_names={"global.md"})
    >>> "Global" in result and "Local" in result
    True
    """
    if not rules:
        return ""

    global_names = global_rule_names or set()

    global_rules = [r for r in rules if r.file_path in global_names]
    project_rules = [r for r in rules if r.file_path not in global_names]

    parts: list[str] = [INSTINCT_HEADER]

    if global_rules:
        parts.append("### Global\n")
        for rule in global_rules:
            parts.append(_format_rule(rule))
        parts.append("\n")

    if project_rules:
        parts.append("### Project\n")
        for rule in project_rules:
            parts.append(_format_rule(rule))
        parts.append("\n")

    # Include log_turn guidance only when capture mode is active
    if capture_active:
        parts.append("""### Memory Tools
- Use `lattice_search` when you need to recall past decisions or context.
- Use `log_turn` to record important interactions for future reference.
""")
    else:
        parts.append("""### Memory Tools
- Use `lattice_search` when you need to recall past decisions or context.
""")

    return "".join(parts)


@deal.post(lambda result: isinstance(result, str))
def _format_rule(rule: Rule) -> str:
    """Format a single rule for instincts."""
    return f"**{rule.title}**\n{rule.content}\n\n"
