# ============================================================
# File: _solve_ode_first_order_exact.py
# Topic: First-order Exact ODE (full-steps HTML)
# Form:  M(x,y) dx + N(x,y) dy = 0
# Exact if:  ∂M/∂y = ∂N/∂x
# Output: dict {problem_s, soln_s, answer_s}
# ============================================================

from __future__ import annotations

from typing import Optional, Union, Dict, Any

import sympy as sp


def solve_ode_first_order_exact(
    M: Union[str, sp.Expr],
    N: Union[str, sp.Expr],
    *,
    x_symbol: str = "x",
    y_symbol: str = "y",
    C_symbol: str = "C",
    x0: Optional[Union[int, float, str, sp.Expr]] = None,
    y0: Optional[Union[int, float, str, sp.Expr]] = None,
    locals_map: Optional[Dict[str, Any]] = None,
    simplify: bool = True,
) -> dict:
    """
    Full-steps HTML solution for the exact ODE:
        M(x,y) dx + N(x,y) dy = 0

    Steps:
      1) Check exactness: My = Nx
      2) Find potential ψ(x,y):
           ψ = ∫ M dx + h(y)
         then ψy = N => h'(y) = N - ∂/∂y (∫ M dx)
      3) Implicit solution:
           ψ(x,y) = C

    Optional initial condition:
        y(x0) = y0  (provide both x0 and y0)
      => C = ψ(x0, y0)

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

    # -----------------------------
    # 1) Setup symbols + parsing
    # -----------------------------
    x = sp.Symbol(x_symbol, real=True)
    y = sp.Symbol(y_symbol, real=True)
    C = sp.Symbol(C_symbol)

    base_locals = {
        x_symbol: x,
        y_symbol: y,
        "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,
    }
    if locals_map:
        base_locals.update(locals_map)

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

    try:
        M_ = sp.sympify(M, locals=base_locals) if isinstance(M, str) else sp.sympify(M)
        N_ = sp.sympify(N, locals=base_locals) if isinstance(N, str) else sp.sympify(N)
    except Exception as e:
        raise ValueError(f"Could not parse inputs: M={M!r}, N={N!r}. Error: {e}") from e

    if simplify:
        M_ = sp.simplify(M_)
        N_ = sp.simplify(N_)

    # -----------------------------
    # 2) Check exactness
    # -----------------------------
    My = sp.diff(M_, y)
    Nx = sp.diff(N_, x)
    diff_exact = sp.simplify(My - Nx) if simplify else (My - Nx)

    if not diff_exact.equals(0):
        raise ValueError(f"ODE is not exact: dM/dy - dN/dx = {diff_exact}")

    # -----------------------------
    # 3) Find potential ψ(x,y)
    # -----------------------------
    psi_part_raw = sp.integrate(M_, x)  # ∫ M dx (treat y constant)
    psi_part = sp.simplify(psi_part_raw) if simplify else psi_part_raw

    dpsi_part_dy = sp.diff(psi_part, y)
    hyprime_raw = N_ - dpsi_part_dy
    hyprime = sp.simplify(hyprime_raw) if simplify else hyprime_raw

    hy_raw = sp.integrate(hyprime, y)
    hy = sp.simplify(hy_raw) if simplify else hy_raw

    psi_raw = psi_part + hy
    psi = sp.simplify(psi_raw) if simplify else psi_raw

    # Optional IC: y(x0)=y0 => C = ψ(x0,y0)
    use_ic = (x0 is not None) and (y0 is not None)
    C_value: Optional[sp.Expr] = None
    parsed_x0 = None
    parsed_y0 = None
    if use_ic:
        parsed_x0 = sp.sympify(x0, locals=base_locals)
        parsed_y0 = sp.sympify(y0, locals=base_locals)
        C_value_raw = psi.subs({x: parsed_x0, y: parsed_y0})
        C_value = sp.simplify(C_value_raw) if simplify else C_value_raw

    # Notes for unevaluated integrals
    note_parts = []
    if bool(psi_part.has(sp.Integral)):
        note_parts.append("SymPy kept ∫M dx unevaluated.")
    if bool(hy.has(sp.Integral)):
        note_parts.append("SymPy kept ∫h'(y) dy unevaluated.")
    anti_note = " ".join(note_parts)

    # -----------------------------
    # 4) Build step-by-step HTML
    # (No duplicated leading arrows. No 'from (i)' in answer.)
    # -----------------------------
    ode_ltx = f"\\left({L(M_)}\\right)\\,d{L(x)} + \\left({L(N_)}\\right)\\,d{L(y)} = 0"

    exact_check_ltx = (
        f"\\frac{{\\partial M}}{{\\partial {L(y)}}} = {L(My)},\\quad "
        f"\\frac{{\\partial N}}{{\\partial {L(x)}}} = {L(Nx)}"
    )
    exact_ok_ltx = (
        f"\\frac{{\\partial M}}{{\\partial {L(y)}}} - \\frac{{\\partial N}}{{\\partial {L(x)}}}"
        f" = {L(diff_exact)} = 0\\;\\text{{ (Exact)}}"
    )

    psi_part_ltx = f"\\psi(x,y) = \\int {L(M_)}\\, d{L(x)} = {L(psi_part)} + h({L(y)})"
    hyprime_ltx = (
        f"\\frac{{\\partial \\psi}}{{\\partial {L(y)}}}"
        f" = {L(sp.diff(psi_part, y))} + h'({L(y)})"
        f" = {L(N_)}"
    )
    hyprime_eq_ltx = f"h'({L(y)}) = {L(hyprime)}"
    hy_ltx = f"h({L(y)}) = \\int {L(hyprime)}\\, d{L(y)} = {L(hy)}"

    psi_final_ltx = f"\\psi(x,y) = {L(psi)}"
    implicit_ltx = f"{L(psi)} = {L(C)} \\qquad \\cdots \\text{{(i)}}"

    soln_stmts = []

    soln_stmt = f"""
    <div class="wrap">
      <div class="card">
        <div class="badge">Note: First-order Exact ODE<br/></div>

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

        & \\displaystyle {ode_ltx} \\\\[6pt]

        & \\displaystyle \\Rightarrow \\text{{Check exactness:}} \\\\
        & \\displaystyle \\Rightarrow {exact_check_ltx} \\\\
        & \\displaystyle \\Rightarrow {exact_ok_ltx} \\\\[8pt]

        & \\displaystyle \\Rightarrow \\text{{Find a potential function }}\\psi(x,y): \\\\
        & \\displaystyle \\Rightarrow {psi_part_ltx} \\\\[6pt]

        & \\displaystyle \\Rightarrow \\text{{Differentiate w.r.t. }} {L(y)} \\text{{ and match with }} N(x,y): \\\\
        & \\displaystyle \\Rightarrow {hyprime_ltx} \\\\
        & \\displaystyle \\Rightarrow {hyprime_eq_ltx} \\\\
        & \\displaystyle \\Rightarrow {hy_ltx} \\\\[8pt]

        & \\displaystyle \\Rightarrow {psi_final_ltx} \\\\
        & \\displaystyle \\Rightarrow {implicit_ltx} \\\\

        \\end{{aligned}}
        \\)
        <div class="note">{anti_note}</div>
      </div>
    </div>
    """
    soln_stmts.append(soln_stmt.replace("\n", ""))

    ic_ltx = ""
    ans_ltx = ""      # answer (NO "from (i)")
    soln_ic_ltx = ""  # solution line (may mention (i))

    if use_ic and (C_value is not None):
        ic_ltx = f"{L(y)}\\Big|_{{{L(x)}={L(parsed_x0)}}} = {L(parsed_y0)}"

        C_at_x0_ltx = (
            f"{L(C)} = \\psi({L(parsed_x0)},{L(parsed_y0)}) = {L(C_value)}"
            f" \\qquad \\text{{Given }} {ic_ltx}"
        )

        # Solution shows the linkage to (i)
        soln_ic_ltx = f"{L(psi)} = {L(C_value)} \\qquad \\cdots \\text{{ from (i)}}"
        # Answer must NOT contain "from (i)"
        ans_ltx = f"{L(psi)} = {L(C_value)}"

        ic_block = f"""
            <br/>
            \\(\\displaystyle
            \\begin{{aligned}}
            & \\displaystyle \\Rightarrow {C_at_x0_ltx} \\\\
            & \\displaystyle \\Rightarrow {soln_ic_ltx} \\\\
            \\end{{aligned}}
            \\)
        """
        soln_stmts.append(ic_block.replace("\n", ""))

    # -----------------------------
    # 5) Final problem/answer strings
    # -----------------------------
    if use_ic and ans_ltx:
        problem_s = f"Solve \\( {ode_ltx} \\) for \\( {ic_ltx} \\)"
        answer_s = f"\\( {ans_ltx} \\)"
    else:
        problem_s = f"Solve \\( {ode_ltx} \\)"
        answer_s = f"\\( {L(psi)} = {L(C)} \\)"

    soln_s = "".join(soln_stmts)

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

