# ============================================================
# File: _solve_nonlinear_planar_systems_phase_plane.py
# Topic: Nonlinear Planar Systems (Phase Plane, Linearization, Stability, Limit Cycles, Bifurcations)
#
# System:
#   x' = f(x,y)
#   y' = g(x,y)
#
# What we do (template-style, app-friendly):
#   1) Find equilibria: solve f=0, g=0
#   2) Compute Jacobian J = [[fx, fy],[gx, gy]]
#   3) For each equilibrium: evaluate J*, compute trace τ and det Δ
#      classify using trace–determinant test:
#        Δ < 0  => saddle (unstable)
#        Δ > 0, τ < 0 => stable (node/spiral)
#        Δ > 0, τ > 0 => unstable (node/spiral)
#        Δ > 0: discriminate by D = τ^2 - 4Δ:
#             D > 0 => node,  D < 0 => spiral,  D=0 => degenerate
#        Δ = 0 => inconclusive/line of equilibria (special)
#
#   4) Nullclines:
#        x-nullcline: f(x,y)=0
#        y-nullcline: g(x,y)=0
#      We return them (implicit equations) for phase-portrait reasoning.
#
#   5) (Optional) Bendixson–Dulac divergence test on a rectangular region:
#        div = fx + gy
#      If div has a strict sign on a simply-connected region, no closed orbits there.
#      (We keep it as a conceptual check; we can evaluate sign only if user provides region.)
#
#   6) (Optional) Bifurcation parameter μ:
#        If system includes μ, we show equilibria and τ(μ), Δ(μ) for Hopf hints:
#          Hopf candidate when Δ>0 and τ crosses 0 with nonzero imaginary part (D<0).
#
# Notes:
#   - We intentionally avoid numeric plotting; this is a symbolic analyzer for your app.
#   - Output: HTML aligned steps + final classification summary.
# ============================================================

from __future__ import annotations

from typing import Optional, Union, Dict, Any, List, Tuple

import sympy as sp


