"""Pydantic models for constraint solver.

This module defines all data models, enums, and types used throughout the solver.
No magic strings - all constants are defined as enums or module-level constants.
"""

from __future__ import annotations

from enum import Enum
from typing import Any

from pydantic import BaseModel, Field

# ============================================================================
# Constants
# ============================================================================

# API versioning
API_VERSION = "1.0.0"

# Constraint kinds
CONSTRAINT_KIND_LINEAR = "linear"
CONSTRAINT_KIND_ALL_DIFFERENT = "all_different"
CONSTRAINT_KIND_ELEMENT = "element"
CONSTRAINT_KIND_TABLE = "table"
CONSTRAINT_KIND_IMPLICATION = "implication"
CONSTRAINT_KIND_CUMULATIVE = "cumulative"
CONSTRAINT_KIND_CIRCUIT = "circuit"
CONSTRAINT_KIND_RESERVOIR = "reservoir"
CONSTRAINT_KIND_NO_OVERLAP = "no_overlap"


# ============================================================================
# Enums - No Magic Strings
# ============================================================================


class SolverMode(str, Enum):
    """Solver execution mode."""

    SATISFY = "satisfy"
    OPTIMIZE = "optimize"


class VariableDomainType(str, Enum):
    """Variable domain types supported by the solver."""

    BOOL = "bool"
    INTEGER = "integer"


class ConstraintSense(str, Enum):
    """Comparison operators for linear constraints."""

    LESS_EQUAL = "<="
    GREATER_EQUAL = ">="
    EQUAL = "=="


class ObjectiveSense(str, Enum):
    """Optimization direction."""

    MINIMIZE = "min"
    MAXIMIZE = "max"


class ConstraintKind(str, Enum):
    """Types of constraints supported by the solver."""

    LINEAR = CONSTRAINT_KIND_LINEAR
    ALL_DIFFERENT = CONSTRAINT_KIND_ALL_DIFFERENT
    ELEMENT = CONSTRAINT_KIND_ELEMENT
    TABLE = CONSTRAINT_KIND_TABLE
    IMPLICATION = CONSTRAINT_KIND_IMPLICATION
    CUMULATIVE = CONSTRAINT_KIND_CUMULATIVE
    CIRCUIT = CONSTRAINT_KIND_CIRCUIT
    RESERVOIR = CONSTRAINT_KIND_RESERVOIR
    NO_OVERLAP = CONSTRAINT_KIND_NO_OVERLAP


class SolverStatus(str, Enum):
    """Solution status returned by the solver."""

    OPTIMAL = "optimal"
    FEASIBLE = "feasible"
    SATISFIED = "satisfied"
    INFEASIBLE = "infeasible"
    UNBOUNDED = "unbounded"
    TIMEOUT = "timeout"  # Deprecated: use TIMEOUT_BEST or TIMEOUT_NO_SOLUTION
    TIMEOUT_BEST = "timeout_best"  # Timeout reached but best-so-far solution available
    TIMEOUT_NO_SOLUTION = "timeout_no_solution"  # Timeout reached with no solution found
    ERROR = "error"


# ============================================================================
# Variable Domain Models
# ============================================================================


class VariableDomain(BaseModel):
    """Domain specification for a decision variable.

    Defines the type and bounds for a variable in the constraint model.
    """

    type: VariableDomainType = Field(
        ...,
        description="Variable domain type: 'bool' for binary variables, 'integer' for integer variables",
    )
    lower: int = Field(
        default=0,
        description="Lower bound (inclusive) for integer variables; ignored for bool variables",
    )
    upper: int = Field(
        default=1,
        description="Upper bound (inclusive) for integer variables; ignored for bool variables",
    )


# ============================================================================
# Variable Models
# ============================================================================


class Variable(BaseModel):
    """Decision variable in the constraint model.

    Each variable represents a choice to be made by the solver.
    """

    id: str = Field(
        ...,
        description="Unique identifier for this variable",
        min_length=1,
    )
    domain: VariableDomain = Field(
        ...,
        description="Domain specification (type and bounds)",
    )
    metadata: dict[str, Any] | None = Field(
        default=None,
        description="Optional metadata for explanations and context; echoed in solutions",
    )


# ============================================================================
# Constraint Parameter Models
# ============================================================================


class LinearTerm(BaseModel):
    """A term in a linear expression: coefficient * variable."""

    var: str = Field(
        ...,
        description="Variable identifier",
        min_length=1,
    )
    coef: float = Field(
        ...,
        description="Coefficient multiplying the variable",
    )


