# ============================================================
# File: _solve_ode_second_order_reduction_of_order.py
# Topic: 2nd-Order Linear Homogeneous ODE (Reduction of Order)
#
# Form (standard):
#   y'' + P(x) y' + Q(x) y = 0
#
# Given one known solution y1(x), a second linearly independent solution is:
#   y2(x) = y1(x) * ∫ [ exp(-∫P(x)dx) / y1(x)^2 ] dx
#
# General solution:
#   y(x) = C1*y1(x) + C2*y2(x)
#
# Notes:
#   - If SymPy can’t integrate, it keeps Integral(...)
#   - We show the formula derivation steps in an aligned block
# ============================================================

from __future__ import annotations

from typing import Any, Dict, Optional, Union

import sympy as sp


def solve_ode_second_order_reduction_of_order(
    P_of_x: Union[str, sp.Expr],
    Q_of_x: Union[str, sp.Expr],
    y1_of_x: Union[str, sp.Expr],
    *,
    x_symbol: str = "x",
    y_symbol: str = "y",
    C1_symbol: str = "C_1",
    C2_symbol: str = "C_2",
    locals_map: Optional[Dict[str, Any]] = None,
    simplify: bool = True,
) -> dict:
    """
    Solve y'' + P(x) y' + Q(x) y = 0 given one solution y1(x), using reduction of order.

    Returns:
        { "problem_s": "...", "soln_s": "...", "answer_s": "..." }
    """

    # -----------------------------
    # 1) Symbols + parsing
    # -----------------------------
    x = sp.Symbol(x_symbol, real=True)
    y = sp.Function(y_symbol)
    yx = y(x)

    C1 = sp.Symbol(C1_symbol)
    C2 = sp.Symbol(C2_symbol)

    base_locals = {
        x_symbol: x,
        "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,
        "asin": sp.asin,
        "acos": sp.acos,
        "atan": sp.atan,
        "sinh": sp.sinh,
        "cosh": sp.cosh,
        "tanh": sp.tanh,
        "Abs": sp.Abs,
        "sec": sp.sec,
        "csc": sp.csc,
        "cot": sp.cot,
        "Piecewise": sp.Piecewise,
        "Heaviside": sp.Heaviside,
    }
    if locals_map:
        base_locals.update(locals_map)

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

    def _parse(expr):
        if isinstance(expr, str):
            return sp.sympify(expr, locals=base_locals)
        return sp.sympify(expr)

    P_raw = _parse(P_of_x)
    Q_raw = _parse(Q_of_x)
    y1_raw = _parse(y1_of_x)

    P = sp.simplify(P_raw) if simplify else P_raw
    Q = sp.simplify(Q_raw) if simplify else Q_raw
    y1 = sp.simplify(y1_raw) if simplify else y1_raw

    # -----------------------------
    # 2) Validate y1 (best-effort)
    # -----------------------------
    residual = None
    try:
        res_raw = sp.diff(y1, x, 2) + P * sp.diff(y1, x) + Q * y1
        residual = sp.simplify(res_raw) if simplify else res_raw
    except Exception:
        residual = None

    # -----------------------------
    # 3) Reduction of order formula
    # -----------------------------
    intP = sp.integrate(P, x)
    mu = sp.exp(-intP)  # exp(-∫P dx)

    integrand = sp.simplify(mu / (y1**2)) if simplify else (mu / (y1**2))
    I = sp.integrate(integrand, x)
    y2 = sp.simplify(y1 * I) if simplify else (y1 * I)

    y_general = sp.simplify(C1 * y1 + C2 * y2) if simplify else (C1 * y1 + C2 * y2)

    # -----------------------------
    # 4) HTML formatting (match your style)
    # -----------------------------
    ode_ltx = (
        f"\\frac{{d^2{L(yx)}}}{{d{L(x)}^2}}"
        f" + \\left({L(P)}\\right)\\frac{{d{L(yx)}}}{{d{L(x)}}}"
        f" + \\left({L(Q)}\\right){L(yx)} = 0"
    )

    y1_ltx = f"y_1(x)={L(y1)}"

    # Use safe exp(...) style for display
    intP_ltx = L(intP) if not intP.has(sp.Integral) else L(sp.Integral(P, x))
    mu_ltx = f"\\exp\\left(-\\left({intP_ltx}\\right)\\right)"

    integrand_ltx = L(integrand)

    I_ltx = L(I) if not I.has(sp.Integral) else L(sp.Integral(integrand, x))
    y2_ltx = L(y2) if not y2.has(sp.Integral) else f"{L(y1)}\\left({I_ltx}\\right)"

    note_parts = []
    if residual is not None and residual != 0:
        note_parts.append(
            "Given \\(y_1\\) did not simplify to an exact solution under current simplification; proceeding symbolically."
        )
    if intP.has(sp.Integral) or I.has(sp.Integral) or y2.has(sp.Integral):
        note_parts.append("SymPy kept one or more integrals unevaluated.")

    anti_note = " ".join(note_parts)

    soln_stmt = f"""
    <div class="wrap">
      <div class="card">
        <div class="badge">Note: 2nd-Order Linear Homogeneous ODE → Reduction of Order<br/></div>

        \\(\\displaystyle
        \\begin{{aligned}}

        & \\displaystyle {ode_ltx} \\\\[8pt]
        & \\displaystyle \\text{{Given }} {y1_ltx}. \\\\[10pt]

        & \\displaystyle \\Rightarrow \\text{{Use the reduction of order formula:}} \\\\
        & \\displaystyle y_2(x)=y_1(x)\\int \\frac{{\\exp\\left(-\\int P(x)\\,dx\\right)}}{{\\left(y_1(x)\\right)^2}}\\,dx \\\\[10pt]

        & \\displaystyle \\Rightarrow \\int P(x)\\,dx = {intP_ltx} \\\\
        & \\displaystyle \\Rightarrow \\exp\\left(-\\int P(x)\\,dx\\right) = {mu_ltx} \\\\[8pt]

        & \\displaystyle \\Rightarrow \\text{{Integrand}}=\\frac{{\\exp\\left(-\\int P\\,dx\\right)}}{{\\left(y_1\\right)^2}} = {integrand_ltx} \\\\
        & \\displaystyle \\Rightarrow \\int \\frac{{\\exp\\left(-\\int P\\,dx\\right)}}{{\\left(y_1\\right)^2}}\\,dx = {I_ltx} \\\\[8pt]

        & \\displaystyle \\Rightarrow y_2(x) = {y2_ltx} \\\\[10pt]

        & \\displaystyle \\Rightarrow \\text{{General solution: }}\\; y(x)=C_1 y_1(x) + C_2 y_2(x) \\\\
        & \\displaystyle \\Rightarrow {L(yx)} = {L(y_general)} \\\\

        \\end{{aligned}}
        \\)

        <div class="note">{anti_note}</div>
      </div>
    </div>
    """.replace("\n", "")

    problem_s = (
        f"Given \\( {ode_ltx} \\) and a known solution \\( {y1_ltx} \\), "
        f"find a second solution \\(y_2\\) by reduction of order and write the general solution."
    )
    answer_s = f"\\( y_2(x) = {y2_ltx},\\quad {L(yx)} = {L(y_general)} \\)"

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