'''
Created on 24-Jul-2025

@author: bthakur
'''

import inspect
import traceback
from xv.util import UtilClass
import sympy as sp
import random
from sympy import Rational
from math import gcd
from functools import reduce

# _base_detailed_operator
# BaseDetailedOperator

class BaseDetailedOperator(UtilClass):
    """
    Base class for subtraction question generators (decimals or integers).
    All math output is formatted with SymPy's latex() for consistency.
    Uses only SymPy number types for easy conversion to LaTeX and symbolic math.
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def op_subtract(self, gen_type, a, b, **kwargs):
        if gen_type == "SubtractIntQGen":
            return self.align_and_subtract_block_int(a, b, **kwargs)
            pass

        if gen_type == "SubtractDecQGen":
            return self.align_and_subtract_block_decimal(a, b, **kwargs)
            pass

        if gen_type == "SubtractFracQGen":
            return self.align_and_subtract_block_fraction(a, b, **kwargs)
            pass
        raise NotImplementedError
        pass

    def align_and_subtract_block_int(self, a, b, **kwargs):
        op = kwargs.get("op", "-")
        # Map op to proper symbol for display
        op_symbol = {
            '-': '−',
            '+': '+',
            '*': '×',
            '/': '÷'
        }.get(op, op)  # Default to op itself if not in the dict

        # Compute result according to op
        if op == '-':
            result = a - b
        elif op == '+':
            result = a + b
        elif op == '*':
            result = a * b
        elif op == '/':
            # Division by zero handled by Python (raises)
            result = a / b
        else:
            # Fallback to subtraction for unknown op
            result = a - b

        a_str = str(a)
        b_str = str(b)
        result_str = str(result)
        width = max(len(a_str), len(b_str), len(result_str)) + 4
        solid_line = "─" * width
        block = (
            f"<pre style='font-size:1.2em; margin-bottom:0;'>"
            f"{a_str.rjust(width)}\n"
            f"{op_symbol + ' ' + b_str.rjust(width-2)}\n"
            f"{solid_line}\n"
            f"{result_str.rjust(width)}\n"
            f"</pre>"
        )

        a_str = f"${self.sp_latex(a_str)}$"
        b_str = f"${self.sp_latex(a_str)}$"
        return a_str, b_str, result_str, block

    def align_and_subtract_block_decimal(self, a, b, **kwargs):
        op = kwargs.get("op", "-")
        # Map op to a display symbol for block
        op_symbol = {
            '-': '−',
            '+': '+',
            '*': '×',
            '/': '÷'
        }.get(op, op)  # fallback: op itself

        # Compute result
        if op == '-':
            result = a - b
        elif op == '+':
            result = a + b
        elif op == '*':
            result = a * b
        elif op == '/':
            result = a / b  # let Python raise if b == 0
        else:
            result = a - b  # fallback: subtraction

        # Format decimals
        a_int, a_dec = str(a).split('.') if '.' in str(a) else (str(a), '')
        b_int, b_dec = str(b).split('.') if '.' in str(b) else (str(b), '')
        max_int = max(len(a_int), len(b_int))
        max_dec = max(len(a_dec), len(b_dec))
        a_str = a_int.zfill(max_int) + '.' + a_dec.ljust(max_dec, '0')
        b_str = b_int.zfill(max_int) + '.' + b_dec.ljust(max_dec, '0')
        # Ensure correct decimal places in result
        try:
            result_str = f"{float(a_str)} {op} {float(b_str)}"
            result_value = {
                '-': float(a_str) - float(b_str),
                '+': float(a_str) + float(b_str),
                '*': float(a_str) * float(b_str),
                '/': float(a_str) / float(b_str)
            }.get(op, float(a_str) - float(b_str))
            result_str = f"{result_value:0.{max_dec}f}"
        except Exception:
            # fallback to Python result
            result_str = str(result)

        width = max(len(a_str), len(b_str), len(result_str)) + 4
        solid_line = "─" * width
        block = (
            f"<pre style='font-size:1.2em; margin-bottom:0;'>"
            f"{a_str.rjust(width)}\n"
            f"{op_symbol + ' ' + b_str.rjust(width-2)}\n"
            f"{solid_line}\n"
            f"{result_str.rjust(width)}\n"
            f"</pre>"
        )

        a_str = f"${self.sp_latex(a_str)}$"
        b_str = f"${self.sp_latex(a_str)}$"
        return a_str, b_str, result_str, block



    def align_and_subtract_block_fraction(self, a, b, **kwargs):
        """
        Returns the multi-step LaTeX solution (as the 4th item), and correct answer (as the 3rd),
        so all question types will display the answer and solution correctly.
        """
        fracs = [a, b]

        op = kwargs.get("op", None)
        if not op is None:
            ops = [op]
        else:
            ops = ['-']

        latex_solution = "<br>" + self._latex_multi_fraction_solution(fracs, ops, **kwargs)
        result = a - b
        result_str = self.sp_latex(result)
        # For backward compatibility: (a_str, b_str, result_str, latex_block/solution)

        a_str = f"${self.sp_latex(a)}$"
        b_str = f"${self.sp_latex(b)}$"
        return a_str, b_str, result_str, latex_solution


    def _latex_lcm_steps(self, x, y):
        """
        Returns a list of LaTeX strings showing LCM calculation via multiples.
        """
        multiples_x = [x * i for i in range(1, 7)]
        multiples_y = [y * i for i in range(1, 7)]
        from math import gcd
        lcm = x * y // gcd(x, y)

        return [
            "\\text{Multiples of }" + self.sp_latex(x) + " = " + ", ".join(self.sp_latex(val) for val in multiples_x) + ", \\ldots",
            "\\text{Multiples of }" + self.sp_latex(y) + " = " + ", ".join(self.sp_latex(val) for val in multiples_y) + ", \\ldots",
            "\\text{The lowest common multiple is } " + self.sp_latex(lcm) + "."
        ]


    def _latex_multi_fraction_solution(self, fracs, ops, **kwargs):
        """
        Returns LaTeX solution as steps (with_steps=True) or as a chained-equality compact answer (with_steps=False).
        Outputs one math block or HTML block per step for perfect MathJax rendering.
        Tabs are added to each math block for clarity.
        """
        with_steps = kwargs.get("with_steps", True)
        try:
            expr = self._latex_frac(fracs[0])
            for op, f in zip(ops, fracs[1:]):
                expr += f" {op} {self._latex_frac(f)}"

            denoms = [f.q for f in fracs]
            L = self._lcm_list(denoms)

            out = []
            # Step 1
            if with_steps:
                out.append({"type": "text", "value": "Step 1: Set up the numbers:"})
            out.append({"type": "math", "value": expr})

            # Step 2: LCM Calculation
            if with_steps:
                out.append({"type": "text", "value": "Step 2: Find the LCM of all denominators."})
                # LCM breakdown
                if len(denoms) == 2:
                    for lcm_line in self._latex_lcm_steps(denoms[0], denoms[1]):
                        out.append({"type": "math", "value": lcm_line})
                else:
                    for i in range(len(denoms) - 1):
                        for lcm_line in self._latex_lcm_steps(denoms[i], denoms[i+1]):
                            out.append({"type": "math", "value": lcm_line})
                # Blue bold, LCM as LaTeX
                out.append({
                    "type": "bluebold",
                    "value": f"Make all denominators equal to their LCM = ${self.sp_latex(L)}$"
                })

            # Step 3: Make common denominator (show multipliers)
            eq1 = []
            for frac in fracs:
                mult = L // frac.q
                if mult == 1:
                    eq1.append(f"\\displaystyle  \\frac{{{self.sp_latex(frac.p)}}}{{{self.sp_latex(frac.q)}}}")
                else:
                    eq1.append(self._latex_frac_with_mult(frac.p, frac.q, mult, mult))
            eq1_expr = eq1[0]
            for op, e in zip(ops, eq1[1:]):
                eq1_expr += f" {op} {e}"
            out.append({"type": "math", "value": "=" + eq1_expr, "extra_space": True})  # ADD SPACE HERE

            # Step 4: All over common denominator
            eq2 = []
            for frac in fracs:
                mult = L // frac.q
                n = frac.p * mult
                eq2.append(f"\\displaystyle  \\frac{{{self.sp_latex(n)}}}{{{self.sp_latex(L)}}}")
            eq2_expr = eq2[0]
            for op, e in zip(ops, eq2[1:]):
                eq2_expr += f" {op} {e}"
            out.append({"type": "math", "value": "=" + eq2_expr})

            # Step 5: Numerators added
            combined_num = ""
            sum_num = 0
            for i, frac in enumerate(fracs):
                mult = L // frac.q
                n = frac.p * mult
                if i == 0:
                    combined_num += f"{self.sp_latex(n)}"
                    sum_num += n
                else:
                    op = "+" if ops[i-1] == "+" else "-"
                    combined_num += f" {op} {self.sp_latex(n)}"
                    if op == "+":
                        sum_num += n
                    else:
                        sum_num -= n
            if with_steps:
                out.append({"type": "text", "value": "Step 3: As we have common denominator for all fractions, we can add all numerators."})
            out.append({"type": "math", "value": f"= \\displaystyle  \\frac{{{combined_num}}}{{{self.sp_latex(L)}}}", "extra_space": True})  # ADD SPACE HERE

            # Step 6: Combine/Reduce if needed
            total = fracs[0]
            for op, f in zip(ops, fracs[1:]):
                total = total + f if op == "+" else total - f
            if total.p != sum_num:
                out.append({"type": "math", "value": f"= \\displaystyle  \\frac{{{sum_num}}}{{{self.sp_latex(L)}}}"})

            out.append({"type": "math", "value": "=" + self._latex_frac(total)})

            # === Format output as HTML with MathJax ===
            if with_steps:
                lines = []
                for block in out:
                    if block["type"] == "text":
                        lines.append(f"<div style='margin:6px 0;'><b>{block['value']}</b></div>")
                    elif block["type"] == "bluebold":
                        lines.append(
                            "<div style='margin:8px 0 8px 2em;font-weight:bold;font-size:1.08em;color:#1976D2;'>"
                            f"{block['value']}</div>"
                        )
                    elif block.get("extra_space"):
                        # This line gets extra bottom space
                        lines.append(
                            f"<div style='margin-left:2em; margin-bottom:24px;'>${block['value']}$</div>"
                        )
                    else:  # math block
                        lines.append(
                            f"<div style='margin-left:2em;'>${block['value']}$</div>"
                        )
                return "\n".join(lines)
            else:
                # Compact, single math block
                compact = " ".join(block['value'] for block in out if block['type'] == "math")
                return f"<div>${compact}$</div>"

        except Exception as e:
            import inspect, traceback
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}: {e}
            fracs = {fracs}
            ops = {ops}
            """
            self._log(sError)
            self._log(traceback.format_exc())
            return None




    def sp_latex(self, x):
        """
        Safely convert a value to a SymPy LaTeX string.
        Only needed if x might not be a SymPy number; otherwise just use self.sp_latex(x).
        """
        return "\\displaystyle " + sp .latex(x)


    def _rand_fractions_and_ops(self, n_terms=2):
        fracs = [self._rand_fraction() for _ in range(n_terms)]
        ops = ['-'] * (n_terms - 1)
        return fracs, ops

    def _lcm_list(self, lst):
        def lcm(a, b):
            return abs(a * b) // gcd(a, b)
        return reduce(lcm, lst, 1)

    def _latex_frac(self, frac):
        return self.sp_latex(frac)

    def _latex_frac_with_mult(self, num, den, mult_num, mult_den):
        return f"\\frac{{{self.sp_latex(num)} \\times {self.sp_latex(mult_num)}}}{{{self.sp_latex(den)} \\times {self.sp_latex(mult_den)}}}"