class LinearConstraintParams(BaseModel):
    """Parameters for a linear constraint: sum(terms) sense rhs."""

    terms: list[LinearTerm] = Field(
        ...,
        description="List of linear terms forming the left-hand side",
        min_length=1,
    )
    sense: ConstraintSense = Field(
        ...,
        description="Comparison operator: '<=', '>=', or '=='",
    )
    rhs: float = Field(
        ...,
        description="Right-hand side constant value",
    )


class AllDifferentParams(BaseModel):
    """Parameters for an all-different constraint."""

    vars: list[str] = Field(
        ...,
        description="List of variable identifiers that must all take different values",
        min_length=2,
    )


class ElementParams(BaseModel):
    """Parameters for an element constraint: target = array[index]."""

    index_var: str = Field(
        ...,
        description="Integer variable used as index into the array",
        min_length=1,
    )
    array: list[int] = Field(
        ...,
        description="Constant integer array to index into",
        min_length=1,
    )
    target_var: str = Field(
        ...,
        description="Variable that equals array[index_var]",
        min_length=1,
    )


class TableParams(BaseModel):
    """Parameters for a table constraint: allowed tuples."""

    vars: list[str] = Field(
        ...,
        description="List of variable identifiers forming the tuple",
        min_length=1,
    )
    allowed_tuples: list[list[int]] = Field(
        ...,
        description="List of allowed integer tuples for the variables",
        min_length=1,
    )


class CumulativeParams(BaseModel):
    """Parameters for a cumulative constraint: resource capacity over time."""

    start_vars: list[str] = Field(
        ...,
        description="List of start time variable identifiers",
        min_length=1,
    )
    duration_vars: list[str] | list[int] = Field(
        ...,
        description="List of duration variable identifiers or constant durations",
        min_length=1,
    )
    demand_vars: list[str] | list[int] = Field(
        ...,
        description="List of demand variable identifiers or constant demands",
        min_length=1,
    )
    capacity: int = Field(
        ...,
        description="Maximum cumulative resource capacity",
        ge=0,
    )


class CircuitParams(BaseModel):
    """Parameters for a circuit constraint: routing/tour problem."""

    arcs: list[tuple[int, int, str]] = Field(
        ...,
        description="List of (from_node, to_node, arc_var) tuples forming possible connections",
        min_length=1,
    )


class ReservoirParams(BaseModel):
    """Parameters for a reservoir constraint: inventory/stock management."""

    time_vars: list[str] = Field(
        ...,
        description="List of time variable identifiers when events occur",
        min_length=1,
    )
    level_changes: list[int] = Field(
        ...,
        description="Change in level at each time point (positive=production, negative=consumption)",
        min_length=1,
    )
    min_level: int = Field(
        default=0,
        description="Minimum reservoir level (default 0)",
    )
    max_level: int = Field(
        ...,
        description="Maximum reservoir level (capacity)",
        ge=0,
    )


class NoOverlapParams(BaseModel):
    """Parameters for a no-overlap constraint: disjunctive scheduling."""

    start_vars: list[str] = Field(
        ...,
        description="List of start time variable identifiers",
        min_length=1,
    )
    duration_vars: list[str] | list[int] = Field(
        ...,
        description="List of duration variable identifiers or constant durations",
        min_length=1,
    )


class ImplicationParams(BaseModel):
    """Parameters for an implication constraint: if bool_var then nested_constraint."""

    if_var: str = Field(
        ...,
        description="Boolean variable: when true, the nested constraint must hold",
        min_length=1,
    )
    then: Constraint = Field(
        ...,
        description="Nested constraint that becomes active when if_var is true",
    )


# ============================================================================
# Constraint Models
# ============================================================================


class Constraint(BaseModel):
    """Constraint in the constraint model.

    Each constraint restricts the values that variables can take.
    """

    id: str = Field(
        ...,
        description="Unique identifier for this constraint",
        min_length=1,
    )
    kind: ConstraintKind = Field(
        ...,
        description="Type of constraint: linear, all_different, element, table, implication, cumulative, circuit, reservoir, or no_overlap",
    )
    params: (
        LinearConstraintParams
        | AllDifferentParams
        | ElementParams
        | TableParams
        | ImplicationParams
        | CumulativeParams
        | CircuitParams
        | ReservoirParams
        | NoOverlapParams
    ) = Field(
        ...,
        description="Parameters specific to the constraint kind",
    )
    metadata: dict[str, Any] | None = Field(
        default=None,
        description="Optional metadata for explanations (e.g., human description)",
    )


# ============================================================================
# Objective Models
# ============================================================================


