use std::fs;
use std::io::ErrorKind;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
use std::path::Path;
use std::thread;
use std::time::{Duration, Instant};

use anyhow::{Context, Result};

use crate::net::protocol::{
    ClientPacket, GameEvent, MAX_PACKET_SIZE, PROTOCOL_VERSION, PlayerSnapshot, ServerPacket,
    decode, encode,
};
use crate::weapon::{PISTOL_COOLDOWN_MS, PISTOL_DAMAGE, RESPAWN_MS, hitscan_target};
use crate::world::{GameMap, MAX_PLAYERS, PLAYER_HEALTH, Vec2, apply_input};

const SERVER_TICK_HZ: f32 = 30.0;
const SNAPSHOT_HZ: f32 = 20.0;
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
const DEFAULT_MAP_PATH: &str = "maps/arena01.txt";
const EMBEDDED_DEFAULT_MAP: &str = include_str!("../../maps/arena01.txt");

#[derive(Debug, Clone)]
struct RemotePlayer {
    id: u8,
    addr: SocketAddr,
    name: String,
    position: Vec2,
    angle: f32,
    health: i16,
    score: u32,
    deaths: u32,
    alive: bool,
    buttons: u16,
    turn: i8,
    last_seq: u32,
    last_fire_seq: u32,
    pending_fire_seq: Option<u32>,
    last_heard: Instant,
    last_shot: Instant,
    respawn_at: Option<Instant>,
}

impl RemotePlayer {
    fn snapshot(&self, now: Instant) -> PlayerSnapshot {
        PlayerSnapshot {
            id: self.id,
            name: self.name.clone(),
            position: self.position,
            angle: self.angle,
            health: self.health,
            score: self.score,
            deaths: self.deaths,
            alive: self.alive,
            respawn_ms: self
                .respawn_at
                .and_then(|time| time.checked_duration_since(now))
                .map(|duration| duration.as_millis().min(u16::MAX as u128) as u16)
                .unwrap_or(0),
        }
    }
}

pub struct ServerGame {
    map: GameMap,
    players: Vec<Option<RemotePlayer>>,
    tick: u64,
    events: Vec<GameEvent>,
}

impl ServerGame {
    pub fn new(map: GameMap) -> Self {
        Self {
            map,
            players: (0..MAX_PLAYERS).map(|_| None).collect(),
            tick: 0,
            events: Vec::new(),
        }
    }

    fn handle_packet(
        &mut self,
        packet: ClientPacket,
        addr: SocketAddr,
        now: Instant,
    ) -> ServerPacket {
        match packet {
            ClientPacket::ClientHello {
                protocol_version,
                name,
            } => self.handle_hello(protocol_version, name, addr, now),
            ClientPacket::ClientInput {
                protocol_version,
                seq,
                buttons,
                turn,
                fire_seq,
                ..
            } => {
                if protocol_version == PROTOCOL_VERSION {
                    self.handle_input(addr, seq, buttons, turn, fire_seq, now);
                }
                self.snapshot_packet(now)
            }
            ClientPacket::Ping {
                protocol_version,
                nonce,
                client_time_ms,
            } => ServerPacket::Pong {
                protocol_version,
                nonce,
                client_time_ms,
                server_tick: self.tick,
            },
            ClientPacket::Disconnect { protocol_version } => {
                if protocol_version == PROTOCOL_VERSION {
                    self.disconnect(addr);
                }
                self.snapshot_packet(now)
            }
        }
    }

