"""DiscoRL-compatible CartPole environment wrapper.

This module:
1. Wraps standard Gym CartPole in DiscoRL's Environment interface
2. CartPole naturally has discrete actions (0 or 1)
3. Provides flexible observation/action preprocessing

The design supports:
- Simple batch handling (Python-level, non-JAX)
- Discrete action space (required by DiscoRL Agent)
- Standard Gym interface (reset/step)
"""

from typing import Any, Dict, Tuple, Optional
import numpy as np
import jax
import jax.numpy as jnp
import gymnasium as gym

from disco_rl.environments import base
from disco_rl import types

try:
    from dm_env import StepType
except ImportError:
    # Fallback with correct mapping
    class StepType:
        FIRST = 0
        MID = 1
        LAST = 2


class DiscoCartPoleEnv(base.Environment):
    """DiscoRL-compatible batched CartPole environment.
    
    CartPole already has discrete actions (0, 1), so no discretization needed.
    This adapter simply wraps Gym CartPole to provide DiscoRL's Environment interface
    with types.EnvironmentTimestep.
    """
    
    def __init__(
        self,
        batch_size: int = 1,
        max_steps: int = 500,
    ):
        self.batch_size = batch_size
        self.max_steps = max_steps
        self._step_counts = np.zeros(batch_size, dtype=np.int32)
        self._episode_done = np.zeros(batch_size, dtype=bool)
        
        # Create env instances
        self._envs = [gym.make('CartPole-v1') for _ in range(batch_size)]
        
        # Build specs from first env
        base_env = self._envs[0]
        
        try:
            from dm_env import specs as dm_specs
            # CartPole has action space Discrete(2), so actions are {0, 1}
            self._single_action_spec = dm_specs.BoundedArray(
                shape=(), dtype=np.int32, minimum=0, maximum=1
            )
            obs_shape = base_env.observation_space.shape
            obs_dtype = base_env.observation_space.dtype
            self._single_observation_spec = {
                'observation': dm_specs.Array(shape=obs_shape, dtype=obs_dtype)
            }
        except Exception:
            self._single_action_spec = type('ActionSpec', (), {
                'shape': (),
                'dtype': np.int32,
                'low': 0,
                'high': 1,
            })
            self._single_observation_spec = {
                'observation': base_env.observation_space
            }
        
        self._last_obs = [None] * batch_size
        self._last_info = [{}] * batch_size
    
    def single_action_spec(self):
        return self._single_action_spec
    
    def single_observation_spec(self):
        return self._single_observation_spec
    
    def step(
        self, state_unused: Any, actions: np.ndarray
    ) -> Tuple[Any, types.EnvironmentTimestep]:
        """Step all envs.
        
        Args:
            state_unused: unused (kept for DiscoRL interface compatibility)
            actions: array of shape (batch_size,) with discrete action indices (0 or 1)
        
        Returns:
            (state, timestep) where timestep is a batched EnvironmentTimestep
        """
        # Convert actions to list if needed
        if isinstance(actions, (np.ndarray, jnp.ndarray)):
            actions_list = [int(a) for a in np.asarray(actions)]
        else:
            actions_list = list(actions)
        
        obs_batch = []
        reward_batch = []
        done_batch = []
        
        for i, env in enumerate(self._envs):
            action = actions_list[i] if i < len(actions_list) else 0
            # Action should be 0 or 1 for CartPole
            action = int(action) % 2
            
            # ✅ FIX: Never auto-reset. Always step the environment.
            # If episode is done, it should have been reset by the caller.
            # This ensures correct reward propagation.
            obs, reward, terminated, truncated, info = env.step(action)
            done = bool(terminated or truncated)
            
            # Increment step counter; mark as done on terminal or max steps
            self._step_counts[i] += 1
            if done or self._step_counts[i] >= self.max_steps:
                self._episode_done[i] = True
            
            self._last_obs[i] = obs
            self._last_info[i] = info
            obs_batch.append(jnp.asarray(obs, dtype=jnp.float32))
            reward_batch.append(float(reward))
            done_batch.append(done)
        
        # Stack into batched timestep
        obs_map = {'observation': jnp.stack(obs_batch)}
        rewards = jnp.asarray(reward_batch, dtype=jnp.float32)
        is_terminal = jnp.asarray(done_batch, dtype=jnp.bool_)
        # Use LAST (2) for terminal, MID (1) for non-terminal
        step_type = jnp.where(is_terminal, StepType.LAST, StepType.MID)
        
        timestep = types.EnvironmentTimestep(
            observation=obs_map,
            step_type=step_type,
            reward=rewards,
        )
        return None, timestep
    
    def reset(self, rng_key: Optional[Any] = None) -> Tuple[Any, types.EnvironmentTimestep]:
        """Reset all envs.
        
        Args:
            rng_key: optional JAX RNG (unused here)
        
        Returns:
            (state, timestep)
        """
        obs_batch = []
        reward_batch = []
        done_batch = []
        
        for i, env in enumerate(self._envs):
            obs, info = env.reset()
            self._last_obs[i] = obs
            self._last_info[i] = info
            self._step_counts[i] = 0
            self._episode_done[i] = False
            obs_batch.append(jnp.asarray(obs, dtype=jnp.float32))
            reward_batch.append(0.0)
            done_batch.append(False)
        
        obs_map = {'observation': jnp.stack(obs_batch)}
        rewards = jnp.asarray(reward_batch, dtype=jnp.float32)
        is_terminal = jnp.asarray(done_batch, dtype=jnp.bool_)
        # Use FIRST (0) for reset, since this is the first step of a new episode
        step_type = jnp.full((len(self._envs),), StepType.FIRST, dtype=jnp.int32)
        
        timestep = types.EnvironmentTimestep(
            observation=obs_map,
            step_type=step_type,
            reward=rewards,
        )
        return None, timestep
