"""Response builders for OR-Tools CP-SAT solver.

This module handles solution response construction and analysis.
"""

from ortools.sat.python import cp_model

from chuk_mcp_solver.models import (
    BindingConstraint,
    ConstraintKind,
    ConstraintSense,
    Explanation,
    LinearConstraintParams,
    Solution,
    SolutionVariable,
    SolveConstraintModelRequest,
    SolveConstraintModelResponse,
    SolverStatus,
)

# Tolerance for checking binding constraints
BINDING_TOLERANCE = 1e-6


def build_success_response(
    status: SolverStatus,
    solver: cp_model.CpSolver,
    var_map: dict[str, cp_model.IntVar],
    request: SolveConstraintModelRequest,
) -> SolveConstraintModelResponse:
    """Build a successful solution response.

    Args:
        status: The solver status.
        solver: The CP-SAT solver with solution.
        var_map: Mapping from variable ID to CP-SAT variable.
        request: The original request.

    Returns:
        SolveConstraintModelResponse with solution data.
    """
    # Extract solution values
    solution_vars = []
    for var_def in request.variables:
        cp_var = var_map[var_def.id]
        value = solver.Value(cp_var)
        solution_vars.append(
            SolutionVariable(
                id=var_def.id,
                value=float(value),
                metadata=var_def.metadata,
            )
        )

    solution = Solution(variables=solution_vars)

    # Get objective value if applicable
    objective_value = None
    if request.objective:
        objective_value = solver.ObjectiveValue()

    # Identify binding constraints
    binding_constraints = identify_binding_constraints(solver, var_map, request)

    # Build explanation
    summary = build_solution_summary(status, objective_value, len(binding_constraints))
    explanation = Explanation(
        summary=summary,
        binding_constraints=binding_constraints,
    )

    return SolveConstraintModelResponse(
        status=status,
        objective_value=objective_value,
        solutions=[solution],
        explanation=explanation,
    )


def build_failure_response(status: SolverStatus) -> SolveConstraintModelResponse:
    """Build a failure response.

    Args:
        status: The solver status (infeasible, unbounded, timeout, error).

    Returns:
        SolveConstraintModelResponse with failure information.
    """
    summary_map = {
        SolverStatus.INFEASIBLE: "Problem is infeasible. No solution exists that satisfies all constraints.",
        SolverStatus.UNBOUNDED: "Problem is unbounded. Objective can be improved infinitely.",
        SolverStatus.TIMEOUT: "Solver timed out before finding optimal solution. Try increasing max_time_ms or simplifying the problem.",
        SolverStatus.ERROR: "Solver encountered an error. Check constraint definitions and variable domains.",
    }

    summary = summary_map.get(status, "Solver failed to find solution. Check problem formulation.")

    return SolveConstraintModelResponse(
        status=status,
        explanation=Explanation(summary=summary),
    )


def identify_binding_constraints(
    solver: cp_model.CpSolver,
    var_map: dict[str, cp_model.IntVar],
    request: SolveConstraintModelRequest,
) -> list[BindingConstraint]:
    """Identify constraints that are tight/binding in the solution.

    Args:
        solver: The CP-SAT solver with solution.
        var_map: Mapping from variable ID to CP-SAT variable.
        request: The original request.

    Returns:
        List of binding constraints.
    """
    binding = []

    for constraint in request.constraints:
        # Only analyze linear constraints for now
        if constraint.kind != ConstraintKind.LINEAR:
            continue

        params: LinearConstraintParams = constraint.params  # type: ignore[assignment]

        # Evaluate LHS with solution values
        lhs_value = sum(term.coef * solver.Value(var_map[term.var]) for term in params.terms)

        # Check if constraint is binding (tight)
        is_binding = False
        if params.sense == ConstraintSense.LESS_EQUAL:
            is_binding = abs(lhs_value - params.rhs) < BINDING_TOLERANCE
        elif params.sense == ConstraintSense.GREATER_EQUAL:
            is_binding = abs(lhs_value - params.rhs) < BINDING_TOLERANCE
        else:  # EQUAL
            is_binding = True  # Equality constraints are always binding

        if is_binding:
            binding.append(
                BindingConstraint(
                    id=constraint.id,
                    sense=params.sense,
                    lhs_value=lhs_value,
                    rhs=params.rhs,
                    metadata=constraint.metadata,
                )
            )

    return binding


def build_solution_summary(
    status: SolverStatus, objective_value: float | None, num_binding: int
) -> str:
    """Build a human-readable solution summary.

    Args:
        status: The solver status.
        objective_value: The objective value, if any.
        num_binding: Number of binding constraints.

    Returns:
        Summary string.
    """
    if status == SolverStatus.OPTIMAL:
        if objective_value is not None:
            summary = f"Found optimal solution with objective value {objective_value:.2f}."
        else:
            summary = "Found optimal solution."
    elif status == SolverStatus.FEASIBLE:
        if objective_value is not None:
            summary = f"Found feasible solution with objective value {objective_value:.2f} (may not be optimal)."
        else:
            summary = "Found feasible solution (may not be optimal)."
    else:  # SATISFIED
        summary = "Satisfied all constraints."

    if num_binding > 0:
        summary += f" {num_binding} constraint(s) are binding (tight)."

    return summary