class Objective(BaseModel):
    """Objective function for optimization.

    Defines a linear objective to minimize or maximize.
    Supports multi-objective via lexicographic ordering.
    """

    sense: ObjectiveSense = Field(
        ...,
        description="Optimization direction: 'min' to minimize, 'max' to maximize",
    )
    terms: list[LinearTerm] = Field(
        ...,
        description="Linear terms forming the objective function",
        min_length=1,
    )
    priority: int = Field(
        default=1,
        description="Priority level for lexicographic multi-objective (higher = more important)",
        ge=1,
    )
    weight: float = Field(
        default=1.0,
        description="Weight for weighted-sum multi-objective optimization",
        gt=0.0,
    )
    metadata: dict[str, Any] | None = Field(
        default=None,
        description="Optional explanation/label for the objective",
    )


# ============================================================================
# Search Configuration
# ============================================================================


class SearchStrategy(str, Enum):
    """Search strategy hint for the solver."""

    AUTO = "auto"  # Let solver decide
    FIRST_FAIL = "first_fail"  # Choose variable with smallest domain first
    LARGEST_FIRST = "largest_first"  # Choose variable with largest domain first
    RANDOM = "random"  # Random variable selection
    CHEAPEST_FIRST = "cheapest_first"  # Choose variable with smallest impact


class SearchConfig(BaseModel):
    """Solver search configuration and limits."""

    max_time_ms: int | None = Field(
        default=None,
        description="Maximum solver time in milliseconds",
        ge=1,
    )
    max_solutions: int = Field(
        default=1,
        description="Maximum number of solutions to return",
        ge=1,
    )
    num_search_workers: int | None = Field(
        default=None,
        description="Number of parallel search workers (default: auto)",
        ge=1,
    )
    log_search_progress: bool = Field(
        default=False,
        description="Enable search progress logging",
    )
    warm_start_solution: dict[str, int] | None = Field(
        default=None,
        description="Optional warm-start solution hint (variable_id -> value mapping)",
    )
    random_seed: int | None = Field(
        default=None,
        description="Random seed for deterministic solving (enables reproducible results)",
        ge=0,
    )
    strategy: SearchStrategy = Field(
        default=SearchStrategy.AUTO,
        description="Search strategy hint for variable selection",
    )
    return_partial_solution: bool = Field(
        default=False,
        description="Return best solution found so far if timeout is reached",
    )
    enable_solution_caching: bool = Field(
        default=True,
        description="Cache solutions to avoid re-solving identical problems",
    )


# ============================================================================
# Request Model
# ============================================================================


class SolveConstraintModelRequest(BaseModel):
    """Request to solve a constraint/optimization model.

    This is the main input to the solve_constraint_model tool.
    """

    mode: SolverMode = Field(
        ...,
        description="Solver mode: 'satisfy' to find any feasible solution, 'optimize' to find optimal solution",
    )
    variables: list[Variable] = Field(
        ...,
        description="List of decision variables",
        min_length=1,
    )
    constraints: list[Constraint] = Field(
        ...,
        description="List of constraints",
    )
    objective: Objective | list[Objective] | None = Field(
        default=None,
        description="Objective function(s); required when mode is 'optimize'. Can be single objective or list for multi-objective optimization",
    )
    search: SearchConfig | None = Field(
        default=None,
        description="Optional solver search/limits configuration",
    )


# ============================================================================
# Solution Models
# ============================================================================


class SolutionVariable(BaseModel):
    """Variable value in a solution."""

    id: str = Field(
        ...,
        description="Variable identifier",
    )
    value: float = Field(
        ...,
        description="Assigned value in this solution",
    )
    metadata: dict[str, Any] | None = Field(
        default=None,
        description="Original metadata from the variable definition",
    )


class Solution(BaseModel):
    """A single solution to the constraint model."""

    variables: list[SolutionVariable] = Field(
        ...,
        description="Variable assignments in this solution",
    )
    derived: dict[str, Any] | None = Field(
        default=None,
        description="Optional derived metrics computed from the solution (e.g., makespan, counts)",
    )


class BindingConstraint(BaseModel):
    """Information about a constraint that is tight/critical in the solution."""

    id: str = Field(
        ...,
        description="Constraint identifier",
    )
    sense: ConstraintSense | None = Field(
        default=None,
        description="Constraint sense (for linear constraints)",
    )
    lhs_value: float = Field(
        ...,
        description="Evaluated left-hand side under the solution",
    )
    rhs: float = Field(
        ...,
        description="Right-hand side value",
    )
    metadata: dict[str, Any] | None = Field(
        default=None,
        description="Optional constraint metadata",
    )


