"""Lscopt module for lscopt."""

from __future__ import annotations

import logging
from dataclasses import dataclass
from functools import cached_property

import numpy as np
from matplotlib import pyplot as plt
from scipy.optimize import OptimizeResult, lsq_linear

__version__ = "0.1.0"


logger = logging.getLogger(__name__)


@dataclass
class LSCCurve:
    """LSC curve calculator for optimization problems.

    This class implements the Least Squares Curve fitting algorithm with
    various geometric constraints for LSC (Least Squares Curve) optimization.
    Provides comprehensive parameter validation and error handling.
    """

    def __post_init__(self) -> None:
        """Post-initialization validation and setup."""
        self._validate_parameters()

    def _validate_parameters(self) -> None:
        """Validate all input parameters for correctness and feasibility.

        Raises
        ------
            ValueError: If any parameter values are invalid or inconsistent
        """
        self._validate_angles()
        self._validate_breakpoints()
        self._validate_heights()
        self._validate_slopes()
        logger.debug(
            f"LSCCurve initialized with parameters: m={self.m}, m1={self.m1}, H={self.H}",
        )

    def _validate_angles(self) -> None:
        """Validate angle parameters J and J1.

        Raises
        ------
            ValueError: If angle values are outside valid range [0, 180]
        """
        if not 0 <= self.J <= 180:
            msg = f"Overall angle J must be between 0 and 180 degrees, got {self.J}"
            raise ValueError(msg)
        if not 0 <= self.J1 <= 180:
            msg = f"Internal angle J1 must be between 0 and 180 degrees, got {self.J1}"
            raise ValueError(msg)

    def _validate_breakpoints(self) -> None:
        """Validate breakpoint parameters m and m1.

        Raises
        ------
            ValueError: If breakpoint values are non-negative or in wrong order
        """
        if self.m >= 0:
            msg = f"Internal breakpoint m must be negative, got {self.m}"
            raise ValueError(msg)
        if self.m1 >= 0:
            msg = f"External breakpoint m1 must be negative, got {self.m1}"
            raise ValueError(msg)
        if self.m1 >= self.m:
            msg = f"External breakpoint m1 ({self.m1}) must be less than internal breakpoint m ({self.m})"
            raise ValueError(msg)

    def _validate_heights(self) -> None:
        """Validate height parameters H, H1, and H2.

        Logs warnings for negative values that may cause unexpected results.
        """
        if self.H < 0:
            logger.warning(f"Negative cutting height H={self.H} may lead to unexpected results")
        if self.H1 < 0:
            logger.warning(
                f"Negative internal retention height H1={self.H1} may lead to unexpected results",
            )
        if self.H2 < 0:
            logger.warning(
                f"Negative external retention height H2={self.H2} may lead to unexpected results",
            )

    def _validate_slopes(self) -> None:
        """Validate slope parameters s and s1.

        Logs warnings for negative values that may cause unexpected results.
        """
        if self.s < 0:
            logger.warning(f"Negative internal slope s={self.s} may lead to unexpected results")
        if self.s1 < 0:
            logger.warning(
                f"Negative external slope s1={self.s1} may lead to unexpected results",
            )

    # Basic parameter settings
    m: float = -1.3  # Internal breakpoint
    m1: float = -2.4  # External breakpoint
    s: float = 1.2183  # Internal slope
    s1: float = 8.1  # External slope
    H: float = 0.5  # Cutting height
    m2: float = 0.5  # Specific point
    H1: float = 0.2  # Internal retention height
    H2: float = 0.65  # External retention height
    J: float = 80  # Overall angle
    J1: float = 40  # Breakpoint angle

    @staticmethod
    def _cot(x: float) -> float:
        """Calculate cotangent function.

        Args:
            x: Input parameter in radians

        Returns
        -------
            float: Cotangent value
        """
        return 1 / np.tan(x)

    @cached_property
    def n(self) -> float:
        """Calculate cot(J) where J is the overall angle."""
        return self._cot(np.radians(self.J))

    @cached_property
    def t(self) -> float:
        """Calculate cot(J1) where J1 is the internal angle."""
        return self._cot(np.radians(self.J1))

    @cached_property
    def ms(self) -> float:
        """Calculate square of m (first breakpoint)."""
        return self.m**2

    @cached_property
    def mc(self) -> float:
        """Calculate cube of m (first breakpoint)."""
        return self.m**3

    @cached_property
    def ms4(self) -> float:
        """Calculate fourth power of m (first breakpoint)."""
        return self.m**4

    @cached_property
    def m1s(self) -> float:
        """Calculate square of m1 (second breakpoint)."""
        return self.m1**2

    @cached_property
    def m1c(self) -> float:
        """Calculate cube of m1 (second breakpoint)."""
        return self.m1**3

    @cached_property
    def m1s4(self) -> float:
        """Calculate fourth power of m1 (second breakpoint)."""
        return self.m1**4

    @cached_property
    def m2s(self) -> float:
        """Calculate square of m2 (specific point)."""
        return self.m2**2

    @cached_property
    def m2c(self) -> float:
        """Calculate cube of m2 (specific point)."""
        return self.m2**3

    @cached_property
    def m2s4(self) -> float:
        """Calculate fourth power of m2 (specific point)."""
        return self.m2**4

    @cached_property
    def C(self) -> np.ndarray:
        """Calculate constraint matrix C.

        Returns
        -------
            np.ndarray: Constraint matrix C (8x16)
        """
        return np.array(
            [
                [1, self.m, self.ms, self.mc, -1, -self.m, -self.ms, -self.mc, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, self.m, self.ms, self.mc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, self.m, self.ms, self.mc, 0, 0, 0, 0, 0, 0, 0, 0],
                [
                    self.m,
                    self.ms / 2,
                    self.mc / 3,
                    self.ms4 / 4,
                    -self.m,
                    -self.ms / 2,
                    -self.mc / 3,
                    -self.ms4 / 4,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                ],
                [0, 0, 0, 0, 0, 0, 0, 0, 1, self.m1, self.m1s, self.m1c, -1, -self.m1, -self.m1s, -self.m1c],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, self.m1, self.m1s, self.m1c, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, self.m1, self.m1s, self.m1c],
                [
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    self.m1,
                    self.m1s / 2,
                    self.m1c / 3,
                    self.m1s4 / 4,
                    -self.m1,
                    -self.m1s / 2,
                    -self.m1c / 3,
                    -self.m1s4 / 4,
                ],
            ],
        )

    @cached_property
    def i(self) -> np.ndarray:
        """Calculate linear space vector i for internal segment."""
        return np.linspace(self.m, 0, 100)

    @cached_property
    def j(self) -> np.ndarray:
        """Calculate linear space vector j for external segment."""
        return np.linspace(self.m1, 0, 100)

    @cached_property
    def d(self) -> np.ndarray:
        """Calculate target vector d for optimization."""
        return np.array(
            [
                0,
                self.n * self.m,
                self.t * self.m,
                self.s / 2,
                0,
                self.n * self.m1,
                self.t * self.m1,
                self.s1 / 2,
            ],
        )

    @cached_property
    def A_ineq(self) -> np.ndarray:
        """Calculate inequality constraint matrix A_ineq.

        Construct matrix A and vector b to convert equality constraints
        to bound constraints since lsq_linear doesn't directly support
        equality constraints. For equality Ax = b, construct two
        inequality constraints: Ax <= b and -Ax <= -b
        """
        return np.array(
            [
                [0, 1, 2 * self.m, 3 * self.ms, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],  # a2+2*a3*m+3*a4*m^2 <= 0
                [0, 0, 0, 0, 0, -1, -2 * self.m, -3 * self.ms, 0, 0, 0, 0, 0, 0, 0, 0],  # -(a6+2*a7*m+3*a8*m^2) <= 0
                [1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],  # a1-a5 <= 0
                [0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],  # -a2 <= 0
                [0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],  # -a6 <= 0
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2 * self.m1, 3 * self.m1s, 0, 0, 0, 0],  # a10+2*a11*m1+3*a12*m1^2 <= 0
                [
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    -1,
                    -2 * self.m1,
                    -3 * self.m1s,
                ],  # -(a14+2*a15*m1+3*a16*m1^2) <= 0
                [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 0],  # a9-a13 <= 0
                [0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0],  # -a10 <= 0
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0],  # -a14 <= 0
                [1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],  # a1-a5 <= -H (即 a5-a1 >= H)
            ],
        )

    @cached_property
    def b_ineq(self) -> np.ndarray:
        """Calculate inequality constraint vector b_ineq."""
        return np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -self.H])

    @cached_property
    def A_eq(self) -> np.ndarray:
        """Calculate equality constraint matrix A_eq."""
        return np.array(
            [
                [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],  # a2 = 0
                [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],  # a6 = 0
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],  # a10 = 0
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],  # a14 = 0
                [1, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0],  # a1-a9 = H1
                [
                    1,
                    self.m2,
                    self.m2s,
                    self.m2c,
                    0,
                    0,
                    0,
                    0,
                    -1,
                    -self.m2,
                    -self.m2s,
                    -self.m2c,
                    0,
                    0,
                    0,
                    0,
                ],  # Condition
                [0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],  # a13-a5 = H2
                [
                    0,
                    0,
                    0,
                    0,
                    -1,
                    -self.m2,
                    -self.m2s,
                    -self.m2c,
                    0,
                    0,
                    0,
                    0,
                    1,
                    self.m2,
                    self.m2s,
                    self.m2c,
                ],  # Condition
                [
                    1,
                    self.m,
                    self.ms,
                    self.mc,
                    -1,
                    -self.m,
                    -self.ms,
                    -self.mc,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                ],  # Continuity condition
                [
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    1,
                    self.m1,
                    self.m1s,
                    self.m1c,
                    -1,
                    -self.m1,
                    -self.m1s,
                    -self.m1c,
                ],  # Continuity condition
                [
                    self.m,
                    self.ms / 2,
                    self.mc / 3,
                    self.ms4 / 4,
                    -self.m,
                    -self.ms / 2,
                    -self.mc / 3,
                    -self.ms4 / 4,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                ],  # Slope
            ],
        )

    @cached_property
    def b_eq(self) -> np.ndarray:
        """Calculate equality constraint vector b_eq."""
        return np.array([0, 0, 0, 0, self.H1, self.H1, self.H2, self.H2, 0, 0, self.s / 2])

    @cached_property
    def R(self) -> OptimizeResult:
        """Calculate optimization result using least squares method.

        Returns
        -------
            OptimizeResult: Optimization result containing solution vector x
        """
        # Ax = b  =>  Ax <= b  and  -Ax <= -b
        np.vstack([self.A_ineq, self.A_eq, -self.A_eq])
        np.hstack([self.b_ineq, self.b_eq, -self.b_eq])

        # Solve using least squares method
        return lsq_linear(
            self.C,
            self.d,
            bounds=(-np.inf, np.inf),
            lsmr_tol="auto",
            verbose=0,
        )

    @cached_property
    def x(self) -> np.ndarray:
        """Calculate solution vector x from optimization result."""
        return self.R.x

    def calculate_angles(self) -> tuple[float, float, float, float]:
        """Calculate angles at specific points.

        Returns
        -------
            tuple[float, float, float, float]: Angles at points m and m1
                (angle_m_upper, angle_m_lower, angle_m1_upper, angle_m1_lower)
                in degrees
        """
        try:
            # Calculate angles at point m
            y3 = self.x[0] + self.x[1] * self.m + self.x[2] * self.ms + self.x[3] * self.mc
            angle_m_upper = np.degrees(np.arctan2((y3 - self.x[0]), self.m))
            angle_m_lower = np.degrees(np.arctan2((y3 - self.x[4]), self.m))

            # Calculate angles at point m1
            g3 = self.x[8] + self.x[9] * self.m1 + self.x[10] * self.m1s + self.x[11] * self.m1c
            angle_m1_upper = np.degrees(np.arctan2((g3 - self.x[8]), self.m1))
            angle_m1_lower = np.degrees(np.arctan2((g3 - self.x[12]), self.m1))

            logger.debug(
                f"Calculated angles: m_upper={angle_m_upper:.2f}°, "
                f"m_lower={angle_m_lower:.2f}°, m1_upper={angle_m1_upper:.2f}°, m1_lower={angle_m1_lower:.2f}°",
            )
        except (ValueError, RuntimeError, OSError) as e:
            logger.exception("Error calculating angles")
            msg = f"Failed to calculate angles: {e}"
            raise RuntimeError(msg) from e
        else:
            return angle_m_upper, angle_m_lower, angle_m1_upper, angle_m1_lower

    def plot(self, ax: plt.Axes | None = None) -> plt.Axes:
        """Plot LSC curves with matplotlib.

        Args:
            ax: Optional matplotlib axes object. If None, creates new figure.

        Returns
        -------
            plt.Axes: The axes object containing the plot

        Raises
        ------
            RuntimeError: If plotting fails due to matplotlib issues
        """
        try:
            # Calculate internal segment curves from -1.3 to 0
            y1 = self.x[0] + self.x[1] * self.i + self.x[2] * self.i**2 + self.x[3] * self.i**3  # Inner upper
            y2 = self.x[4] + self.x[5] * self.i + self.x[6] * self.i**2 + self.x[7] * self.i**3  # Inner lower

            # Calculate external segment curves from -2.4 to 0
            g1 = self.x[8] + self.x[9] * self.j + self.x[10] * self.j**2 + self.x[11] * self.j**3  # Outer upper
            g2 = self.x[12] + self.x[13] * self.j + self.x[14] * self.j**2 + self.x[15] * self.j**3  # Outer lower

            if not ax:
                # Create figure
                fig = plt.figure(figsize=(12, 8))
                ax = fig.add_subplot(111)

            ax.clear()

            # Plot curves
            ax.plot(self.i, y1, "b-", linewidth=2, label="Inner top")
            ax.plot(self.i, y2, "r-", linewidth=2, label="Inner bottom")
            ax.plot(self.j, g1, "g-", linewidth=2, label="Outer top")
            ax.plot(self.j, g2, "m-", linewidth=2, label="Outer bottom")

            # Mark key points
            ax.plot(
                self.m,
                self.x[0] + self.x[1] * self.m + self.x[2] * self.ms + self.x[3] * self.mc,
                "bo",
                markersize=8,
                label=f"Inner Point({self.m}, y1)",
            )
            ax.plot(
                self.m1,
                self.x[8] + self.x[9] * self.m1 + self.x[10] * self.m1s + self.x[11] * self.m1c,
                "gs",
                markersize=8,
                label=f"Outer Point({self.m1}, g1)",
            )

            # Set figure properties
            ax.set_xlabel("X", fontfamily="sans-serif", fontsize=12)
            ax.set_ylabel("Y", fontfamily="sans-serif", fontsize=12)
            ax.legend()
            ax.grid(visible=True, alpha=0.3)
            ax.axis("equal")
        except (ValueError, RuntimeError, OSError) as e:
            logger.exception("Error creating plot")
            msg = f"Failed to create plot: {e}"
            raise RuntimeError(msg) from e
        else:
            logger.debug("LSC curve plot created successfully")
            return ax