def solve_nonlinear_planar_systems_phase_plane(
    f_of_xy: Union[str, sp.Expr],
    g_of_xy: Union[str, sp.Expr],
    *,
    x_symbol: str = "x",
    y_symbol: str = "y",
    mu_symbol: str = "mu",
    # Optional region for divergence test: x in [xmin,xmax], y in [ymin,ymax]
    xmin: Optional[Union[int, float, str, sp.Expr]] = None,
    xmax: Optional[Union[int, float, str, sp.Expr]] = None,
    ymin: Optional[Union[int, float, str, sp.Expr]] = None,
    ymax: Optional[Union[int, float, str, sp.Expr]] = None,
    # control
    max_equilibria: int = 8,
    locals_map: Optional[Dict[str, Any]] = None,
    simplify: bool = True,
) -> dict:
    """
    Analyze planar system:
        x' = f(x,y),  y' = g(x,y)

    Returns:
        { "problem_s": "...", "soln_s": "...", "answer_s": "..." }
    """
    x = sp.Symbol(x_symbol, real=True)
    y = sp.Symbol(y_symbol, real=True)
    mu = sp.Symbol(mu_symbol, real=True)

    base_locals = {
        x_symbol: x,
        y_symbol: y,
        mu_symbol: mu,
        "pi": sp.pi,
        "E": sp.E,
        "exp": sp.exp,
        "log": sp.log,
        "ln": sp.log,
        "sqrt": sp.sqrt,
        "sin": sp.sin,
        "cos": sp.cos,
        "tan": sp.tan,
        "sinh": sp.sinh,
        "cosh": sp.cosh,
        "tanh": sp.tanh,
        "Abs": sp.Abs,
        "Piecewise": sp.Piecewise,
        "Heaviside": sp.Heaviside,
        "Max": sp.Max,
        "Min": sp.Min,
    }
    if locals_map:
        base_locals.update(locals_map)

    def P(v: Union[str, sp.Expr, int, float]) -> sp.Expr:
        return sp.sympify(v, locals=base_locals)

    def Ltx(expr: sp.Expr) -> str:
        return sp.latex(expr)

    # Parse f, g
    try:
        f = P(f_of_xy)
    except Exception as e:
        raise ValueError(f"Could not parse f(x,y)={f_of_xy!r}. Error: {e}") from e

    try:
        g = P(g_of_xy)
    except Exception as e:
        raise ValueError(f"Could not parse g(x,y)={g_of_xy!r}. Error: {e}") from e

    if simplify:
        f = sp.simplify(f)
        g = sp.simplify(g)

    # Jacobian
    fx = sp.diff(f, x)
    fy = sp.diff(f, y)
    gx = sp.diff(g, x)
    gy = sp.diff(g, y)

    if simplify:
        fx = sp.simplify(fx)
        fy = sp.simplify(fy)
        gx = sp.simplify(gx)
        gy = sp.simplify(gy)

    J = sp.Matrix([[fx, fy], [gx, gy]])

    # Equilibria (best-effort)
    eq_solutions: List[Dict[sp.Symbol, sp.Expr]] = []
    try:
        sols = sp.solve([sp.Eq(f, 0), sp.Eq(g, 0)], [x, y], dict=True)
        if isinstance(sols, list):
            eq_solutions = sols[:max_equilibria]
    except Exception:
        eq_solutions = []

    # If solve fails, try nonlinear solve on simple cases (skip heavy)
    # We'll keep it: return nullclines only
    has_eq = len(eq_solutions) > 0

    # Classification helper
    def classify_from_trace_det(tau: sp.Expr, det: sp.Expr) -> str:
        tau_s = sp.simplify(tau) if simplify else tau
        det_s = sp.simplify(det) if simplify else det
        D = sp.simplify(tau_s**2 - 4 * det_s) if simplify else (tau_s**2 - 4 * det_s)

        # Try to decide sign symbolically; if can't, keep generic
        def sign_hint(expr: sp.Expr) -> Optional[int]:
            s = sp.sign(expr)
            if s in (sp.Integer(-1), -1):
                return -1
            if s in (sp.Integer(1), 1):
                return 1
            if s in (sp.Integer(0), 0):
                return 0
            return None

        det_sign = sign_hint(det_s)
        tau_sign = sign_hint(tau_s)
        D_sign = sign_hint(D)

        if det_sign == -1:
            return "Saddle (unstable)"
        if det_sign == 0:
            return "Degenerate (det=0): linear test inconclusive / line of equilibria possible"
        if det_sign == 1:
            # stable/unstable by tau
            if tau_sign == -1:
                # stable node or spiral
                if D_sign == 1:
                    return "Stable node"
                if D_sign == -1:
                    return "Stable spiral (focus)"
                if D_sign == 0:
                    return "Stable degenerate node (repeated eigenvalue)"
                return "Stable (node/spiral depends on discriminant)"
            if tau_sign == 1:
                if D_sign == 1:
                    return "Unstable node"
                if D_sign == -1:
                    return "Unstable spiral (focus)"
                if D_sign == 0:
                    return "Unstable degenerate node (repeated eigenvalue)"
                return "Unstable (node/spiral depends on discriminant)"
            if tau_sign == 0:
                # center/spiral boundary
                if D_sign == -1:
                    return "Center (linearized) / possible Hopf boundary (need nonlinear terms)"
                if D_sign == 0:
                    return "Pure imaginary repeated / degenerate (inconclusive)"
                return "Trace=0: borderline (needs nonlinear analysis)"
        return "Inconclusive (symbolic signs undetermined)"

    # Analyze each equilibrium
    eq_rows = []
    for sol in eq_solutions:
        xs = sol.get(x, None)
        ys = sol.get(y, None)
        if xs is None or ys is None:
            continue

        Jstar = J.subs({x: xs, y: ys})
        if simplify:
            Jstar = sp.simplify(Jstar)

        tau = sp.trace(Jstar)
        det = sp.det(Jstar)
        disc = sp.simplify(tau**2 - 4 * det) if simplify else (tau**2 - 4 * det)

        eigs = None
        try:
            eigs = [ev for ev in Jstar.eigenvals().keys()]
        except Exception:
            eigs = None

        cls = classify_from_trace_det(tau, det)

        eq_rows.append({
            "x*": xs,
            "y*": ys,
            "J": Jstar,
            "tau": tau,
            "det": det,
            "disc": disc,
            "eigs": eigs,
            "class": cls,
        })

    # Nullclines
    x_nullcline = sp.Eq(f, 0)
    y_nullcline = sp.Eq(g, 0)

    # Divergence (Bendixson–Dulac with B=1)
    div = fx + gy
    if simplify:
        div = sp.simplify(div)

    region_ready = (xmin is not None and xmax is not None and ymin is not None and ymax is not None)
    div_note = ""
    if region_ready:
        xmin_e = sp.sympify(xmin, locals=base_locals)
        xmax_e = sp.sympify(xmax, locals=base_locals)
        ymin_e = sp.sympify(ymin, locals=base_locals)
        ymax_e = sp.sympify(ymax, locals=base_locals)
        if simplify:
            xmin_e = sp.simplify(xmin_e); xmax_e = sp.simplify(xmax_e)
            ymin_e = sp.simplify(ymin_e); ymax_e = sp.simplify(ymax_e)

        # We won’t attempt rigorous sign proof; we provide the criterion statement + expression.
        div_note = (
            "Bendixson–Dulac check (B=1) on the region "
            "\\(x\\in[" + Ltx(xmin_e) + "," + Ltx(xmax_e) + "],\\; y\\in[" + Ltx(ymin_e) + "," + Ltx(ymax_e) + "]\\): "
            "If \\(\\nabla\\cdot F = \\frac{\\partial f}{\\partial x}+\\frac{\\partial g}{\\partial y}\\) has a strict sign "
            "throughout a simply-connected region, then no limit cycle lies entirely in that region."
        )

    # Build output strings (avoid f-string brace issues)
    sys_ltx = "\\dot{x}=" + Ltx(f) + ",\\quad \\dot{y}=" + Ltx(g)
    problem_s = "Analyze the nonlinear planar system: \\( " + sys_ltx + " \\). Find equilibria and classify stability."

    # Equilibria block
    eq_block = ""
    if not has_eq:
        eq_block = (
            "& \\displaystyle \\Rightarrow \\text{Equilibria: solve } f(x,y)=0,\\; g(x,y)=0 \\\\"
            "& \\displaystyle \\Rightarrow \\text{(Symbolic solve did not return isolated equilibria; use nullclines to locate intersections.)} \\\\"
        )
    else:
        eq_block += "& \\displaystyle \\Rightarrow \\text{Equilibria (critical points):} \\\\"
        for i, row in enumerate(eq_rows[:max_equilibria], start=1):
            eq_block += (
                "& \\displaystyle \\Rightarrow (x^*,y^*)_" + str(i) + " = \\left(" + Ltx(row["x*"]) + "," + Ltx(row["y*"]) + "\\right) \\\\"
            )

    # Jacobian and classification block
    jac_block = (
        "& \\displaystyle \\Rightarrow J(x,y)=\\begin{pmatrix}"
        + Ltx(fx) + " & " + Ltx(fy) + "\\\\"
        + Ltx(gx) + " & " + Ltx(gy)
        + "\\end{pmatrix} \\\\[10pt]"
    )

    class_block = ""
    if has_eq:
        class_block += "& \\displaystyle \\Rightarrow \\text{Linearize and classify each equilibrium (trace–determinant test):} \\\\"
        for i, row in enumerate(eq_rows[:max_equilibria], start=1):
            eig_line = ""
            if row["eigs"] is not None:
                eig_line = ",\\; \\lambda\\text{'s}=" + ",\\;".join([Ltx(ev) for ev in row["eigs"]])
            class_block += (
                "& \\displaystyle \\Rightarrow (x^*,y^*)_" + str(i) + ":\\; "
                + "\\tau=" + Ltx(row["tau"]) + ",\\; \\Delta=" + Ltx(row["det"]) + ",\\; "
                + "(\\tau^2-4\\Delta)=" + Ltx(row["disc"])
                + eig_line
                + " \\\\"
                "& \\displaystyle \\Rightarrow \\text{Classification: " + row["class"] + "} \\\\[6pt]"
            )

    # Nullclines block
    null_block = (
        "& \\displaystyle \\Rightarrow \\text{Nullclines:} \\\\"
        "& \\displaystyle \\Rightarrow x\\text{-nullcline: } " + Ltx(x_nullcline) + " \\\\"
        "& \\displaystyle \\Rightarrow y\\text{-nullcline: } " + Ltx(y_nullcline) + " \\\\[8pt]"
        "& \\displaystyle \\Rightarrow \\text{They partition the plane into regions where }(\\dot{x},\\dot{y})\\text{ signs are constant.} \\\\"
    )

    # Limit cycle tools note
    lc_note = (
        "Limit cycles (nonlinear-only): use Poincar\\'e–Bendixson (bounded region with no equilibria) to prove existence, "
        "or Bendixson–Dulac (divergence sign) to rule them out."
    )

    # Bifurcation / Hopf hint note (parameter mu)
    hopf_note = (
        "Bifurcation hint (Hopf): if an equilibrium has \\(\\Delta>0\\) and \\(\\tau(\\mu)\\) crosses 0 while \\(\\tau^2-4\\Delta<0\\), "
        "a Hopf bifurcation may occur (limit cycle born)."
    )

    soln_s = (
        "<div class='wrap'><div class='card'>"
        "<div class='badge'>Nonlinear Planar Systems — Phase Plane & Linearization<br/></div>"
        "\\(\\displaystyle \\begin{aligned}"
        "& \\displaystyle " + sys_ltx + " \\\\[8pt]"
        + eq_block +
        jac_block +
        class_block +
        null_block +
        "& \\displaystyle \\Rightarrow \\text{Divergence: } \\nabla\\cdot F = " + Ltx(div) + " \\\\[6pt]"
        "\\end{aligned}\\)"
        "<div class='note'>" + lc_note + "</div>"
        "<div class='note'>" + (div_note if div_note else "") + "</div>"
        "<div class='note'>" + hopf_note + "</div>"
        "</div></div>"
    )

    # Answer: compact equilibrium classifications (or nullclines)
    if not has_eq:
        answer_s = "\\(\\text{Use nullclines }" + Ltx(x_nullcline) + "\\text{ and }" + Ltx(y_nullcline) + "\\text{ to locate equilibria; then classify via Jacobian.}\\)"
    else:
        ans_lines = []
        for i, row in enumerate(eq_rows[:max_equilibria], start=1):
            ans_lines.append(
                "(x^*,y^*)_" + str(i) + "=\\left(" + Ltx(row["x*"]) + "," + Ltx(row["y*"]) + "\\right):\\; " + row["class"]
            )
        answer_s = "\\( " + " \\; ; \\; ".join(ans_lines) + " \\)"

    return {"problem_s": problem_s.strip(), "soln_s": soln_s.strip(), "answer_s": answer_s.strip()}