class Explanation(BaseModel):
    """Human-readable explanation of the solution."""

    summary: str = Field(
        ...,
        description="High-level textual summary of the result",
    )
    binding_constraints: list[BindingConstraint] = Field(
        default_factory=list,
        description="Constraints that are tight/critical in the solution",
    )


# ============================================================================
# Response Model
# ============================================================================


class SolveConstraintModelResponse(BaseModel):
    """Response from solving a constraint/optimization model."""

    apiversion: str = Field(
        default=API_VERSION,
        description="API version",
    )
    status: SolverStatus = Field(
        ...,
        description="Solution status: optimal, feasible, satisfied, infeasible, unbounded, timeout_best, timeout_no_solution, or error",
    )
    objective_value: float | None = Field(
        default=None,
        description="Objective value for the best solution, if applicable",
    )
    optimality_gap: float | None = Field(
        default=None,
        description="Optimality gap as percentage (0-100) from best bound; only for optimization problems. Lower is better, 0 means proven optimal",
    )
    solve_time_ms: int = Field(
        default=0,
        description="Actual wall-clock solve time in milliseconds",
        ge=0,
    )
    solutions: list[Solution] = Field(
        default_factory=list,
        description="List of solutions; usually length 1 unless max_solutions > 1",
    )
    explanation: Explanation | None = Field(
        default=None,
        description="Optional human-readable explanation of the result",
    )


# ============================================================================
# High-Level Scheduling Models (Phase 4)
# ============================================================================


class Task(BaseModel):
    """A task to be scheduled with duration, dependencies, and resource requirements."""

    id: str = Field(
        ...,
        description="Unique task identifier",
        min_length=1,
    )
    duration: int = Field(
        ...,
        description="Task duration in time units (hours, minutes, etc.)",
        ge=0,
    )
    resources_required: dict[str, int] = Field(
        default_factory=dict,
        description="Resource requirements as {resource_id: amount}",
    )
    dependencies: list[str] = Field(
        default_factory=list,
        description="List of task IDs that must complete before this task starts",
    )
    earliest_start: int | None = Field(
        default=None,
        description="Optional earliest start time (release time)",
        ge=0,
    )
    deadline: int | None = Field(
        default=None,
        description="Optional deadline (due date)",
        ge=0,
    )
    priority: int = Field(
        default=1,
        description="Task priority (higher = more important)",
        ge=1,
    )
    metadata: dict[str, Any] = Field(
        default_factory=dict,
        description="Optional metadata for explanations",
    )


class Resource(BaseModel):
    """A resource with capacity constraints."""

    id: str = Field(
        ...,
        description="Unique resource identifier",
        min_length=1,
    )
    capacity: int = Field(
        ...,
        description="Maximum units available at any time",
        ge=0,
    )
    cost_per_unit: float = Field(
        default=0.0,
        description="Optional cost per unit-time for cost optimization",
        ge=0.0,
    )
    metadata: dict[str, Any] = Field(
        default_factory=dict,
        description="Optional metadata",
    )


class SchedulingObjective(str, Enum):
    """Scheduling optimization objectives."""

    MINIMIZE_MAKESPAN = "minimize_makespan"  # Finish ASAP
    MINIMIZE_COST = "minimize_cost"  # Minimize resource costs
    MINIMIZE_LATENESS = "minimize_lateness"  # Minimize deadline violations


class SolveSchedulingProblemRequest(BaseModel):
    """High-level scheduling problem definition.

    Use this instead of solve_constraint_model when you have tasks with
    durations, dependencies, and resource constraints. The solver will
    automatically build the appropriate CP-SAT model.
    """

    tasks: list[Task] = Field(
        ...,
        description="List of tasks to schedule",
        min_length=1,
    )
    resources: list[Resource] = Field(
        default_factory=list,
        description="Optional list of resources with capacity constraints",
    )
    objective: SchedulingObjective = Field(
        default=SchedulingObjective.MINIMIZE_MAKESPAN,
        description="Optimization objective",
    )
    max_makespan: int | None = Field(
        default=None,
        description="Optional hard deadline for project completion",
        ge=1,
    )
    no_overlap_tasks: list[list[str]] = Field(
        default_factory=list,
        description="Optional groups of tasks that cannot run concurrently",
    )
    max_time_ms: int = Field(
        default=60000,
        description="Maximum solver time in milliseconds",
        ge=1,
    )
    return_partial_solution: bool = Field(
        default=True,
        description="Return best solution found if timeout occurs",
    )
    metadata: dict[str, Any] = Field(
        default_factory=dict,
        description="Optional metadata",
    )


