#!/usr/bin/env python3
"""
CP-SAT Deterministic Scheduling Solver
Phase 3 of Spec 038

Reads JSON problem from stdin, solves with OR-Tools CP-SAT, writes JSON result to stdout.
"""
import sys
import json
import time
from typing import Dict, Any, Optional
from ortools.sat.python import cp_model

from model_builder import build_model
from result_formatter import format_result
from infeasibility_analyzer import analyze_infeasibility


def solve_schedule(problem_data: Dict[str, Any]) -> Dict[str, Any]:
    """
    Solve scheduling problem using CP-SAT.

    Args:
        problem_data: Problem input JSON

    Returns:
        Result JSON with status, assignments, and metrics
    """
    config = problem_data.get("config", {})
    timeout_seconds = config.get("timeout_seconds", 60)
    random_seed = config.get("random_seed", 42)

    # Build model
    model, x = build_model(problem_data)

    # Create solver
    solver = cp_model.CpSolver()

    # CRITICAL: num_search_workers=1 for determinism
    solver.parameters.num_search_workers = 1
    solver.parameters.random_seed = random_seed
    solver.parameters.max_time_in_seconds = timeout_seconds
    solver.parameters.log_search_progress = False  # Don't pollute stdout

    # Solve
    start_time = time.time()
    status = solver.Solve(model)
    end_time = time.time()

    solver_duration_ms = int((end_time - start_time) * 1000)

    # Extract assignments
    assignments: Dict[int, int] = {}

    if status in [cp_model.OPTIMAL, cp_model.FEASIBLE]:
        # Extract variable assignments
        for (emp_id, shift_id), var in x.items():
            if solver.Value(var) == 1:
                assignments[shift_id] = emp_id

    # Handle infeasibility
    infeasible_subset: Optional[Dict[str, Any]] = None
    if status == cp_model.INFEASIBLE:
        sys.stderr.write("Model is infeasible, analyzing conflicts...\n")
        infeasible_subset = analyze_infeasibility(model, problem_data, timeout_seconds=10)

    # Format result
    result = format_result(
        problem_data,
        solver,
        model,
        assignments,
        solver_duration_ms,
        status,
        infeasible_subset
    )

    return result


def run_self_test() -> int:
    """
    Run self-test with simple problem.

    Returns:
        Exit code (0 = pass, 1 = fail)
    """
    sys.stderr.write("Running self-test...\n")

    # Create simple test problem: 5 shifts, 3 employees
    test_problem = {
        "config": {
            "timeout_seconds": 30,
            "random_seed": 42,
            "priorities": [
                {"name": "position_coverage", "rank": 1, "weight": 100000},
                {"name": "labor_cost", "rank": 2, "weight": 10000},
                {"name": "hours_fairness", "rank": 3, "weight": 1000},
                {"name": "seniority", "rank": 4, "weight": 100},
                {"name": "minimize_overtime", "rank": 5, "weight": 10},
                {"name": "employee_preferences", "rank": 6, "weight": 1}
            ]
        },
        "shifts": [
            {
                "shiftId": i,
                "date": f"2026-02-{16+i}",
                "dayOfWeek": 1 + i,
                "startTime": "09:00",
                "endTime": "17:00",
                "durationHours": 8.0,
                "positionId": 1,
                "positionName": "Sales",
                "minRoleId": 4,
                "isLocked": False
            }
            for i in range(5)
        ],
        "employees": [
            {
                "userId": j,
                "name": f"Employee_{j}",
                "role": 4,
                "hourlyRate": 15.0 + j,
                "hoursRequested": 32.0,
                "hoursMin": 20.0,
                "hoursMax": 40.0,
                "currentPeriodHours": 0.0,
                "shiftsThisWeek": 0,
                "availability": [
                    {"dayOfWeek": dow, "startTime": "08:00", "endTime": "22:00"}
                    for dow in range(1, 8)
                ],
                "timeOff": []
            }
            for j in range(1, 4)
        ],
        "lockedShifts": []
    }

    # Solve
    result = solve_schedule(test_problem)

    # Verify
    errors = []

    if result["status"] not in ["OPTIMAL", "FEASIBLE"]:
        errors.append(f"Expected OPTIMAL or FEASIBLE, got {result['status']}")

    if result["status"] in ["OPTIMAL", "FEASIBLE"]:
        # Check all assignments are valid
        assignments = result["assignments"]

        # C1: Each shift assigned at most once
        assigned_shifts = set()
        for assign in assignments:
            shift_id = assign["shiftId"]
            if shift_id in assigned_shifts:
                errors.append(f"Shift {shift_id} assigned multiple times")
            assigned_shifts.add(shift_id)

        # C7: hoursMax not exceeded
        hours_by_employee = {emp["userId"]: emp["currentPeriodHours"] for emp in test_problem["employees"]}
        shift_lookup = {s["shiftId"]: s for s in test_problem["shifts"]}

        for assign in assignments:
            emp_id = assign["employeeId"]
            shift = shift_lookup[assign["shiftId"]]
            hours_by_employee[emp_id] += shift["durationHours"]

        for emp in test_problem["employees"]:
            emp_id = emp["userId"]
            if hours_by_employee[emp_id] > emp["hoursMax"] + 0.01:  # Small tolerance
                errors.append(
                    f"Employee {emp_id} exceeds hoursMax: "
                    f"{hours_by_employee[emp_id]:.2f} > {emp['hoursMax']}"
                )

        # C3: Role qualification
        emp_lookup = {emp["userId"]: emp for emp in test_problem["employees"]}
        for assign in assignments:
            emp = emp_lookup[assign["employeeId"]]
            shift = shift_lookup[assign["shiftId"]]
            if emp["role"] > shift["minRoleId"]:
                errors.append(
                    f"Employee {emp['userId']} (role {emp['role']}) "
                    f"assigned to shift {shift['shiftId']} requiring role <= {shift['minRoleId']}"
                )

        # Coverage should be good (at least 3 of 5 shifts filled)
        if len(assignments) < 3:
            errors.append(f"Poor coverage: only {len(assignments)}/5 shifts filled")

    if errors:
        sys.stderr.write("Self-test FAILED:\n")
        for error in errors:
            sys.stderr.write(f"  - {error}\n")
        return 1
    else:
        sys.stderr.write("Self-test PASSED ✓\n")
        return 0


def main() -> int:
    """Main entry point."""
    # Check for --test flag
    if len(sys.argv) > 1 and sys.argv[1] == "--test":
        return run_self_test()

    # Read problem from stdin
    try:
        problem_data = json.load(sys.stdin)
    except json.JSONDecodeError as e:
        sys.stderr.write(f"Error parsing input JSON: {e}\n")
        return 1

    # Solve
    try:
        result = solve_schedule(problem_data)
    except Exception as e:
        sys.stderr.write(f"Error solving schedule: {e}\n")
        import traceback
        traceback.print_exc(file=sys.stderr)
        return 1

    # Write result to stdout
    try:
        json.dump(result, sys.stdout, indent=2)
        sys.stdout.write("\n")
    except Exception as e:
        sys.stderr.write(f"Error writing output JSON: {e}\n")
        return 1

    return 0


if __name__ == "__main__":
    sys.exit(main())