    fn handle_hello(
        &mut self,
        protocol_version: u16,
        name: String,
        addr: SocketAddr,
        now: Instant,
    ) -> ServerPacket {
        if protocol_version != PROTOCOL_VERSION {
            return ServerPacket::ServerWelcome {
                protocol_version: PROTOCOL_VERSION,
                accepted: false,
                player_id: u8::MAX,
                map_text: String::new(),
                server_tick: self.tick,
                initial: None,
                message: format!(
                    "protocol mismatch: client {protocol_version}, server {PROTOCOL_VERSION}"
                ),
            };
        }

        if let Some(id) = self.find_by_addr(addr) {
            let mut reconnected = None;
            if let Some(player) = &mut self.players[id] {
                player.name = clean_name(&name);
                player.last_heard = now;
                let initial = player.snapshot(now);
                reconnected = Some((player.id, initial));
            }
            if let Some((player_id, initial)) = reconnected {
                return self.welcome(true, player_id, Some(initial), "reconnected");
            }
        }

        let Some(id) = self.players.iter().position(Option::is_none) else {
            return self.welcome(false, u8::MAX, None, "server is full");
        };

        let spawn = self.map.spawn_for(id);
        let player = RemotePlayer {
            id: id as u8,
            addr,
            name: clean_name(&name),
            position: spawn,
            angle: 0.0,
            health: PLAYER_HEALTH,
            score: 0,
            deaths: 0,
            alive: true,
            buttons: 0,
            turn: 0,
            last_seq: 0,
            last_fire_seq: 0,
            pending_fire_seq: None,
            last_heard: now,
            last_shot: now - Duration::from_millis(PISTOL_COOLDOWN_MS),
            respawn_at: None,
        };

        let initial = player.snapshot(now);
        self.events.push(GameEvent::Joined {
            player: player.id,
            name: player.name.clone(),
        });
        self.players[id] = Some(player);
        self.welcome(true, id as u8, Some(initial), "welcome")
    }

    fn welcome(
        &self,
        accepted: bool,
        player_id: u8,
        initial: Option<PlayerSnapshot>,
        message: &str,
    ) -> ServerPacket {
        ServerPacket::ServerWelcome {
            protocol_version: PROTOCOL_VERSION,
            accepted,
            player_id,
            map_text: if accepted {
                self.map.source.clone()
            } else {
                String::new()
            },
            server_tick: self.tick,
            initial,
            message: message.to_owned(),
        }
    }

    fn handle_input(
        &mut self,
        addr: SocketAddr,
        seq: u32,
        buttons: u16,
        turn: i8,
        fire_seq: u32,
        now: Instant,
    ) {
        let Some(id) = self.find_by_addr(addr) else {
            return;
        };
        let Some(player) = &mut self.players[id] else {
            return;
        };

        player.last_heard = now;
        if seq >= player.last_seq {
            player.last_seq = seq;
            player.buttons = buttons;
            player.turn = turn.clamp(-1, 1);
        }
        if fire_seq > player.last_fire_seq {
            player.last_fire_seq = fire_seq;
            player.pending_fire_seq = Some(fire_seq);
        }
    }

    pub fn update(&mut self, dt: f32, now: Instant) {
        self.tick = self.tick.wrapping_add(1);
        self.remove_stale(now);

        for player in self.players.iter_mut().flatten() {
            if !player.alive {
                if player.respawn_at.is_some_and(|time| time <= now) {
                    player.alive = true;
                    player.health = PLAYER_HEALTH;
                    player.position = self.map.spawn_for(player.id as usize);
                    player.angle = 0.0;
                    player.respawn_at = None;
                    player.buttons = 0;
                    player.turn = 0;
                }
                continue;
            }

            let (position, angle) = apply_input(
                &self.map,
                player.position,
                player.angle,
                player.buttons,
                player.turn,
                dt,
            );
            player.position = position;
            player.angle = angle;
        }

        let shooters: Vec<u8> = self
            .players
            .iter()
            .flatten()
            .filter(|player| player.pending_fire_seq.is_some())
            .map(|player| player.id)
            .collect();
        for shooter_id in shooters {
            self.fire_if_ready(shooter_id, now);
        }
    }

    fn fire_if_ready(&mut self, shooter_id: u8, now: Instant) {
        let Some(shooter_index) = self.slot_for_id(shooter_id) else {
            return;
        };
        let Some(shooter) = &mut self.players[shooter_index] else {
            return;
        };
        shooter.pending_fire_seq = None;

        if !shooter.alive
            || now.duration_since(shooter.last_shot) < Duration::from_millis(PISTOL_COOLDOWN_MS)
        {
            return;
        }
        shooter.last_shot = now;
        let origin = shooter.position;
        let angle = shooter.angle;

        let snapshots = self.public_snapshots(now);
        let Some(hit) = hitscan_target(&self.map, shooter_id, origin, angle, &snapshots) else {
            return;
        };

        let Some(target_index) = self.slot_for_id(hit.target_id) else {
            return;
        };

        let mut killed = false;
        if let Some(target) = &mut self.players[target_index] {
            if !target.alive {
                return;
            }
            target.health -= PISTOL_DAMAGE;
            self.events.push(GameEvent::Hit {
                shooter: shooter_id,
                target: target.id,
                damage: PISTOL_DAMAGE,
            });
            if target.health <= 0 {
                target.health = 0;
                target.alive = false;
                target.deaths += 1;
                target.respawn_at = Some(now + Duration::from_millis(RESPAWN_MS));
                killed = true;
            }
        }

        if killed {
            if let Some(shooter) = &mut self.players[shooter_index] {
                shooter.score += 1;
            }
            self.events.push(GameEvent::Kill {
                shooter: shooter_id,
                target: hit.target_id,
            });
        }
    }

