from pathlib import Path
from typing import Any, Generic

from routix.constants import SubroutineReportStatisticsKeys
from routix.io import dump_json, dump_yaml

from .subroutine_report import SubroutineReportT


class SubroutineReportStatistics(Generic[SubroutineReportT]):
    """Computes statistics and summary information from collected subroutine reports."""

    def __init__(
        self,
        name: str,
        reports: list[SubroutineReportT],
        method_call_counts: dict[str, int],
    ):
        self.name = name
        self.reports = reports
        self.method_call_counts = method_call_counts

    @property
    def has_valid_objective_value(self) -> bool:
        return any(r.obj_value is not None for r in self.reports)

    @property
    def total_elapsed_time(self) -> float:
        return sum(r.elapsed_time for r in self.reports)

    @property
    def first_report(self) -> SubroutineReportT | None:
        return self.reports[0] if self.reports else None

    @property
    def last_report(self) -> SubroutineReportT | None:
        return self.reports[-1] if self.reports else None

    @property
    def valid_reports(self) -> list[SubroutineReportT]:
        return [r for r in self.reports if r.obj_value is not None]

    @property
    def min_obj_report(self) -> SubroutineReportT | None:
        """
        Returns:
            SubroutineReportT | None: The report with the minimum objective value.
                - If no valid reports exist, returns None.
                - Tie-breaker: prefers later reports in case of equal objective values.
        """
        fea = self.valid_reports
        if not fea:
            return None
        idx, _ = min(enumerate(fea), key=lambda t: (t[1].obj_value, -t[0]))
        return fea[idx]

    @property
    def max_obj_report(self) -> SubroutineReportT | None:
        """
        Returns:
            SubroutineReportT | None: The report with the maximum objective value.
                - If no valid reports exist, returns None.
                - Tie-breaker: prefers later reports in case of equal objective values.
        """
        fea = self.valid_reports
        if not fea:
            return None
        idx, _ = max(enumerate(fea), key=lambda t: (t[1].obj_value, t[0]))
        return fea[idx]

    def get_best_report(self, is_maximize: bool = False) -> SubroutineReportT | None:
        """Get the best report based on the objective value.

        Args:
            is_maximize (bool): True if the objective is to maximize, False if to minimize.

        Returns:
            SubroutineReportT | None: The best report based on objective value.
                - If is_maximize is True, returns the report with the maximum objective value.
                - If is_maximize is False, returns the report with the minimum objective value.
                - If no valid reports exist, returns None.
        """
        return self.max_obj_report if is_maximize else self.min_obj_report

    def get_improvement_ratio(self, is_maximize: bool = False) -> float | None:
        first = self.first_report
        best = self.get_best_report(is_maximize=is_maximize)
        if not (
            first
            and best
            and first.obj_value is not None
            and best.obj_value is not None
        ):
            return None
        if first.obj_value == 0:
            return None
        if is_maximize:
            return (best.obj_value - first.obj_value) / first.obj_value
        return (first.obj_value - best.obj_value) / first.obj_value

    # Serialization methods

    def to_dict(self, is_maximize: bool = False) -> dict[str, Any]:
        """Return a dictionary representation of the statistics.

        Args:
            is_maximize (bool, optional): True if the objective is to maximize, False if to minimize.
                Defaults to False.

        Returns:
            dict[str, Any]: A dictionary representation of the statistics.

        The dictionary includes:
            - `instanceName`: Name of the subroutine instance.
            - `foundFeasibleSol`: Boolean indicating if a feasible solution was found.
            - `totalElapsedTime`: Total elapsed time across all reports.
            - `firstObj`: Objective value of the first report, or None if not available.
            - `firstBound`: Objective bound of the first report, or None if not available.
            - `bestObj`: Objective value of the best report, or None if not available.
            - `bestBound`: Objective bound of the best report, or None if not available.
            - `improvementRatio`: Improvement ratio from the first to the best report.
            - `methodCallCounts`: Serialized string of method call counts.
            - `reportCount`: Total number of reports collected.
        """
        first = self.first_report
        best = self.get_best_report(is_maximize=is_maximize)
        return {
            SubroutineReportStatisticsKeys.INSTANCE_NAME: self.name,
            SubroutineReportStatisticsKeys.FOUND_FEASIBLE_SOL: self.has_valid_objective_value,
            SubroutineReportStatisticsKeys.TOTAL_ELAPSED_TIME: self.total_elapsed_time,
            SubroutineReportStatisticsKeys.FIRST_OBJ: getattr(first, "obj_value", None)
            if first
            else None,
            SubroutineReportStatisticsKeys.FIRST_BOUND: getattr(
                first, "obj_bound", None
            )
            if first
            else None,
            SubroutineReportStatisticsKeys.BEST_OBJ: getattr(best, "obj_value", None)
            if best
            else None,
            SubroutineReportStatisticsKeys.BEST_BOUND: getattr(best, "obj_bound", None)
            if best
            else None,
            SubroutineReportStatisticsKeys.IMPROVEMENT_RATIO: self.get_improvement_ratio(
                is_maximize
            ),
            SubroutineReportStatisticsKeys.METHOD_CALL_COUNTS: f'"{self.method_call_counts}"',
            SubroutineReportStatisticsKeys.REPORT_COUNT: len(self.reports),
        }

    def to_yaml(self, file_path: Path, is_maximize: bool = False) -> None:
        dump_yaml(self.to_dict(is_maximize), file_path)

    def to_json(self, file_path: Path, is_maximize: bool = False) -> None:
        dump_json(self.to_dict(is_maximize), file_path)

    def to_string_dict(self, is_maximize: bool = False) -> dict[str, str]:
        """
        Return a dictionary with string representations of each field, suitable for CSV export.

        - Scalar fields are converted to strings.
        - Method call count dictionary is serialized with "".
          - If the log is empty, the string is empty.

        Args:
            is_maximize (bool, optional): True if the objective is to maximize, False if to minimize.
                Defaults to False.

        Returns:
            dict[str, str]: Dictionary with string representations.
        """
        data = self.to_dict(is_maximize=is_maximize)
        return_dict = {k: str(v) for k, v in data.items()}
        # Override dictionary
        # Remove the "defaultdict(...)" wrapper if present, and just use the dict string
        return_dict[SubroutineReportStatisticsKeys.METHOD_CALL_COUNTS] = (
            f'"{str(dict(self.method_call_counts))}"'
        )

        return return_dict
