use std::collections::HashMap;

use instant::Duration;

use crate::{
    network::protocol::UdpProtocol, sessions::p2p_session::PlayerRegistry, Config, DesyncDetection,
    GgrsError, NonBlockingSocket, P2PSession, PlayerHandle, PlayerType, SpectatorSession,
    SyncTestSession,
};

// The amount of inputs a spectator can buffer (a second worth of inputs at 60 FPS)
pub(crate) const SPECTATOR_BUFFER_SIZE: usize = 60;

const DEFAULT_PLAYERS: usize = 2;
const DEFAULT_SAVE_MODE: bool = false;
const DEFAULT_DETECTION_MODE: DesyncDetection = DesyncDetection::Off;
const DEFAULT_INPUT_DELAY: usize = 0;
const DEFAULT_DISCONNECT_TIMEOUT: Duration = Duration::from_millis(2000);
const DEFAULT_DISCONNECT_NOTIFY_START: Duration = Duration::from_millis(500);
const DEFAULT_FPS: usize = 60;
const DEFAULT_MAX_PREDICTION_FRAMES: usize = 8;
const DEFAULT_CHECK_DISTANCE: usize = 2;
// If the spectator is more than this amount of frames behind, it will use catch-up speed.
const DEFAULT_MAX_FRAMES_BEHIND: usize = 10;
// The amount of frames the spectator advances in a single step if too far behind
const DEFAULT_CATCHUP_SPEED: usize = 1;
// The amount of events a spectator can buffer; should never be an issue if the user polls the events at every step
pub(crate) const MAX_EVENT_QUEUE_SIZE: usize = 100;

/// The [`SessionBuilder`] builds all GGRS Sessions. After setting all appropriate values, use `SessionBuilder::start_yxz_session(...)`
/// to consume the builder and create a Session of desired type.
#[must_use]
#[derive(Debug)]
pub struct SessionBuilder<T>
where
    T: Config,
{
    num_players: usize,
    local_players: usize,
    max_prediction: usize,
    /// FPS defines the expected update frequency of this session.
    fps: usize,
    sparse_saving: bool,
    desync_detection: DesyncDetection,
    /// The time until a remote player gets disconnected.
    disconnect_timeout: Duration,
    /// The time until the client will get a notification that a remote player is about to be disconnected.
    disconnect_notify_start: Duration,
    player_reg: PlayerRegistry<T>,
    input_delay: usize,
    check_dist: usize,
    max_frames_behind: usize,
    catchup_speed: usize,
}

impl<T: Config> Default for SessionBuilder<T> {
    fn default() -> Self {
        Self::new()
    }
}

impl<T: Config> SessionBuilder<T> {
    /// Construct a new builder with all values set to their defaults.
    pub fn new() -> Self {
        Self {
            player_reg: PlayerRegistry::new(),
            local_players: 0,
            num_players: DEFAULT_PLAYERS,
            max_prediction: DEFAULT_MAX_PREDICTION_FRAMES,
            fps: DEFAULT_FPS,
            sparse_saving: DEFAULT_SAVE_MODE,
            desync_detection: DEFAULT_DETECTION_MODE,
            disconnect_timeout: DEFAULT_DISCONNECT_TIMEOUT,
            disconnect_notify_start: DEFAULT_DISCONNECT_NOTIFY_START,
            input_delay: DEFAULT_INPUT_DELAY,
            check_dist: DEFAULT_CHECK_DISTANCE,
            max_frames_behind: DEFAULT_MAX_FRAMES_BEHIND,
            catchup_speed: DEFAULT_CATCHUP_SPEED,
        }
    }

    /// Must be called for each player in the session (e.g. in a 3 player session, must be called 3 times) before starting the session.
    /// Local and remote player handles must be in `0..num_players`, while spectator handles must be `>= num_players`.
    /// Later, you will need the player handle to add input, change parameters or disconnect the player or spectator.
    ///
    /// # Errors
    /// - Returns [`InvalidRequest`] if a player with that handle has been added before
    /// - Returns [`InvalidRequest`] if the handle is invalid for the given [`PlayerType`]
    ///
    /// [`InvalidRequest`]: GgrsError::InvalidRequest
    /// [`num_players`]: Self#structfield.num_players
    pub fn add_player(
        mut self,
        player_type: PlayerType<T::Address>,
        player_handle: PlayerHandle,
    ) -> Result<Self, GgrsError> {
        // check if the player handle is already in use
        if self.player_reg.handles.contains_key(&player_handle) {
            return Err(GgrsError::InvalidRequest {
                info: "Player handle already in use.".to_owned(),
            });
        }
        Self::validate_player_handle(&player_type, player_handle, self.num_players)?;

        if matches!(player_type, PlayerType::Local) {
            self.local_players += 1;
        }
        self.player_reg.handles.insert(player_handle, player_type);
        Ok(self)
    }

