import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Generic

from .report import SubroutineReportT
from .type_defs import SolutionT

# --- Data Structures ---


@dataclass
class SolutionRecord(Generic[SubroutineReportT, SolutionT]):
    """
    A data container for the output of a single subroutine execution, linking
    the performance report with the actual solution produced.
    """

    report: SubroutineReportT
    """The report detailing the subroutine's performance and metadata."""
    solution: SolutionT | None
    """
    The solution object generated by the subroutine.
    This can be None if the subroutine does not produce a new solution
    (e.g., configuration steps like 'set_random_seed').
    """


# --- Base Class ---


class SolutionManager(Generic[SubroutineReportT, SolutionT], ABC):
    """
    An abstract base class for managing the lifecycle of solutions during an
    optimization process.
    """

    def __init__(self) -> None:
        self.history: list[SolutionRecord[SubroutineReportT, SolutionT]] = []
        self.incumbent_solution: SolutionT | None = None
        self.best_obj_value: float | None = None
        self.best_obj_bound: float | None = None

    @abstractmethod
    def _get_obj_value(self, solution: SolutionT) -> float:
        """
        Extracts the objective value from a given solution object.
        """
        ...

    @abstractmethod
    def _a_is_better_obj_value(self, value_a: float, value_b: float | None) -> bool:
        """True if the first objective value is better than the second."""
        ...

    def current_obj_value_is_worse_than(self, new_value: float) -> bool:
        """Checks if the new objective value is worse than the current best.

        Args:
            new_value (float): The new objective value to compare.

        Returns:
            bool: True if the new value is worse than the current objective value, False otherwise.
        """
        return self._a_is_better_obj_value(new_value, self.best_obj_value)

    @abstractmethod
    def _a_is_better_obj_bound(self, bound_a: float, bound_b: float | None) -> bool:
        """True if the first objective bound is better than the second."""
        ...

    def current_obj_bound_is_worse_than(self, new_bound: float) -> bool:
        """Checks if the new objective bound is worse than the current best.

        Args:
            new_bound (float): The new objective bound to compare.

        Returns:
            bool: True if the new bound is worse than the current objective bound, False otherwise.
        """
        return self._a_is_better_obj_bound(new_bound, self.best_obj_bound)

    def register(self, report: SubroutineReportT, solution: SolutionT | None) -> bool:
        """
        Registers the result of a subroutine, updates the history, and updates
        the incumbent solution and bound if they are improved.

        Args:
            report (SubroutineReportT): The report generated by the subroutine.
            solution (SolutionT | None): The solution produced by the subroutine, if any.

        Raises:
            ValueError: If the reported objective value is inconsistent with the computed value.

        Returns:
            bool: True if the incumbent solution was updated, False otherwise.
        """
        self.history.append(SolutionRecord(report=report, solution=solution))

        # Update objective bound if a new one is reported
        if report.obj_bound is not None:
            if self._a_is_better_obj_bound(report.obj_bound, self.best_obj_bound):
                self.best_obj_bound = report.obj_bound
                logging.info(f"Objective bound updated: {self.best_obj_bound}")

        # Update incumbent solution if a new solution is provided and is better
        if solution is None:
            return False

        new_obj_value = self._get_obj_value(solution)
        if new_obj_value != report.obj_value:
            raise ValueError(
                "Inconsistent objective value: "
                f"computed {new_obj_value}, reported {report.obj_value}"
            )
        if self._a_is_better_obj_value(new_obj_value, self.best_obj_value):
            self.incumbent_solution = solution
            self.best_obj_value = new_obj_value
            logging.info(
                f"Incumbent solution updated with objective: {self.best_obj_value}"
            )
            return True

        return False

    def get_incumbent(self) -> SolutionT | None:
        """Returns the best solution found so far."""
        return self.incumbent_solution

    def has_incumbent(self) -> bool:
        """Checks if a valid incumbent solution exists."""
        return self.incumbent_solution is not None

    def get_last_solution(self) -> SolutionT | None:
        """
        Retrieves the most recently generated valid solution from the history.
        """
        for record in reversed(self.history):
            if record.solution is not None:
                return record.solution
        return None