    fn snapshot_packet(&self, now: Instant) -> ServerPacket {
        ServerPacket::ServerSnapshot {
            protocol_version: PROTOCOL_VERSION,
            server_tick: self.tick,
            players: self.public_snapshots(now),
            events: Vec::new(),
        }
    }

    fn public_snapshots(&self, now: Instant) -> Vec<PlayerSnapshot> {
        self.players
            .iter()
            .flatten()
            .map(|player| player.snapshot(now))
            .collect()
    }

    fn snapshot_with_events(&mut self, now: Instant) -> ServerPacket {
        let events = std::mem::take(&mut self.events);
        ServerPacket::ServerSnapshot {
            protocol_version: PROTOCOL_VERSION,
            server_tick: self.tick,
            players: self.public_snapshots(now),
            events,
        }
    }

    fn find_by_addr(&self, addr: SocketAddr) -> Option<usize> {
        self.players
            .iter()
            .position(|player| player.as_ref().is_some_and(|player| player.addr == addr))
    }

    fn slot_for_id(&self, id: u8) -> Option<usize> {
        let slot = id as usize;
        self.players
            .get(slot)
            .and_then(|p| p.as_ref())
            .map(|_| slot)
    }

    fn disconnect(&mut self, addr: SocketAddr) {
        if let Some(slot) = self.find_by_addr(addr)
            && let Some(player) = self.players[slot].take()
        {
            self.events.push(GameEvent::Left { player: player.id });
        }
    }

    pub fn remove_stale(&mut self, now: Instant) {
        for slot in 0..self.players.len() {
            let stale = self.players[slot]
                .as_ref()
                .is_some_and(|player| now.duration_since(player.last_heard) > CLIENT_TIMEOUT);
            if stale && let Some(player) = self.players[slot].take() {
                self.events.push(GameEvent::Left { player: player.id });
            }
        }
    }

    #[cfg(test)]
    fn insert_test_player(&mut self, id: usize, last_heard: Instant) {
        let addr: SocketAddr = format!("127.0.0.1:{}", 50_000 + id).parse().unwrap();
        self.players[id] = Some(RemotePlayer {
            id: id as u8,
            addr,
            name: format!("P{id}"),
            position: self.map.spawn_for(id),
            angle: 0.0,
            health: PLAYER_HEALTH,
            score: 0,
            deaths: 0,
            alive: true,
            buttons: 0,
            turn: 0,
            last_seq: 0,
            last_fire_seq: 0,
            pending_fire_seq: None,
            last_heard,
            last_shot: last_heard,
            respawn_at: None,
        });
    }
}

pub fn run(bind: &str, map_path: &Path) -> Result<()> {
    let map_text = match fs::read_to_string(map_path) {
        Ok(text) => text,
        Err(_) if map_path == Path::new(DEFAULT_MAP_PATH) => EMBEDDED_DEFAULT_MAP.to_owned(),
        Err(err) => {
            return Err(err).with_context(|| format!("failed to read map {}", map_path.display()));
        }
    };
    let map = GameMap::parse(&map_text).context("failed to parse map")?;
    let socket = UdpSocket::bind(bind).with_context(|| format!("failed to bind {bind}"))?;
    socket
        .set_nonblocking(true)
        .context("failed to set UDP socket nonblocking")?;

    println!("tfps server listening on {}", socket.local_addr()?);
    if let Some(ip) = local_ip_hint() {
        println!("LAN hint: tfps join {ip}:4000 --name Player");
    } else {
        println!("LAN hint unavailable. Use your Wi-Fi/LAN IPv4, usually 192.168.x.x or 10.x.x.x.");
    }
    println!("Press Ctrl+C to stop.");

    let mut game = ServerGame::new(map);
    let mut buf = [0_u8; MAX_PACKET_SIZE];
    let tick_step = Duration::from_secs_f32(1.0 / SERVER_TICK_HZ);
    let snapshot_step = Duration::from_secs_f32(1.0 / SNAPSHOT_HZ);
    let mut last_tick = Instant::now();
    let mut next_tick = last_tick + tick_step;
    let mut next_snapshot = last_tick + snapshot_step;

    loop {
        let now = Instant::now();
        drain_packets(&socket, &mut game, &mut buf, now);

        if now >= next_tick {
            let dt = now.duration_since(last_tick).as_secs_f32().min(0.1);
            game.update(dt, now);
            last_tick = now;
            next_tick = now + tick_step;
        }

        if now >= next_snapshot {
            let packet = game.snapshot_with_events(now);
            broadcast(&socket, &game, &packet);
            next_snapshot = now + snapshot_step;
        }

        thread::sleep(Duration::from_millis(1));
    }
}