class TaskAssignment(BaseModel):
    """A scheduled task with timing information."""

    task_id: str = Field(
        ...,
        description="Task identifier",
    )
    start_time: int = Field(
        ...,
        description="Task start time",
        ge=0,
    )
    end_time: int = Field(
        ...,
        description="Task end time",
        ge=0,
    )
    resources_used: dict[str, int] = Field(
        default_factory=dict,
        description="Resources allocated to this task",
    )
    on_critical_path: bool = Field(
        default=False,
        description="Whether this task is on the critical path",
    )
    slack: int = Field(
        default=0,
        description="Slack time (how much task can be delayed without affecting makespan)",
        ge=0,
    )
    metadata: dict[str, Any] = Field(
        default_factory=dict,
        description="Original task metadata",
    )


class ResourceUtilization(BaseModel):
    """Resource usage over time."""

    resource_id: str = Field(
        ...,
        description="Resource identifier",
    )
    peak_usage: int = Field(
        ...,
        description="Peak utilization",
        ge=0,
    )
    avg_usage: float = Field(
        ...,
        description="Average utilization",
        ge=0.0,
    )
    capacity: int = Field(
        ...,
        description="Resource capacity",
        ge=0,
    )
    total_cost: float = Field(
        default=0.0,
        description="Total cost (if cost_per_unit defined)",
        ge=0.0,
    )


class SchedulingExplanation(BaseModel):
    """Human-readable explanation of the schedule."""

    summary: str = Field(
        ...,
        description="High-level summary of the result",
    )
    bottlenecks: list[str] = Field(
        default_factory=list,
        description="Identified bottlenecks (e.g., 'Task Deploy limited by available GPUs')",
    )
    recommendations: list[str] = Field(
        default_factory=list,
        description="Suggestions for improvement",
    )
    binding_constraints: list[str] = Field(
        default_factory=list,
        description="Constraints that are tight/critical",
    )


class SolveSchedulingProblemResponse(BaseModel):
    """Scheduling solution with domain-specific details."""

    status: SolverStatus = Field(
        ...,
        description="Solution status",
    )
    makespan: int | None = Field(
        default=None,
        description="Project completion time (max end time of all tasks)",
        ge=0,
    )
    total_cost: float | None = Field(
        default=None,
        description="Total resource cost if applicable",
        ge=0.0,
    )
    schedule: list[TaskAssignment] = Field(
        default_factory=list,
        description="Scheduled tasks with timing",
    )
    resource_utilization: list[ResourceUtilization] = Field(
        default_factory=list,
        description="Resource usage summary",
    )
    critical_path: list[str] = Field(
        default_factory=list,
        description="Task IDs on the critical path",
    )
    solve_time_ms: int = Field(
        default=0,
        description="Actual solve time in milliseconds",
        ge=0,
    )
    optimality_gap: float | None = Field(
        default=None,
        description="Optimality gap percentage",
    )
    explanation: SchedulingExplanation = Field(
        ...,
        description="Human-readable explanation",
    )


# ============================================================================
# Routing Models (Phase 4.1.2)
# ============================================================================


class Location(BaseModel):
    """A location to visit in a routing problem."""

    id: str = Field(
        ...,
        description="Unique location identifier",
        min_length=1,
    )
    coordinates: tuple[float, float] | None = Field(
        default=None,
        description="Optional (latitude, longitude) or (x, y) coordinates",
    )
    service_time: int = Field(
        default=0,
        description="Time spent servicing this location",
        ge=0,
    )
    time_window: tuple[int, int] | None = Field(
        default=None,
        description="Optional (earliest, latest) arrival time window",
    )
    demand: int = Field(
        default=0,
        description="Demand at this location (for capacity-constrained routing)",
        ge=0,
    )
    priority: int = Field(
        default=1,
        description="Location priority (higher = more important)",
        ge=0,
    )
    metadata: dict[str, Any] = Field(
        default_factory=dict,
        description="Optional metadata",
    )


class Vehicle(BaseModel):
    """A vehicle for routing problems."""

    id: str = Field(
        ...,
        description="Unique vehicle identifier",
        min_length=1,
    )
    capacity: int = Field(
        default=999999,
        description="Maximum load capacity",
        ge=0,
    )
    start_location: str = Field(
        ...,
        description="Starting location ID",
        min_length=1,
    )
    end_location: str | None = Field(
        default=None,
        description="Ending location ID (if different from start for open tours)",
    )
    max_distance: int | None = Field(
        default=None,
        description="Maximum distance this vehicle can travel",
        ge=0,
    )
    max_time: int | None = Field(
        default=None,
        description="Maximum time this vehicle can be in use",
        ge=0,
    )
    cost_per_distance: float = Field(
        default=1.0,
        description="Cost per unit distance",
        ge=0.0,
    )
    fixed_cost: float = Field(
        default=0.0,
        description="Fixed cost if vehicle is used",
        ge=0.0,
    )
    metadata: dict[str, Any] = Field(
        default_factory=dict,
        description="Optional metadata",
    )


