import typing
from dataclasses import dataclass, field

# --- Types ---


@dataclass
class BeliefLiteral:
    """
    Represents a literal or atom.
    Example: phase(1), user_said("hello"), ~started
    """

    functor: str
    args: list[str] = field(default_factory=list)
    negated: bool = False

    def __str__(self):
        # In ASL, 'not' is usually for closed-world assumption (prolog style),
        # '~' is for explicit negation in beliefs.
        # For simplicity in behavior trees, we often use 'not' for conditions.
        prefix = "not " if self.negated else ""
        if not self.args:
            return f"{prefix}{self.functor}"

        # Clean args to ensure strings are quoted if they look like strings,
        # but usually the converter handles the quoting of string literals.
        args_str = ", ".join(self.args)
        return f"{prefix}{self.functor}({args_str})"


@dataclass
class GoalLiteral:
    name: str

    def __str__(self):
        return f"!{self.name}"


@dataclass
class ActionLiteral:
    """
    Represents a step in a plan body.
    Example: .say("Hello") or !achieve_goal
    """

    code: str

    def __str__(self):
        return self.code


@dataclass
class BinaryOp:
    """
    Represents logical operations.
    Example: (A & B) | C
    """

    left: "Expression | str"
    operator: typing.Literal["&", "|"]
    right: "Expression | str"

    def __str__(self):
        l_str = str(self.left)
        r_str = str(self.right)

        if isinstance(self.left, BinaryOp):
            l_str = f"({l_str})"
        if isinstance(self.right, BinaryOp):
            r_str = f"({r_str})"

        return f"{l_str} {self.operator} {r_str}"


Literal = BeliefLiteral | GoalLiteral | ActionLiteral
Expression = Literal | BinaryOp | str


@dataclass
class Rule:
    """
    Represents an inference rule.
    Example: head :- body.
    """

    head: Expression
    body: Expression | None = None

    def __str__(self):
        if not self.body:
            return f"{self.head}."
        return f"{self.head} :- {self.body}."


@dataclass
class PersistentRule:
    """
    Represents an inference rule, where the inferred belief is persistent when formed.
    """

    head: Expression
    body: Expression

    def __str__(self):
        if not self.body:
            raise Exception("Rule without body should not be persistent.")

        lines = []

        if isinstance(self.body, BinaryOp):
            lines.append(f"+{self.body.left}")
            if self.body.operator == "&":
                lines.append(f"  :   {self.body.right}")
            lines.append(f"  <-  +{self.head}.")
            if self.body.operator == "|":
                lines.append(f"+{self.body.right}")
                lines.append(f"  <-  +{self.head}.")

        return "\n".join(lines)


@dataclass
class Plan:
    """
    Represents a plan.
    Syntax: +trigger : context <- body.
    """

    trigger: BeliefLiteral | GoalLiteral
    context: list[Expression] = field(default_factory=list)
    body: list[ActionLiteral] = field(default_factory=list)

    def __str__(self):
        # Indentation settings
        INDENT = "      "
        ARROW = "\n  <-  "
        COLON = "\n  :   "

        # Build Header
        header = f"+{self.trigger}"
        if self.context:
            ctx_str = f" &\n{INDENT}".join(str(c) for c in self.context)
            header += f"{COLON}{ctx_str}"

        # Case 1: Empty body
        if not self.body:
            return f"{header}."

        # Case 2: Short body (optional optimization, keeping it uniform usually better)
        header += ARROW

        lines = []
        # We start the first action on the same line or next line.
        # Let's put it on the next line for readability if there are multiple.

        if len(self.body) == 1:
            return f"{header}{self.body[0]}."

        # First item
        lines.append(f"{header}{self.body[0]};")
        # Middle items
        for item in self.body[1:-1]:
            lines.append(f"{INDENT}{item};")
        # Last item
        lines.append(f"{INDENT}{self.body[-1]}.")

        return "\n".join(lines)


@dataclass
class AgentSpeakFile:
    """
    Root element representing the entire generated file.
    """

    initial_beliefs: list[Rule] = field(default_factory=list)
    inference_rules: list[Rule | PersistentRule] = field(default_factory=list)
    plans: list[Plan] = field(default_factory=list)

    def __str__(self):
        sections = []

        if self.initial_beliefs:
            sections.append("// --- Initial Beliefs & Facts ---")
            sections.extend(str(rule) for rule in self.initial_beliefs)
            sections.append("")

        if self.inference_rules:
            sections.append("// --- Inference Rules ---")
            sections.extend(str(rule) for rule in self.inference_rules if isinstance(rule, Rule))
            sections.append("")
            sections.extend(
                str(rule) for rule in self.inference_rules if isinstance(rule, PersistentRule)
            )
            sections.append("")

        if self.plans:
            sections.append("// --- Plans ---")
            # Separate plans by a newline for readability
            sections.extend(str(plan) + "\n" for plan in self.plans)

        return "\n".join(sections)