    fn validate_player_handle(
        player_type: &PlayerType<T::Address>,
        player_handle: PlayerHandle,
        num_players: usize,
    ) -> Result<(), GgrsError> {
        match player_type {
            PlayerType::Local => {
                if player_handle >= num_players {
                    return Err(GgrsError::InvalidRequest {
                        info: "The player handle you provided is invalid. For a local player, the handle should be between 0 and num_players".to_owned(),
                    });
                }
            }
            PlayerType::Remote(_) => {
                if player_handle >= num_players {
                    return Err(GgrsError::InvalidRequest {
                        info: "The player handle you provided is invalid. For a remote player, the handle should be between 0 and num_players".to_owned(),
                    });
                }
            }
            PlayerType::Spectator(_) => {
                if player_handle < num_players {
                    return Err(GgrsError::InvalidRequest {
                        info: "The player handle you provided is invalid. For a spectator, the handle should be num_players or higher".to_owned(),
                    });
                }
            }
        }
        Ok(())
    }

    /// Change the maximum prediction window. Default is 8.
    ///
    /// ## Lockstep mode
    ///
    /// Setting this to 0 enables lockstep mode:
    /// - `AdvanceFrame` is only emitted once all remote inputs for the current frame are confirmed.
    /// - `SaveGameState` and `LoadGameState` are never emitted — no rollback occurs.
    ///
    /// Plain lockstep mode is conservative: the game stalls until remote inputs are observed by
    /// the next call to [`P2PSession::advance_frame`]. Smooth playback may therefore require input
    /// delay that covers network latency plus polling/scheduling jitter. For a bounded wait that
    /// polls during the current tick, use [`P2PSession::advance_frame_with_wait`].
    ///
    /// Input-delay semantics match rollback mode: input pressed on frame `F` takes effect on frame
    /// `F + D`.
    pub fn with_max_prediction_window(mut self, window: usize) -> Self {
        self.max_prediction = window;
        self
    }

    /// Change the amount of frames GGRS will delay the inputs for local players. Default is 0.
    ///
    /// In rollback mode, input delay (typically 2–4 frames) reduces rollbacks by letting remote
    /// inputs arrive before they are needed, at the cost of constant perceived latency.
    ///
    /// In lockstep mode, input delay determines how far ahead local input is scheduled, but the
    /// public session frame remains the game frame. Without [`P2PSession::advance_frame_with_wait`],
    /// smooth playback may require enough delay to cover network latency plus polling/scheduling
    /// jitter.
    ///
    /// There is no enforced upper bound, but values above ~8 frames will produce noticeable
    /// input lag. In rollback mode, setting this higher than `max_prediction_window` is not
    /// recommended because inputs delayed beyond the prediction window will stall the session.
    pub fn with_input_delay(mut self, delay: usize) -> Self {
        self.input_delay = delay;
        self
    }

    /// Change number of total players. Default is 2.
    ///
    /// Must be at least 1. This value determines valid player handle ranges: local and remote
    /// player handles must be in `0..num_players`, and spectator handles must be `>= num_players`.
    ///
    /// If called after [`add_player()`], already-registered handles are revalidated against the
    /// new value.
    ///
    /// # Errors
    /// - Returns [`InvalidRequest`] if `num_players` is 0.
    /// - Returns [`InvalidRequest`] if already-registered player handles would become invalid.
    ///
    /// [`add_player()`]: Self::add_player
    /// [`InvalidRequest`]: GgrsError::InvalidRequest
    pub fn with_num_players(mut self, num_players: usize) -> Result<Self, GgrsError> {
        if num_players == 0 {
            return Err(GgrsError::InvalidRequest {
                info: "Number of players must be at least 1.".to_owned(),
            });
        }
        for (&player_handle, player_type) in &self.player_reg.handles {
            Self::validate_player_handle(player_type, player_handle, num_players)?;
        }
        self.num_players = num_players;
        Ok(self)
    }

    /// Sets the sparse saving mode. With sparse saving turned on, only the minimum confirmed frame
    /// (for which all inputs from all players are confirmed correct) will be saved. This leads to
    /// much less save requests at the cost of potentially longer rollbacks and thus more advance
    /// frame requests. Recommended, if saving your gamestate takes much more time than advancing
    /// the game state.
    pub fn with_sparse_saving_mode(mut self, sparse_saving: bool) -> Self {
        self.sparse_saving = sparse_saving;
        self
    }

