"""
The ``verbalize`` module provides verbalizers for presenting RAI model structure
and semantics to Cortex agents.
"""
from abc import ABC
from typing import Any, List
import inspect
import re
import textwrap

import relationalai.semantics

from relationalai.util.docutils import include_in_docs

__include_in_docs__ = True


@include_in_docs
class Verbalizer(ABC):
    """Abstract base class for verbalizing RAI model structure and semantics.

    Verbalizers control how model information is presented to Cortex agents.
    They provide two levels of explanation:

    - Model-level: Overall structure, relationships, and available concepts
    - Concept-level: Detailed business rules for specific concepts

    Implement this interface to customize how your models are explained to agents.
    """

    @include_in_docs
    def explain_model(self) -> str:
        """Return comprehensive explanation of the model's structure and relationships.

        Returns
        -------
        str
            A natural language explanation of the model.
        """
        raise NotImplementedError()

    @include_in_docs
    def explain_concept(self, concept: str) -> str:
        """Return detailed explanation of business rules for a specific concept.

        Parameters
        ----------
        concept : str
            The name of the concept to explain.

        Returns
        -------
        str
            A natural language explanation of the concept.
        """
        raise NotImplementedError()


@include_in_docs
class ModelVerbalizer(Verbalizer):
    """Default verbalizer that returns relationship readings from a RAI Model.

    Extracts relationship readings from a RAI Model and returns them as sorted,
    newline-separated text. This provides a concise overview of the model's
    semantic relationships.

    Parameters
    ----------
    model : relationalai.semantics.Model
        RAI Model to verbalize.

    Examples
    --------

    >>> model = rai.Model("jaffle")
    >>> verbalizer = ModelVerbalizer(model)
    >>> print(verbalizer.explain_model())
    Customer has many Orders
    Order has one Customer
    """

    def __init__(self, model: relationalai.semantics.Model):
        self.model = model

    @include_in_docs
    def explain_model(self) -> str:
        """Return sorted, newline-separated relationship readings from the model.

        Returns
        -------
        str
            Sorted relationship readings, one per line.
        """
        results = []
        for rel in self.model.relationships:
            for reading in rel._readings:
                results.append(re.sub(r'#\d+', '', reading._reading))
        results.sort()
        return "\n".join(results)

    @include_in_docs
    def explain_concept(self, concept: str) -> str:
        """Return a generic message since all concepts are explained at the model level.

        Parameters
        ----------
        concept : str
            The name of the concept to explain.

        Returns
        -------
        str
            A message indicating that concepts are explained in the model verbalization.
        """
        return "All concepts are explained in the model verbalization"


@include_in_docs
class SourceCodeVerbalizer(ModelVerbalizer):
    """Verbalizer that uses relationship readings for model-level explanations
    and returns Python source code for concept-level explanations.

    Extends :class:`ModelVerbalizer`. The ``explain_model`` method is inherited
    and returns sorted relationship readings. The ``explain_concept`` method
    filters the provided modules to those that reference the requested concept
    and returns their source code.

    Leading indentation is normalized and excessive newlines are collapsed for
    cleaner presentation.

    Parameters
    ----------
    model : relationalai.semantics.Model
        RAI Model to verbalize.
    *modules : Any
        Variable number of Python callables (functions, classes) or modules
        containing model definitions. Each must have retrievable source code
        via ``inspect.getsource()``. Built-ins without source are silently
        skipped.

    Examples
    --------

    >>> from myproject.model import define_customer, define_order
    >>> verbalizer = SourceCodeVerbalizer(model, define_customer, define_order)
    >>> print(verbalizer.explain_concept("Customer"))
    def define_customer(model):
        ...
    """

    def __init__(self, model: relationalai.semantics.Model, *modules: Any):
        super().__init__(model)
        self.modules: List[Any] = list(modules)

    @include_in_docs
    def explain_concept(self, concept: str) -> str:
        """Extract source code from modules that reference the given concept.

        Filters to only include modules where the concept name appears in
        source code.

        Parameters
        ----------
        concept : str
            Concept name to search for in the source code.

        Returns
        -------
        str
            Concatenated source code from matching modules with normalized
            formatting. Returns ``"Could not explain {concept}"`` if no modules
            contain the concept.
        """
        source_codes = []

        for item in self.modules:
            try:
                source = inspect.getsource(item)
                # Only include this module if it contains the concept
                if concept in source:
                    # Dedent to remove leading indentation
                    source = textwrap.dedent(source)
                    source_codes.append(source)
            except (OSError, TypeError):
                # Skip items where source cannot be retrieved
                continue

        # If no matching modules found, return error message
        if not source_codes:
            return f"Could not explain {concept}"

        # Concatenate all matching source codes
        combined = "\n".join(source_codes)

        # Remove extra newlines (collapse 3+ consecutive newlines to 2)
        combined = re.sub(r'\n{3,}', '\n\n', combined)

        return combined
