"""Rule diff computation for Lattice.

Maps proposals to current rules to produce structured diffs.
"""

from __future__ import annotations

import deal

from lattice.core.types.proposal import Proposal, ProposalAction
from lattice.core.types.rule import Rule, RuleDiff


@deal.post(lambda result: isinstance(result, dict))
def _build_rules_index(current_rules: list[Rule]) -> dict[str, Rule]:
    """Build index of rules by title (case-insensitive)."""
    rules_by_title: dict[str, Rule] = {}
    for rule in current_rules:
        key = rule.title.lower().strip()
        rules_by_title[key] = rule
    return rules_by_title


@deal.pre(
    lambda proposals, rules_by_title: isinstance(proposals, list)
    and isinstance(rules_by_title, dict)
)
@deal.post(lambda result: len(result) == 4)
def _classify_proposals(
    proposals: list[Proposal],
    rules_by_title: dict[str, Rule],
) -> tuple[
    list[Proposal],
    list[tuple[Proposal, Rule]],
    list[tuple[Proposal, Rule]],
    list[tuple[Proposal, Rule]],
]:
    """Classify proposals into additions, removals, merges, updates.

    Note: Non-ADD proposals (REMOVE, MERGE, UPDATE) that don't match an existing
    rule by title are silently dropped. This is intentional: you cannot remove,
    merge, or update a rule that doesn't exist. ADD proposals are always included
    since they create new rules.

    Args:
        proposals: List of proposals to classify.
        rules_by_title: Dictionary mapping rule titles (lowercase) to Rules.

    Returns:
        Tuple of (additions, removals, merges, updates) where:
        - additions: Proposals for new rules (no matching rule needed)
        - removals: (Proposal, Rule) pairs for rules to remove
        - merges: (Proposal, Rule) pairs for rules to merge into
        - updates: (Proposal, Rule) pairs for rules to update
    """
    additions: list[Proposal] = []
    removals: list[tuple[Proposal, Rule]] = []
    merges: list[tuple[Proposal, Rule]] = []
    updates: list[tuple[Proposal, Rule]] = []

    for proposal in proposals:
        key = proposal.title.lower().strip()
        matched_rule = rules_by_title.get(key)

        if proposal.action == ProposalAction.ADD:
            additions.append(proposal)
        elif proposal.action == ProposalAction.REMOVE and matched_rule:
            removals.append((proposal, matched_rule))
        elif proposal.action == ProposalAction.MERGE and matched_rule:
            merges.append((proposal, matched_rule))
        elif proposal.action == ProposalAction.UPDATE and matched_rule:
            updates.append((proposal, matched_rule))

    return additions, removals, merges, updates


@deal.post(lambda result: isinstance(result[0], list) and isinstance(result[1], set))
def _build_old_content_lines(
    items: list[tuple[Proposal, Rule]],
) -> tuple[list[str], set[str]]:
    """Build content lines from matched rules (for old_content)."""
    lines: list[str] = []
    file_paths: set[str] = set()

    for _proposal, rule in items:
        lines.append(f"## {rule.title}")
        lines.append(rule.content)
        lines.append("")
        file_paths.add(rule.file_path)

    return lines, file_paths


@deal.post(lambda result: isinstance(result[0], list) and isinstance(result[1], set))
def _build_new_content_lines(
    items: list[tuple[Proposal, Rule]],
) -> tuple[list[str], set[str]]:
    """Build content lines from proposals (for new_content)."""
    lines: list[str] = []
    file_paths: set[str] = set()

    for proposal, rule in items:
        lines.append(f"## {proposal.title}")
        lines.append(proposal.content)
        lines.append("")
        file_paths.add(rule.file_path)

    return lines, file_paths


@deal.pre(lambda current_rules, proposals: isinstance(current_rules, list))
@deal.pre(lambda current_rules, proposals: isinstance(proposals, list))
@deal.pre(
    lambda current_rules, proposals: all(isinstance(r, Rule) for r in current_rules)
)
@deal.pre(
    lambda current_rules, proposals: all(isinstance(p, Proposal) for p in proposals)
)
@deal.post(lambda result: isinstance(result, RuleDiff))
def diff_rules(
    current_rules: list[Rule],
    proposals: list[Proposal],
) -> RuleDiff:
    """Compute the diff between current rules and proposed changes.

    Maps each Proposal to the affected current rules to produce
    a structured diff showing what would be added, removed, merged, unchanged.

    Matching logic:
    - Proposals match existing rules by title (case-insensitive)
    - ADD: No matching rule found → old_content is None
    - REMOVE/MERGE/UPDATE: Matching rule found → old_content from matched rule

    Args:
        current_rules: Current list of Rule objects.
        proposals: List of Proposal objects from extract_proposals.

    Returns:
        RuleDiff describing the full change set.

    >>> from lattice.core.types.rule import Rule, RuleDiff
    >>> diff = diff_rules([], [])
    >>> isinstance(diff, RuleDiff)
    True
    >>> # Test matching: ADD with no existing rule
    >>> from lattice.core.types.enums import ProposalAction, PatternType
    >>> proposal = Proposal(
    ...     proposal_id="test",
    ...     pattern=PatternType.CONVENTION,
    ...     action=ProposalAction.ADD,
    ...     title="New Rule",
    ...     content="New content",
    ...     evidence_session_ids=["s1"],
    ... )
    >>> diff = diff_rules([], [proposal])
    >>> diff.old_content is None
    True
    >>> "New Rule" in diff.new_content
    True
    """
    # Build index and classify proposals
    rules_by_title = _build_rules_index(current_rules)
    additions, removals, merges, updates = _classify_proposals(
        proposals, rules_by_title
    )

    # Build old_content from matched rules
    old_lines: list[str] = []
    file_paths: set[str] = set()

    for items in (removals, merges, updates):
        lines, paths = _build_old_content_lines(items)
        old_lines.extend(lines)
        file_paths.update(paths)

    old_content = "\n".join(old_lines).strip() if old_lines else None

    # Build new_content from proposals
    new_lines: list[str] = []

    for proposal in additions:
        new_lines.append(f"## {proposal.title}")
        new_lines.append(proposal.content)
        new_lines.append("")

    for items in (merges, updates):
        lines, paths = _build_new_content_lines(items)
        new_lines.extend(lines)
        file_paths.update(paths)

    new_content = "\n".join(new_lines).strip() if new_lines else ""

    # Determine primary file_path
    file_path = sorted(file_paths)[0] if file_paths else "rules.md"

    return RuleDiff(
        file_path=file_path,
        old_content=old_content,
        new_content=new_content,
    )