class LSCOptimizer:
    """Main class for LSC optimization operations.

    Provides high-level interface for LSC curve optimization
    with configurable parameters, comprehensive error handling,
    and logging support.
    """

    def __init__(self) -> None:
        """Initialize LSC optimizer instance."""

    def optimize(self, **kwargs: float) -> tuple[np.ndarray, float]:
        """Perform LSC optimization with given parameters.

        Args:
            **kwargs: LSC curve parameters including m, m1, s, s1, H, m2, H1, H2, J, J1

        Returns
        -------
            tuple[np.ndarray, float]: Tuple containing (solution_vector, residual_cost)
                - solution_vector: 16-element array of polynomial coefficients
                - residual_cost: Optimization residual/error value

        Raises
        ------
            ValueError: If input parameters are invalid
            RuntimeError: If optimization fails
        """
        try:
            logger.info(f"Starting LSC optimization with parameters: {kwargs}")
            lscc = LSCCurve(**kwargs)
            solution_vector = lscc.x
            residual_cost = lscc.R.cost
        except ValueError:
            logger.exception("Invalid parameters provided")
            raise
        except (OSError, RuntimeError) as e:
            logger.exception("Optimization failed")
            msg = "LSC optimization failed"
            raise RuntimeError(msg) from e
        else:
            logger.info(f"Optimization completed. Residual cost: {residual_cost:.6f}")
            return solution_vector, residual_cost