    /// Sets the desync detection mode. With desync detection, the session compares checksums for all peers to detect desyncs.
    /// If a desync is found, the session sends a `DesyncDetected` event. `DesyncDetection::On` requires an interval higher than 0 when starting a P2P session.
    pub fn with_desync_detection_mode(mut self, desync_detection: DesyncDetection) -> Self {
        self.desync_detection = desync_detection;
        self
    }

    /// Sets the disconnect timeout. The session will automatically disconnect from a remote peer if it has not received a packet in the timeout window.
    pub fn with_disconnect_timeout(mut self, timeout: Duration) -> Self {
        self.disconnect_timeout = timeout;
        self
    }

    /// Sets the time before the first notification will be sent in case of a prolonged period of no received packages.
    pub fn with_disconnect_notify_delay(mut self, notify_delay: Duration) -> Self {
        self.disconnect_notify_start = notify_delay;
        self
    }

    /// Sets the FPS this session is used with. This influences estimations for frame synchronization between sessions.
    /// # Errors
    /// - Returns [`InvalidRequest`] if the fps is 0
    ///
    /// [`InvalidRequest`]: GgrsError::InvalidRequest
    pub fn with_fps(mut self, fps: usize) -> Result<Self, GgrsError> {
        if fps == 0 {
            return Err(GgrsError::InvalidRequest {
                info: "FPS should be higher than 0.".to_owned(),
            });
        }
        self.fps = fps;
        Ok(self)
    }

    /// Change the check distance for [`SyncTestSession`]. Default is 2.
    ///
    /// The check distance is the number of frames that will be rolled back and re-simulated each
    /// update. After re-simulation, the resulting checksums are compared against the originals. A
    /// mismatch indicates a non-determinism bug. Higher values catch more bugs but increase CPU
    /// cost.
    ///
    /// Must be **less than** `max_prediction_window` (default 8). A value of 0 or 1 disables
    /// checksum comparison — use 2 or higher to actually catch non-determinism.
    /// [`start_synctest_session()`] returns [`GgrsError::InvalidRequest`] if this constraint
    /// is violated.
    ///
    /// [`start_synctest_session()`]: Self::start_synctest_session
    /// [`GgrsError::InvalidRequest`]: GgrsError::InvalidRequest
    pub fn with_check_distance(mut self, check_distance: usize) -> Self {
        self.check_dist = check_distance;
        self
    }

    /// Sets the spectator catch-up threshold. If the spectator is more than this amount of frames behind the received inputs,
    /// it will advance up to `catchup_speed` frames per step.
    ///
    /// # Errors
    /// - Returns [`InvalidRequest`] if `max_frames_behind` is 0 or `>= SPECTATOR_BUFFER_SIZE`.
    ///
    /// [`InvalidRequest`]: GgrsError::InvalidRequest
    pub fn with_max_frames_behind(mut self, max_frames_behind: usize) -> Result<Self, GgrsError> {
        if max_frames_behind < 1 {
            return Err(GgrsError::InvalidRequest {
                info: "Max frames behind cannot be smaller than 1.".to_owned(),
            });
        }

        if max_frames_behind >= SPECTATOR_BUFFER_SIZE {
            return Err(GgrsError::InvalidRequest {
                info: format!(
                    "Max frames behind cannot be larger or equal than the Spectator buffer size ({SPECTATOR_BUFFER_SIZE})"
                ),
            });
        }
        self.max_frames_behind = max_frames_behind;
        Ok(self)
    }

    /// Sets the spectator catch-up speed. By default, this is set to 1, so the spectator never catches up faster than normal.
    /// If you want the spectator to catch up to the host once `max_frames_behind` is surpassed, set this to a value higher than 1.
    /// The actual number of frames advanced is capped by the number of confirmed frames available from the host.
    ///
    /// # Errors
    /// - Returns [`InvalidRequest`] if `catchup_speed` is 0.
    ///
    /// [`InvalidRequest`]: GgrsError::InvalidRequest
    pub fn with_catchup_speed(mut self, catchup_speed: usize) -> Result<Self, GgrsError> {
        if catchup_speed < 1 {
            return Err(GgrsError::InvalidRequest {
                info: "Catchup speed cannot be smaller than 1.".to_owned(),
            });
        }

        self.catchup_speed = catchup_speed;
        Ok(self)
    }