class RoutingObjective(str, Enum):
    """Routing optimization objectives."""

    MINIMIZE_DISTANCE = "minimize_distance"  # Minimize total distance
    MINIMIZE_TIME = "minimize_time"  # Minimize total time
    MINIMIZE_VEHICLES = "minimize_vehicles"  # Use fewest vehicles
    MINIMIZE_COST = "minimize_cost"  # Minimize total cost


class SolveRoutingProblemRequest(BaseModel):
    """High-level routing problem definition.

    Use this for TSP (Traveling Salesman Problem), VRP (Vehicle Routing Problem),
    and delivery route optimization. The solver will automatically build the
    appropriate CP-SAT model using circuit constraints.
    """

    locations: list[Location] = Field(
        ...,
        description="List of locations to visit",
        min_length=2,
    )
    vehicles: list[Vehicle] = Field(
        default_factory=list,
        description="List of vehicles (if empty, assumes single vehicle TSP)",
    )
    distance_matrix: list[list[int]] | None = Field(
        default=None,
        description="Distance matrix where [i][j] = distance from location i to j. If not provided, uses Euclidean distance from coordinates.",
    )
    objective: RoutingObjective = Field(
        default=RoutingObjective.MINIMIZE_DISTANCE,
        description="Optimization objective",
    )
    force_visit_all: bool = Field(
        default=True,
        description="If True, all locations must be visited. If False, some can be skipped.",
    )
    max_route_distance: int | None = Field(
        default=None,
        description="Optional maximum distance for any single route",
        ge=0,
    )
    max_time_ms: int = Field(
        default=60000,
        description="Maximum solver time in milliseconds",
        ge=1,
    )
    return_partial_solution: bool = Field(
        default=True,
        description="Return best solution found if timeout occurs",
    )
    metadata: dict[str, Any] = Field(
        default_factory=dict,
        description="Optional metadata",
    )


class Route(BaseModel):
    """A vehicle route through locations."""

    vehicle_id: str = Field(
        ...,
        description="Vehicle identifier",
    )
    sequence: list[str] = Field(
        ...,
        description="Location IDs in visit order",
    )
    total_distance: int = Field(
        ...,
        description="Total route distance",
        ge=0,
    )
    total_time: int = Field(
        ...,
        description="Total route time including service times",
        ge=0,
    )
    total_cost: float = Field(
        ...,
        description="Total route cost",
        ge=0.0,
    )
    load_timeline: list[tuple[str, int]] = Field(
        default_factory=list,
        description="Load after visiting each location: [(location_id, load), ...]",
    )


class RoutingExplanation(BaseModel):
    """Human-readable explanation of the routing solution."""

    summary: str = Field(
        ...,
        description="High-level summary of the result",
    )
    bottlenecks: list[str] = Field(
        default_factory=list,
        description="Identified bottlenecks (e.g., 'Vehicle capacity constraint forces 2 trips')",
    )
    recommendations: list[str] = Field(
        default_factory=list,
        description="Suggestions for improvement",
    )


class SolveRoutingProblemResponse(BaseModel):
    """Routing solution with domain-specific details."""

    status: SolverStatus = Field(
        ...,
        description="Solution status",
    )
    routes: list[Route] = Field(
        default_factory=list,
        description="Vehicle routes",
    )
    unvisited: list[str] = Field(
        default_factory=list,
        description="Location IDs not visited (if force_visit_all=False)",
    )
    total_distance: int = Field(
        default=0,
        description="Total distance across all routes",
        ge=0,
    )
    total_time: int = Field(
        default=0,
        description="Total time across all routes",
        ge=0,
    )
    total_cost: float = Field(
        default=0.0,
        description="Total cost across all routes",
        ge=0.0,
    )
    vehicles_used: int = Field(
        default=0,
        description="Number of vehicles actually used",
        ge=0,
    )
    solve_time_ms: int = Field(
        default=0,
        description="Actual solve time in milliseconds",
        ge=0,
    )
    optimality_gap: float | None = Field(
        default=None,
        description="Optimality gap percentage",
    )
    explanation: RoutingExplanation = Field(
        ...,
        description="Human-readable explanation",
    )


# ============================================================================
# Budget Allocation Models (Phase 4.1.3)
# ============================================================================


