import sys

import numpy as np
from braket.device_schema import DeviceCapabilities
from braket.ir.ahs.program_v1 import Program
from braket.task_result.analog_hamiltonian_simulation_task_result_v1 import (
    AnalogHamiltonianSimulationTaskResult,
)
from braket.task_result.task_metadata_v1 import TaskMetadata
from pydantic import create_model  # This is temporary for defining properties below

from braket.analog_hamiltonian_simulator.rydberg.constants import (
    RYDBERG_INTERACTION_COEF,
    SPACE_UNIT,
    TIME_UNIT,
    capabilities_constants,
)
from braket.analog_hamiltonian_simulator.rydberg.numpy_solver import rk_run
from braket.analog_hamiltonian_simulator.rydberg.rydberg_simulator_helpers import (
    get_blockade_configurations,
    sample_state,
)
from braket.analog_hamiltonian_simulator.rydberg.rydberg_simulator_result_converter import (
    convert_result,
)
from braket.analog_hamiltonian_simulator.rydberg.rydberg_simulator_unit_converter import (
    convert_unit,
)
from braket.analog_hamiltonian_simulator.rydberg.scipy_solver import scipy_integrate_ode_run
from braket.analog_hamiltonian_simulator.rydberg.validators.blockade_radius import (
    validate_blockade_radius,
)
from braket.analog_hamiltonian_simulator.rydberg.validators.ir_validator import validate_program
from braket.analog_hamiltonian_simulator.rydberg.validators.rydberg_coefficient import (
    validate_rydberg_interaction_coef,
)
from braket.default_simulator.simulation import Simulation
from braket.default_simulator.simulator import BaseLocalSimulator


class RydbergAtomSimulator(BaseLocalSimulator):
    DEVICE_ID = "braket_ahs"

    def run(
        self,
        program: Program,
        shots: int = 100,
        steps: int = 1000,
        rydberg_interaction_coef: float = RYDBERG_INTERACTION_COEF,
        blockade_radius: float = 0.0,
        progress_bar: bool = False,
        atol: float = 1e-8,
        rtol: float = 1e-6,
        solver_method: str = "adams",
        order: int = 12,
        nsteps: int = 1000,
        first_step: int = 0,
        max_step: int = 0,
        min_step: int = 0,
        *args,
        **kwargs
    ) -> AnalogHamiltonianSimulationTaskResult:
        """
        Run the simulation for the dynamics of the Rydberg system, and return the result

        Args:
            program (Program): An analog simulation Hamiltonian for the Rydberg system simulated
            shots (int): The number of shots. Default: 100
            steps (int): The number of time points for the simulation. Default: 1000
            rydberg_interaction_coef (float): The interaction coefficient. Default: 5.42e-24
            blockade_radius (float): The blockade radius for the Rydberg system. Default: 0.0
            progress_bar (bool): If true, a progress bar will be printed during the simulation.
                Default: False
            atol (float): Absolute tolerance for solution. Default: 1e-8
            rtol (float): Relative tolerance for solution. Default: 1e-6
            solver_method (str): Which solver to use, `adams` for non-stiff problems or `bdf`
                for stiff problems. Default: "adams"
            order (int): Maximum order used by the integrator, order <= 12 for Adams, <= 5 for BDF.
                Default: 12
            nsteps (int): Maximum number of (internally defined) steps allowed during one call to
                the solver. Default: 1000
            first_step (int): Default: 0
            max_step (int): Limits for the step sizes used by the integrator. Default: 0
            min_step (int): Default: 0

        Returns:
            AnalogHamiltonianSimulationTaskResult: The result of the simulation
        """

        # Validate the given program against the capabilities
        validate_program(program, capabilities_constants())

        # Validate the Rydberg interaction coefficients and blockade radius
        self.rydberg_interaction_coef = validate_rydberg_interaction_coef(
            rydberg_interaction_coef
        ) / ((SPACE_UNIT**6) / TIME_UNIT)
        self.blockade_radius = validate_blockade_radius(blockade_radius) / SPACE_UNIT

        # Convert the units of the program from SI unit to microsecond and micrometer etc.
        program = convert_unit(program)

        # Get the duration of the program. Note that there could be 0 or 1 driving field
        # (same for the shifting field) for a given program. Hence we need to checkout
        # the duration for both fields
        self.duration = 0
        if len(program.hamiltonian.drivingFields) == 1:
            self.duration = float(
                program.hamiltonian.drivingFields[0].amplitude.time_series.times[-1]
            )
        elif len(program.hamiltonian.shiftingFields) == 1:
            self.duration = float(
                program.hamiltonian.shiftingFields[0].magnitude.time_series.times[-1]
            )

        if self.duration == 0:
            self.simulation_times = [0]
        else:
            self.simulation_times = np.linspace(0, self.duration, steps)

        # Get valid configurations that comply with the blockade approximation
        self.atomArray = program.setup.ahs_register
        self.configurations = get_blockade_configurations(self.atomArray, self.blockade_radius)

        # Run the solver
        # We shall adaptively change between numpy solver (RK6 method) and scipy solver
        if len(self.configurations) <= 1000:
            states = rk_run(
                program,
                self.configurations,
                self.simulation_times,
                self.rydberg_interaction_coef,
            )
        else:
            states = scipy_integrate_ode_run(
                program,
                self.configurations,
                self.simulation_times,
                self.rydberg_interaction_coef,
                progress_bar=progress_bar,
                atol=atol,
                rtol=rtol,
                solver_method=solver_method,
                order=order,
                nsteps=nsteps,
                first_step=first_step,
                max_step=max_step,
                min_step=min_step,
            )

        # Convert the result type
        if shots == 0:
            raise NotImplementedError("shots=0 is not implemented")
        else:
            dist = sample_state(states[-1], shots)
            return convert_result(
                dist,
                self.atomArray.filling,
                self.configurations,
                self._task_metadata(shots),
            )

    def _task_metadata(self, shots: int) -> TaskMetadata:
        return TaskMetadata(
            id="rydberg",
            shots=shots,
            deviceId="rydbergLocalSimulator",
        )

    @property
    def properties(self) -> DeviceCapabilities:
        """
        Device properties for the RydbergAtomSimulator.

        Returns:
            DeviceCapabilities: Device capabilities for this simulator.
        """
        properties = {
            "service": {
                "executionWindows": [
                    {
                        "executionDay": "Everyday",
                        "windowStartHour": "00:00",
                        "windowEndHour": "23:59:59",
                    }
                ],
                "shotsRange": [0, sys.maxsize],
            },
            "action": {"braket.ir.ahs.program": {}},
        }

        RydbergSimulatorDeviceCapabilities = create_model(
            "RydbergSimulatorDeviceCapabilities", **properties
        )

        return RydbergSimulatorDeviceCapabilities.parse_obj(properties)

    def initialize_simulation(self, **kwargs) -> Simulation:
        """
        Initialize Rydberg Hamiltonian simulation.

        Returns:
            Simulation: Initialized simulation.
        """
        pass