fn drain_packets(socket: &UdpSocket, game: &mut ServerGame, buf: &mut [u8], now: Instant) {
    loop {
        match socket.recv_from(buf) {
            Ok((len, addr)) => {
                let Ok(packet) = decode::<ClientPacket>(&buf[..len]) else {
                    continue;
                };
                let response = game.handle_packet(packet, addr, now);
                if let Ok(bytes) = encode(&response) {
                    let _ = socket.send_to(&bytes, addr);
                }
            }
            Err(err) if err.kind() == ErrorKind::WouldBlock => break,
            Err(err) => {
                eprintln!("server recv error: {err}");
                break;
            }
        }
    }
}

fn broadcast(socket: &UdpSocket, game: &ServerGame, packet: &ServerPacket) {
    let Ok(bytes) = encode(packet) else {
        return;
    };
    for player in game.players.iter().flatten() {
        let _ = socket.send_to(&bytes, player.addr);
    }
}

fn clean_name(name: &str) -> String {
    let cleaned: String = name
        .chars()
        .filter(|ch| !ch.is_control())
        .take(16)
        .collect();
    if cleaned.trim().is_empty() {
        "Player".to_owned()
    } else {
        cleaned
    }
}

fn local_ip_hint() -> Option<String> {
    let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
    socket.connect("8.8.8.8:80").ok()?;
    let ip = socket.local_addr().ok()?.ip();
    if is_good_lan_hint(ip) {
        Some(ip.to_string())
    } else {
        None
    }
}

fn is_good_lan_hint(ip: IpAddr) -> bool {
    match ip {
        IpAddr::V4(ip) => is_good_lan_ipv4(ip),
        IpAddr::V6(_) => false,
    }
}

fn is_good_lan_ipv4(ip: Ipv4Addr) -> bool {
    let [a, b, ..] = ip.octets();
    if ip.is_loopback()
        || ip.is_unspecified()
        || ip.is_link_local()
        || ip.is_multicast()
        || ip.is_broadcast()
    {
        return false;
    }
    if a == 198 && (b == 18 || b == 19) {
        return false;
    }
    true
}

#[cfg(test)]
mod tests {
    use super::*;

    fn test_map() -> GameMap {
        GameMap::parse(
            "\
#####
#1.2#
#...#
#3.4#
#####
",
        )
        .unwrap()
    }

    #[test]
    fn stale_players_are_removed() {
        let now = Instant::now();
        let mut game = ServerGame::new(test_map());
        game.insert_test_player(0, now - CLIENT_TIMEOUT - Duration::from_secs(1));
        game.insert_test_player(1, now);

        game.remove_stale(now);

        assert!(game.players[0].is_none());
        assert!(game.players[1].is_some());
    }

    #[test]
    fn death_respawns_after_delay() {
        let now = Instant::now();
        let mut game = ServerGame::new(test_map());
        game.insert_test_player(0, now);
        let player = game.players[0].as_mut().unwrap();
        player.alive = false;
        player.health = 0;
        player.respawn_at = Some(now);

        game.update(0.016, now);

        let player = game.players[0].as_ref().unwrap();
        assert!(player.alive);
        assert_eq!(player.health, PLAYER_HEALTH);
    }

    #[test]
    fn filters_bad_lan_hints() {
        assert!(!is_good_lan_hint(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));
        assert!(!is_good_lan_hint(IpAddr::V4(Ipv4Addr::new(169, 254, 1, 1))));
        assert!(!is_good_lan_hint(IpAddr::V4(Ipv4Addr::new(198, 18, 0, 1))));
        assert!(!is_good_lan_hint(IpAddr::V4(Ipv4Addr::new(198, 19, 0, 1))));
        assert!(is_good_lan_hint(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10))));
        assert!(is_good_lan_hint(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 10))));
    }
}
