#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Bataille Navale - Serveur LAN (arbitre)
Lance le serveur, attend 2 clients, échange messages JSON ligne par ligne.
"""

import socket
import threading
import json
import time

HOST = "0.0.0.0"
PORT = 5000

# Board constants
ROWS = 10
COLS = 10
SHIPS_DEF = {
    "Carrier": 5,
    "Battleship": 4,
    "Cruiser": 3,
    "Submarine": 3,
    "Destroyer": 2
}

# Helper: send JSON line
def send_line(conn, data):
    try:
        conn.sendall((json.dumps(data) + "\n").encode())
    except Exception as e:
        print("Send error:", e)

# Helper: read JSON line from file-like socket
def recv_line(f):
    line = f.readline()
    if not line:
        return None
    return json.loads(line.strip())

class Player:
    def __init__(self, conn, addr, idx):
        self.conn = conn
        self.f = conn.makefile(mode="r", encoding="utf-8")
        self.addr = addr
        self.idx = idx
        self.ready = False
        self.board_ships = None  # dict of ship_id -> list of (r,c)
        self.hits_received = set()  # positions hit by opponent

    def close(self):
        try:
            self.f.close()
        except:
            pass
        try:
            self.conn.close()
        except:
            pass

class GameServer:
    def __init__(self, host, port):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.bind((host, port))
        self.sock.listen(4)
        print(f"[SERVER] Listening on {host}:{port}")
        self.players = []
        self.lock = threading.Lock()
        self.turn = 0  # player index whose turn it is

    def accept_players(self):
        while len(self.players) < 2:
            conn, addr = self.sock.accept()
            with self.lock:
                idx = len(self.players)
                player = Player(conn, addr, idx)
                self.players.append(player)
            print(f"[SERVER] Player {idx+1} connected from {addr}")
            threading.Thread(target=self.handle_player, args=(player,), daemon=True).start()

        # when two players connected start handshake
        self.broadcast({"type": "info", "msg": "2 players connected. Place your ships."})
        # ask for placements
        self.broadcast({"type": "start_place", "ships": SHIPS_DEF})

    def broadcast(self, data):
        with self.lock:
            for p in list(self.players):
                try:
                    send_line(p.conn, data)
                except:
                    pass

    def handle_player(self, player):
        try:
            send_line(player.conn, {"type": "welcome", "player_idx": player.idx, "msg": f"You are player {player.idx+1}"})
            while True:
                data = recv_line(player.f)
                if data is None:
                    print(f"[SERVER] Player {player.idx+1} disconnected")
                    break
                self.process_message(player, data)
        except Exception as e:
            print("Player handler error:", e)
        finally:
            player.close()

    def process_message(self, player, data):
        t = data.get("type")
        if t == "place":
            # expects 'ships': {name: [[r,c],...], ...}
            ships = data.get("ships")
            if not self.validate_placement(ships):
                send_line(player.conn, {"type": "error", "msg": "Invalid placement"})
                return
            player.board_ships = ships
            player.ready = True
            send_line(player.conn, {"type": "placed_ok"})
            print(f"[SERVER] Player {player.idx+1} placed ships")
            # If both ready, start game
            if all(p.ready for p in self.players) and len(self.players) >= 2:
                time.sleep(0.2)
                self.start_game()
        elif t == "shot":
            # only process if both ready
            if not all(p.ready for p in self.players):
                send_line(player.conn, {"type": "error", "msg": "Both players not ready"})
                return
            if player.idx != self.turn:
                send_line(player.conn, {"type": "error", "msg": "Not your turn"})
                return
            r = data.get("r")
            c = data.get("c")
            self.handle_shot(player.idx, r, c)
        elif t == "forfeit":
            # opponent wins
            winner = 1 - player.idx
            self.end_game(winner, reason=f"Player {player.idx+1} forfeited")
        else:
            send_line(player.conn, {"type": "error", "msg": "Unknown message type"})

    def validate_placement(self, ships):
        # ships is a dict name->list of [r,c]
        if not isinstance(ships, dict):
            return False
        occupied = set()
        for name, coords in ships.items():
            if name not in SHIPS_DEF:
                return False
            if not isinstance(coords, list) or len(coords) != SHIPS_DEF[name]:
                return False
            # each coord valid and not overlapping, must form straight contiguous line
            rows = [p[0] for p in coords]
            cols = [p[1] for p in coords]
            if any(not (0 <= rr < ROWS and 0 <= cc < COLS) for rr, cc in coords):
                return False
            # contiguous line check: either same row or same col
            if not (len(set(rows)) == 1 or len(set(cols)) == 1):
                return False
            # ensure consecutive
            if len(set(rows)) == 1:
                sorted_cols = sorted(cols)
                if sorted_cols != list(range(min(cols), max(cols)+1)):
                    return False
            else:
                sorted_rows = sorted(rows)
                if sorted_rows != list(range(min(rows), max(rows)+1)):
                    return False
            for p in coords:
                if tuple(p) in occupied:
                    return False
                occupied.add(tuple(p))
        return True

    def start_game(self):
        # tell players game starts and whose turn
        self.turn = 0
        for p in self.players:
            send_line(p.conn, {"type": "start_game", "your_idx": p.idx})
        self.notify_turn()

    def notify_turn(self):
        # notify both players whose turn it is
        for p in self.players:
            send_line(p.conn, {"type": "turn", "current": self.turn})

    def handle_shot(self, shooter_idx, r, c):
        shooter = self.players[shooter_idx]
        target = self.players[1 - shooter_idx]
        pos = (r, c)
        # check bounds
        if not (0 <= r < ROWS and 0 <= c < COLS):
            send_line(shooter.conn, {"type": "error", "msg": "Shot out of bounds"})
            return
        # if already shot there? check by looking at hits_received of target
        if pos in target.hits_received:
            send_line(shooter.conn, {"type": "error", "msg": "Position already shot"})
            return
        # mark hit
        target.hits_received.add(pos)
        # determine result
        hit_ship = None
        for name, coords in target.board_ships.items():
            coords_t = [tuple(p) for p in coords]
            if pos in coords_t:
                hit_ship = name
                break
        if hit_ship is None:
            # miss
            send_line(shooter.conn, {"type": "result", "res": "miss", "r": r, "c": c})
            send_line(target.conn, {"type": "update", "opponent_shot": {"r": r, "c": c, "res": "miss"}})
            # switch turn
            self.turn = 1 - self.turn
            self.notify_turn()
        else:
            # check if ship sunk
            coords_t = [tuple(p) for p in target.board_ships[hit_ship]]
            sunk = all(p in target.hits_received for p in coords_t)
            if sunk:
                send_line(shooter.conn, {"type": "result", "res": "sunk", "ship": hit_ship, "r": r, "c": c})
                send_line(target.conn, {"type": "update", "opponent_shot": {"r": r, "c": c, "res": "sunk", "ship": hit_ship}})
                # check win: all ships coords in hits_received?
                all_coords = []
                for coords in target.board_ships.values():
                    all_coords.extend([tuple(p) for p in coords])
                if set(all_coords).issubset(target.hits_received):
                    # shooter wins
                    self.end_game(shooter_idx, reason="All ships sunk")
                    return
                else:
                    # shooter keeps the turn after hit (classic rule variations exist; we keep shooter on turn)
                    self.notify_turn()
            else:
                send_line(shooter.conn, {"type": "result", "res": "hit", "r": r, "c": c})
                send_line(target.conn, {"type": "update", "opponent_shot": {"r": r, "c": c, "res": "hit"}})
                # shooter keeps turn
                self.notify_turn()

    def end_game(self, winner_idx, reason=""):
        print(f"[SERVER] Game ended. Winner: Player {winner_idx+1}. Reason: {reason}")
        for p in self.players:
            send_line(p.conn, {"type": "end", "winner": winner_idx, "reason": reason})
        # close sockets after a short pause
        time.sleep(1.0)
        for p in self.players:
            try:
                p.close()
            except:
                pass
        # reset server for new game? here we exit
        print("[SERVER] Shutting down.")
        try:
            self.sock.close()
        except:
            pass
        exit(0)


if __name__ == "__main__":
    server = GameServer(HOST, PORT)
    server.accept_players()