class Item(BaseModel):
    """An item to potentially select in budget allocation."""

    id: str = Field(
        ...,
        description="Unique item identifier",
    )
    cost: float = Field(
        ...,
        description="Cost of selecting this item",
        ge=0.0,
    )
    value: float = Field(
        ...,
        description="Value/benefit of selecting this item (ROI, utility, priority score)",
        ge=0.0,
    )
    resources_required: dict[str, float] = Field(
        default_factory=dict,
        description="Additional resources required (e.g., {'headcount': 2.5, 'servers': 1})",
    )
    dependencies: list[str] = Field(
        default_factory=list,
        description="Item IDs that must also be selected if this item is selected",
    )
    conflicts: list[str] = Field(
        default_factory=list,
        description="Item IDs that cannot be selected together with this item",
    )
    metadata: dict[str, Any] = Field(
        default_factory=dict,
        description="Additional context for explanations",
    )


class BudgetConstraint(BaseModel):
    """A budget or resource limit."""

    resource: str = Field(
        ...,
        description="Resource name (e.g., 'money', 'time', 'headcount')",
    )
    limit: float = Field(
        ...,
        description="Maximum amount available",
        ge=0.0,
    )
    penalty_per_unit_over: float = Field(
        default=0.0,
        description="Penalty for exceeding limit (0 = hard constraint)",
        ge=0.0,
    )


class AllocationObjective(str, Enum):
    """Budget allocation optimization objectives."""

    MAXIMIZE_VALUE = "maximize_value"
    MAXIMIZE_COUNT = "maximize_count"  # Select as many items as possible
    MINIMIZE_COST = "minimize_cost"  # While meeting value threshold


class SolveBudgetAllocationRequest(BaseModel):
    """High-level budget allocation / knapsack problem definition."""

    items: list[Item] = Field(
        ...,
        min_length=1,
        description="Items to choose from",
    )
    budgets: list[BudgetConstraint] = Field(
        ...,
        min_length=1,
        description="Budget and resource constraints",
    )
    objective: AllocationObjective = Field(
        default=AllocationObjective.MAXIMIZE_VALUE,
        description="Optimization goal",
    )
    min_value_threshold: float | None = Field(
        default=None,
        description="Minimum total value required (optional constraint)",
        ge=0.0,
    )
    max_cost_threshold: float | None = Field(
        default=None,
        description="Maximum total cost allowed (optional constraint)",
        ge=0.0,
    )
    min_items: int | None = Field(
        default=None,
        description="Minimum number of items to select",
        ge=0,
    )
    max_items: int | None = Field(
        default=None,
        description="Maximum number of items to select",
        ge=0,
    )
    max_time_ms: int = Field(
        default=60000,
        description="Maximum solver time in milliseconds",
        ge=1,
    )
    return_partial_solution: bool = Field(
        default=True,
        description="Return best solution found if timeout occurs",
    )
    metadata: dict[str, Any] = Field(
        default_factory=dict,
        description="Additional context",
    )


class AllocationExplanation(BaseModel):
    """Human-readable explanation of the allocation solution."""

    summary: str = Field(
        ...,
        description="High-level summary of the result",
    )
    binding_constraints: list[str] = Field(
        default_factory=list,
        description="Constraints that are fully utilized",
    )
    marginal_items: list[dict[str, Any]] = Field(
        default_factory=list,
        description="Items almost selected but excluded",
    )
    recommendations: list[str] = Field(
        default_factory=list,
        description="Suggestions for improvement",
    )


class SolveBudgetAllocationResponse(BaseModel):
    """Budget allocation solution with domain-specific details."""

    status: SolverStatus = Field(
        ...,
        description="Solution status",
    )
    selected_items: list[str] = Field(
        default_factory=list,
        description="IDs of selected items",
    )
    total_cost: float = Field(
        default=0.0,
        description="Total cost of selected items",
        ge=0.0,
    )
    total_value: float = Field(
        default=0.0,
        description="Total value of selected items",
        ge=0.0,
    )
    resource_usage: dict[str, float] = Field(
        default_factory=dict,
        description="Resource consumption by resource name",
    )
    resource_slack: dict[str, float] = Field(
        default_factory=dict,
        description="Unused capacity by resource name",
    )
    solve_time_ms: int = Field(
        default=0,
        description="Actual solve time in milliseconds",
        ge=0,
    )
    optimality_gap: float | None = Field(
        default=None,
        description="Optimality gap percentage",
    )
    explanation: AllocationExplanation = Field(
        ...,
        description="Human-readable explanation",
    )


