import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.google.gson.GsonBuilder;
 
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.io.FileWriter;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.time.temporal.ChronoUnit;
import java.time.Instant;
import java.util.Date;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

public class HOTELIERServer implements ResponseCodes{

    private Map<String, User> users;  
    private List<Hotel> hotels;
    private List<Review> reviews;
    private String usersJsonFilePath;
    private String reviewsJsonFilePath;
    private String hotelsJsonFilePath;
    // lock per la scrittura su file json
    private final Object lockReviews = new Object();
    private final Object lockUsers = new Object();
    private final Object lockHotels = new Object();

    public HOTELIERServer(String hotelsJsonFilePath, String usersJsonFilePath, String reviewsJsonFilePath) {
        this.usersJsonFilePath = usersJsonFilePath;
        this.reviewsJsonFilePath = reviewsJsonFilePath;
        this.hotelsJsonFilePath = hotelsJsonFilePath;
        this.users = new HashMap<>();

        loadHotels(hotelsJsonFilePath);
        loadUsers(usersJsonFilePath);
        loadReview(reviewsJsonFilePath);
    }

    // deserializza gli hotel
    private void loadHotels(String jsonFilePath) {
        // creo istanza della classe Gson che userò per deserializzare
        Gson gson = new Gson();
        // apro file hotel in lettura
        try (FileReader reader = new FileReader(jsonFilePath)) {
            // definisco il tipo di dati che si vogliono leggere
            Type hotelListType = new TypeToken<ArrayList<Hotel>>(){}.getType();
            // deserializza il contenuto letto nel file nel tipo definito
            List<Hotel> deserializedHotels = gson.fromJson(reader, hotelListType);
            hotels = new CopyOnWriteArrayList<>(deserializedHotels);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // deserializza gli utenti
    private void loadUsers(String jsonFilePath) {
        // creo istanza della classe Gson che userò per deserializzare
        Gson gson = new Gson();
        // apro file user in lettura
        try (FileReader reader = new FileReader(jsonFilePath)) {
            // definisco il tipo di dati che si vogliono leggere
            Type userType = new TypeToken<HashMap<String, User>>(){}.getType();
            // deserializza il contenuto letto nel file nel tipo definito
            Map<String, User> deserializedUsers = gson.fromJson(reader, userType);
            // controllo che il file non fosse vuote
            if (deserializedUsers == null) {
                users = new ConcurrentHashMap<>();
            } else {
                users = new ConcurrentHashMap<>(deserializedUsers);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // deserializza le recensioni
    private void loadReview(String jsonFilePath) {
        // creo istanza della classe Gson che userò per deserializzare
        Gson gson = new Gson();
        // apro file hotel in lettura
        try (FileReader reader = new FileReader(jsonFilePath)) {
            // definisco il tipo di dati che si vogliono leggere
            Type reviewsListType = new TypeToken<ArrayList<Review>>(){}.getType();
            // deserializza il contenuto letto nel file nel tipo definito
            List<Review> deserializedReviews = gson.fromJson(reader, reviewsListType);
            // controllo che il file non fosse vuoto
            if (deserializedReviews == null) {
                reviews = new CopyOnWriteArrayList<>();
            } else {
                reviews = new CopyOnWriteArrayList<>(deserializedReviews);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // Salva l'hotel nel file JSON
    private void saveHotel() {
        synchronized(lockHotels) {
            // Creo istanza della classe Gson che userò per serializzare
            Gson gson = new GsonBuilder()
                        .setPrettyPrinting() 
                        .create();
            // apro il file user in scrittura
            try (FileWriter writer = new FileWriter(hotelsJsonFilePath)) { 
                // serializzo la lista di hotel
                gson.toJson(hotels, writer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    // Salva gli utenti nel file JSON
    private void saveUsers() {
        synchronized(lockUsers) {
            // Creo istanza della classe Gson che userò per serializzare
            Gson gson = new GsonBuilder()
                        .setPrettyPrinting() 
                        .create();
            // apro il file user in scrittura
            try (FileWriter writer = new FileWriter(usersJsonFilePath)) { 
                // serializzo la hash map di user
                gson.toJson(users, writer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    // Salva le recensioni nel file JSON
    private void saveReview() {
        synchronized(lockReviews) {
            // Creo istanza della classe Gson che userò per serializzare
            Gson gson = new GsonBuilder()
                        .setPrettyPrinting() 
                        .create();
            // apro il file user in scrittura
            try (FileWriter writer = new FileWriter(reviewsJsonFilePath)) { 
                // serializzo la lista di recensioni 
                gson.toJson(reviews, writer);
            } catch (IOException e) {
                e.printStackTrace();
            }   
        }
    }

    // controlla se il voto è valido
    private boolean isRateValid(int rate) {
        return rate >= 0 && rate <= 5;
    }

    // Arrotonda al primo decimale
    public double round(double value) {
        return new BigDecimal(value)
                    .setScale(1, RoundingMode.HALF_DOWN) // Usa HALF_DOWN per l’effetto desiderato
                    .doubleValue();
    }

    public Hotel findHotelWithHighestRateInCity(String city) {
        Hotel highestRatedHotel = null;
        double highestRate = -1; 
    
        for (Hotel hotel : hotels) {
            if (hotel.getCity().equals(city) && hotel.getRate() > highestRate) {
                highestRatedHotel = hotel;
                highestRate = hotel.getRate(); // Aggiorna il rate più alto trovato
            }
        }
    
        return highestRatedHotel;
    }
    // Aggiorna il ranking di un hotel
    private void updateHotel(int hotelId) {
        // per ridurre il valore della recensione del 5% ogni giorno che passa
        double timeAttenuation = 0.95; 
        // variabili che contengono la somma di ogni singola punteggio di ogni singola recensione
        int totalGlobalScore = 0;
        int totalPosition = 0;
        int totalCleaning = 0;
        int totalQuality = 0;
        int totalServices = 0;
        // numero totale recesioni dell'hotel
        int reviewsQuantity = 0;
        // cerco le recensioni e le aggiungo alla lista di 
        for (Review r : reviews) {
            if (r.getIdHotel() == hotelId) {
                reviewsQuantity++;
                // calcolo la differenza in giorni tra la data della recensione e l’attuale
                long daysDifference = ChronoUnit.DAYS.between(r.getTimestamp().toInstant(), Instant.now());
                // calcola il fattore di decadimento in base ai giorni passati
                double decayFactor = Math.pow(timeAttenuation, daysDifference);
                
                // aggiungo i punteggi ponderati di attualità per ogni categoria
                totalGlobalScore += decayFactor * r.getGlobalScore();
                totalPosition += decayFactor * r.getPosition();
                totalCleaning += decayFactor * r.getCleaning();
                totalQuality += decayFactor * r.getQuality();
                totalServices += decayFactor * r.getServices();
            }
        }
        // Calcola la media ponderata dei punteggi totali per ogni categoria
        double avarageGlobalScore = round(totalGlobalScore / (double) reviewsQuantity);
        double avaragePosition = round(totalPosition / (double) reviewsQuantity);
        double avarageCleaning = round(totalCleaning / (double) reviewsQuantity);
        double avarageQuality = round(totalQuality / (double) reviewsQuantity);
        double avarageServices = round(totalServices / (double) reviewsQuantity);
        // calcolo il voto generale 
        double genericRate = (avarageGlobalScore + avaragePosition + avarageCleaning + avarageQuality + avarageServices) / 5.0;
        // considero anche la quantità di recensioni
        double quantityImpact = Math.min(1.0,(Math.log10((double) reviewsQuantity + 1.0)) / 5.0);
        // calcolo il voto finale tenendo conto di qualità quantità e attualità
        double finalRate = round(Math.min(5.0, genericRate + quantityImpact));
        // per salvarmi la citta e aggiornare i ranking
        String city = null;
        // per salvarmi il miglior hotel prima dell'insermento dei voti nuovi
        Hotel fristHotelBeforeUpdate = null;
        // inserisco i voti nell'hotel
        for (Hotel h : hotels) {
            if (h.getId() == hotelId) {
                city = h.getCity();
                // ricavo l'hotel migliore della città PRIMA dell'insermento dei voti
                fristHotelBeforeUpdate = findHotelWithHighestRateInCity(city);
                System.out.println("Il miglior hotel a " + city + " e' " + fristHotelBeforeUpdate.getName());
                Hotel.Ratings ratings = h.getRatings();
                h.setRate(finalRate);
                ratings.setPosition(avaragePosition);
                ratings.setCleaning(avarageCleaning);
                ratings.setQuality(avarageQuality);
                ratings.setServices(avarageServices);
                break;
            }
        }
        // aggiorno file json
        saveHotel();
        // ricavo l'hotel migliore della città DOPO l'inserimento dei voti
        Hotel fristHotelAfterUpdate = findHotelWithHighestRateInCity(city);
        // se è cambiato l'hotel migliore della città mando la notifica a gli utenti loggati
        if (fristHotelAfterUpdate.getId() != fristHotelBeforeUpdate.getId()) 
            notifyClient(city, fristHotelAfterUpdate);

    }

    // NOTIFY
    private void notifyClient(String city, Hotel hotel) {
        String message = "Il miglior hotel a " + city + " e’ adesso " + hotel.getName();
        byte[] messageBytes = message.getBytes();
        // Sostituire con il tuo indirizzo e porta del gruppo di multicast
        String multicastAddress = "224.0.0.1"; // Esempio di indirizzo di multicast
        int multicastPort = 4446; // Esempio di porta
    
        try (MulticastSocket socket = new MulticastSocket()) {
            InetAddress group = InetAddress.getByName(multicastAddress);
            DatagramPacket packet = new DatagramPacket(messageBytes, messageBytes.length, group, multicastPort);
            
            socket.send(packet);
            System.out.println("Notifica multicast inviata: " + message);
        } catch (IOException e) {
            System.err.println("Errore durante l’invio della notifica multicast: " + e.getMessage());
            e.printStackTrace();
        }
    }

    // REGISTRAZIONE
    public void register(String[] params, PrintWriter out) {
        String username = params.length >= 1 ? params[0] : null;
        String password = params.length >= 2 ? params[1] : null;
        // controllo che la password o username non siano vuoti
        if (username == null || password == null) {
            out.println(ERROR_EMPTY_FIELD);
            return;
        }
        // controllo che l'username non sia già stato utilizzato
        if (users.containsKey(username)) {
            out.println(ERROR_USERNAME_TAKEN);
            return;
        }
        // creo nuovo user
        User newUser = new User(username, password);
        // aggiungo il nuovo user nella hash map
        users.put(username, newUser);
        // aggiorno 
        saveUsers(); 

        out.println(SUCCESS);
    }

    // LOGIN
    public String login(String[] params, PrintWriter out) {
        String username = params.length >= 1 ? params[0] : null;
        String password = params.length >= 2 ? params[1] : null;
        // controllo che la password o username non siano vuoti
        if (username == null || password == null) {
            out.println(ERROR_EMPTY_FIELD);
            return null;
        }
        // controllo che l’username esista
        if (!users.containsKey(username)) {
            out.println(ERROR_USERNAME_NOT_FOUND);
            return null;
        }
        // carico lo user associato a quell’username
        User loadUser = users.get(username);
        // controllo che la password sia corretta
        if (!loadUser.getPassword().equals(password)) {
            out.println(ERROR_WRONG_PASSWORD);
            return null;
        }
        out.println(SUCCESS);
        return username;
    }

    // LOGOUT
    public void logout(String[] params, PrintWriter out) {
        String username = params.length >= 1 ? params[0] : null;
        // controllo che la password o username non siano vuoti
        if (username == null) {
            out.println(ERROR_EMPTY_FIELD);
            return;
        }
        // controllo che l’username esista
        if (!users.containsKey(username)) {
            out.println(ERROR_USERNAME_NOT_FOUND);
            return;
        }
        out.println(SUCCESS);
    }

    // SEARCH HOTEL
    public void searchHotel(String[] params, PrintWriter out) {
        String nomeHotel = params.length >= 1 ? params[0] : null;
        String citta = params.length >= 2 ? params[1] : null;
        // controllo che la citta o il nomeHotel non siano vuoti
        if (nomeHotel == null || citta == null) {
            out.println(ERROR_EMPTY_FIELD);
            return;
        }
        Hotel foundHotel = null;
        // cerco l'hotel
        for (Hotel h : hotels) {
            if (h.getName().equals(nomeHotel) && h.getCity().equals(citta)) {
                foundHotel = h;
                break;
            }
        }
        // se l'hotel non esiste lo comunico al client altrmimenti comunico che l'hotel è stato trovato con successo
        if (foundHotel == null) {
            out.println(ERROR_HOTEL_NOT_FOUND);
            return;
        } else {
            out.println(SUCCESS);
        }
        // serializzo
        Gson gson = new Gson();
        String hotelJson = gson.toJson(foundHotel);
        // mando l'hotel al client
        out.println(hotelJson);
    }

    // SEARCH ALL HOTELS
    public void searchAllHotels(String[] params, PrintWriter out) {
        String citta = params.length >= 1 ? params[0] : null;
        // controllo che la citta o il nomeHotel non siano vuoti
        if (citta == null) {
            out.println(ERROR_EMPTY_FIELD);
            return;
        }
        List<Hotel> hotelsFound = new ArrayList<>(); // Inizializza la lista
        // cerco gli hotel
        for (Hotel h : hotels) {
            if (h.getCity().equals(citta)) {
                hotelsFound.add(h);
            }
        }
        // se non esiste nessun hotel non esiste lo comunico al client altrimenti comunico che gli hotel sono stati trovati con successo
        if (hotelsFound.isEmpty()) { 
            out.println(ERROR_HOTEL_NOT_FOUND);
            return;
        } else {
            out.println(SUCCESS);
        }
        // ordino hotel
        hotelsFound.sort((hotel1, hotel2) -> Double.compare(hotel1.getRate(), hotel2.getRate()));
        // serializzo 
        Gson gson = new Gson();
        String hotelsJson = gson.toJson(hotelsFound);
        // mando l'hotel al client
        out.println(hotelsJson);
    }

    // INSERT INTERVIEW
    public void insertReview(String[] params, String loggedUser, PrintWriter out) {
        try {
            String nomeHotel = params.length >= 1 ? params[0] : null; 
            String citta = params.length >= 2 ? params[1] : null;
            Integer globalScore = params.length >= 3 ? Integer.parseInt(params[2]) : null;
            Integer pulizia = params.length >= 4 ? Integer.parseInt(params[3]) : null;
            Integer posizione = params.length >= 5 ? Integer.parseInt(params[4]) : null;
            Integer servizi = params.length >= 6 ? Integer.parseInt(params[5]) : null;
            Integer qualita = params.length >= 7 ? Integer.parseInt(params[6]) : null;
            if (citta == null || nomeHotel == null || globalScore == null || pulizia == null || posizione == null || servizi == null || qualita == null) {
                out.println(ERROR_EMPTY_FIELD);
                return;
            }
            if(!isRateValid(globalScore) || !isRateValid(pulizia) || !isRateValid(posizione) || !isRateValid(servizi) || !isRateValid(qualita)) {
                out.println(ERROR_INVALID_RATE);
                return;
            }
            // cerco lhotel
            Hotel foundHotel = null;
            for (Hotel h : hotels) {
                if (h.getName().equals(nomeHotel) && h.getCity().equals(citta)) {
                    foundHotel = h;
                    break;
                }
            }
            // se l'hotel non esiste lo comunico al client
            if (foundHotel == null) {
                out.println(ERROR_HOTEL_NOT_FOUND);
                return;
            }
            // ricavo l'id dell'hotel
            int idHotel = foundHotel.getId(); // Assumo che esista un metodo getId() nella classe Hotel
            // ricavo la stampa 
            Date timestamp = new Date(); 
            // creo la nuova recensione
            Review newReview = new Review(loggedUser, idHotel, globalScore, pulizia, posizione, servizi, qualita, timestamp);
            // aggiungo la recensione alla lista di recensioni
            reviews.add(newReview);
            // aggiorno
            saveReview(); 
            // aggiorno ranking
            updateHotel(idHotel);
            // la recensione è stata pubblicata con successo
            out.println(SUCCESS);
        } catch (NumberFormatException e) {
            out.println(ERROR_CONVERSION_FAILED);
        }
    }

    public void showMyBadges(String[] params, PrintWriter out) {
        String nomeHotel = params[0];
        String citta = params[1];
        int globalScore = Integer.parseInt(params[2]);
        int pulizia = Integer.parseInt(params[3]);
        int posizione = Integer.parseInt(params[4]);
        int servizi = Integer.parseInt(params[5]);
        int qualita = Integer.parseInt(params[6]);

        out.println("showMy");
    }
}

Mi dici se è tutto sincronizzato correttamente?