    /// Consumes the builder to construct a [`P2PSession`] and starts synchronization of endpoints.
    /// # Errors
    /// - Returns [`InvalidRequest`] if insufficient players have been registered.
    ///
    /// [`InvalidRequest`]: GgrsError::InvalidRequest
    pub fn start_p2p_session(
        mut self,
        socket: impl NonBlockingSocket<T::Address> + 'static,
    ) -> Result<P2PSession<T>, GgrsError> {
        if let DesyncDetection::On { interval: 0 } = self.desync_detection {
            return Err(GgrsError::InvalidRequest {
                info: "Desync detection interval must be higher than 0.".to_owned(),
            });
        }

        // check if all players are added
        for player_handle in 0..self.num_players {
            if !self.player_reg.handles.contains_key(&player_handle) {
                return Err(GgrsError::InvalidRequest{
                    info: "Not enough players have been added. Keep registering players up to the defined player number.".to_owned(),
                });
            }
        }

        // count the number of players per address
        let mut addr_count = HashMap::<PlayerType<T::Address>, Vec<PlayerHandle>>::new();
        for (handle, player_type) in &self.player_reg.handles {
            match player_type {
                PlayerType::Remote(_) | PlayerType::Spectator(_) => addr_count
                    .entry(player_type.clone())
                    .or_insert_with(Vec::new)
                    .push(*handle),
                PlayerType::Local => (),
            }
        }

        // for each unique address, create an endpoint
        for (player_type, handles) in addr_count {
            match player_type {
                PlayerType::Remote(peer_addr) => {
                    self.player_reg.remotes.insert(
                        peer_addr.clone(),
                        self.create_endpoint(handles, peer_addr.clone(), self.local_players),
                    );
                }
                PlayerType::Spectator(peer_addr) => {
                    self.player_reg.spectators.insert(
                        peer_addr.clone(),
                        self.create_endpoint(handles, peer_addr.clone(), self.num_players), // the host of the spectator sends inputs for all players
                    );
                }
                PlayerType::Local => (),
            }
        }

        Ok(P2PSession::<T>::new(
            self.num_players,
            self.max_prediction,
            Box::new(socket),
            self.player_reg,
            self.sparse_saving,
            self.desync_detection,
            self.input_delay,
            self.fps,
        ))
    }

    /// Consumes the builder to create a new [`SpectatorSession`].
    /// A [`SpectatorSession`] provides all functionality to connect to a remote host in a peer-to-peer fashion.
    /// The host will broadcast all confirmed inputs to this session.
    /// This session can be used to spectate a session without contributing to the game input.
    pub fn start_spectator_session(
        self,
        host_addr: T::Address,
        socket: impl NonBlockingSocket<T::Address> + 'static,
    ) -> SpectatorSession<T> {
        // create host endpoint
        let mut host = UdpProtocol::new(
            (0..self.num_players).collect(),
            host_addr,
            self.num_players,
            1, //should not matter since the spectator is never sending
            self.max_prediction,
            self.disconnect_timeout,
            self.disconnect_notify_start,
            self.fps,
            DesyncDetection::Off,
        );
        host.synchronize();
        SpectatorSession::new(
            self.num_players,
            Box::new(socket),
            host,
            self.max_frames_behind,
            self.catchup_speed,
        )
    }

    /// Consumes the builder to construct a new [`SyncTestSession`].
    ///
    /// During a [`SyncTestSession`], GGRS simulates a rollback every frame and re-simulates the
    /// last `check_distance` frames, then compares checksums against the originals. A mismatch
    /// indicates a non-determinism bug in your save/load/advance logic. No network is involved.
    ///
    /// Checksum comparisons require a `check_distance` of 2 or higher (the default).
    ///
    /// # Errors
    /// - Returns [`InvalidRequest`] if `check_distance` is greater than or equal to `max_prediction_window`.
    /// - Returns [`InvalidRequest`] if sparse saving is enabled — synctest must save every frame
    ///   to compare checksums across the full check window, so sparse saving is incompatible.
    ///
    /// [`InvalidRequest`]: GgrsError::InvalidRequest
    pub fn start_synctest_session(self) -> Result<SyncTestSession<T>, GgrsError> {
        if self.check_dist >= self.max_prediction {
            return Err(GgrsError::InvalidRequest {
                info: "Check distance too big.".to_owned(),
            });
        }
        if self.sparse_saving {
            return Err(GgrsError::InvalidRequest {
                info: "Sparse saving is not supported for synctest sessions.".to_owned(),
            });
        }
        Ok(SyncTestSession::new(
            self.num_players,
            self.max_prediction,
            self.check_dist,
            self.input_delay,
        ))
    }

    fn create_endpoint(
        &self,
        handles: Vec<PlayerHandle>,
        peer_addr: T::Address,
        local_players: usize,
    ) -> UdpProtocol<T> {
        // create the endpoint, set parameters
        let mut endpoint = UdpProtocol::new(
            handles,
            peer_addr,
            self.num_players,
            local_players,
            self.max_prediction,
            self.disconnect_timeout,
            self.disconnect_notify_start,
            self.fps,
            self.desync_detection,
        );
        // start the synchronization
        endpoint.synchronize();
        endpoint
    }
}