# ============================================================================
# Assignment Models (Phase 4.1.4)
# ============================================================================


class Agent(BaseModel):
    """An agent that can be assigned tasks."""

    id: str = Field(
        ...,
        description="Unique agent identifier",
    )
    capacity: int = Field(
        default=1,
        description="Maximum number of tasks this agent can handle",
        ge=0,
    )
    skills: list[str] = Field(
        default_factory=list,
        description="Skills this agent possesses",
    )
    cost_multiplier: float = Field(
        default=1.0,
        description="Cost multiplier for this agent",
        ge=0.0,
    )
    metadata: dict[str, Any] = Field(
        default_factory=dict,
        description="Additional context for explanations",
    )


class AssignmentTask(BaseModel):
    """A task to be assigned to an agent."""

    id: str = Field(
        ...,
        description="Unique task identifier",
    )
    required_skills: list[str] = Field(
        default_factory=list,
        description="Skills required to perform this task",
    )
    duration: int = Field(
        default=1,
        description="Task duration or workload",
        ge=0,
    )
    priority: int = Field(
        default=1,
        description="Task priority (higher = more important)",
        ge=0,
    )
    metadata: dict[str, Any] = Field(
        default_factory=dict,
        description="Additional context for explanations",
    )


class AssignmentObjective(str, Enum):
    """Assignment optimization objectives."""

    MINIMIZE_COST = "minimize_cost"
    MAXIMIZE_ASSIGNMENTS = "maximize_assignments"  # Assign as many tasks as possible
    BALANCE_LOAD = "balance_load"  # Distribute tasks evenly across agents


class SolveAssignmentProblemRequest(BaseModel):
    """High-level assignment problem definition."""

    agents: list[Agent] = Field(
        ...,
        min_length=1,
        description="Agents available to perform tasks",
    )
    tasks: list[AssignmentTask] = Field(
        ...,
        min_length=1,
        description="Tasks to be assigned",
    )
    cost_matrix: list[list[float]] | None = Field(
        default=None,
        description="Cost matrix where [i][j] = cost to assign task i to agent j. "
        "If not provided, uses agent.cost_multiplier * task.duration",
    )
    objective: AssignmentObjective = Field(
        default=AssignmentObjective.MINIMIZE_COST,
        description="Optimization goal",
    )
    force_assign_all: bool = Field(
        default=True,
        description="If True, all tasks must be assigned (problem is infeasible if not possible). "
        "If False, some tasks can remain unassigned.",
    )
    max_time_ms: int = Field(
        default=60000,
        description="Maximum solver time in milliseconds",
        ge=1,
    )
    return_partial_solution: bool = Field(
        default=True,
        description="Return best solution found if timeout occurs",
    )
    metadata: dict[str, Any] = Field(
        default_factory=dict,
        description="Additional context",
    )


class Assignment(BaseModel):
    """A task assigned to an agent."""

    task_id: str = Field(
        ...,
        description="ID of the assigned task",
    )
    agent_id: str = Field(
        ...,
        description="ID of the agent assigned to the task",
    )
    cost: float = Field(
        ...,
        description="Cost of this assignment",
        ge=0.0,
    )


class AssignmentExplanation(BaseModel):
    """Human-readable explanation of the assignment solution."""

    summary: str = Field(
        ...,
        description="High-level summary of the result",
    )
    overloaded_agents: list[str] = Field(
        default_factory=list,
        description="Agents assigned more tasks than typical",
    )
    underutilized_agents: list[str] = Field(
        default_factory=list,
        description="Agents with capacity but few/no assignments",
    )
    recommendations: list[str] = Field(
        default_factory=list,
        description="Suggestions for improvement",
    )


class SolveAssignmentProblemResponse(BaseModel):
    """Assignment solution with domain-specific details."""

    status: SolverStatus = Field(
        ...,
        description="Solution status",
    )
    assignments: list[Assignment] = Field(
        default_factory=list,
        description="Task-to-agent assignments",
    )
    unassigned_tasks: list[str] = Field(
        default_factory=list,
        description="Tasks that could not be assigned",
    )
    agent_load: dict[str, int] = Field(
        default_factory=dict,
        description="Number of tasks assigned to each agent",
    )
    total_cost: float = Field(
        default=0.0,
        description="Total cost of all assignments",
        ge=0.0,
    )
    solve_time_ms: int = Field(
        default=0,
        description="Actual solve time in milliseconds",
        ge=0,
    )
    optimality_gap: float | None = Field(
        default=None,
        description="Optimality gap percentage",
    )
    explanation: AssignmentExplanation = Field(
        ...,
        description="Human-readable explanation",
    )
