import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * MeshareSync - console tool (Java 8) that decides which "rank.txt" wins among
 * the servers listed in "servers.txt" and writes the servers that hold it to
 * "sync_servers.txt".
 *
 * How it works:
 *   1. Downloads "rank.txt" from every server (baseUrl + "rank.txt").
 *   2. Groups the servers whose rank.txt is the same. Two rank.txt files are
 *      the same when they contain the same valid "<sha256>.<ext>" entries in
 *      the same order (line endings, blank lines, invalid lines, repeated
 *      entries and upper/lower case of the hash are ignored).
 *   3. Scores each group:
 *          points = (number of DIFFERENT IP addresses in the group)
 *                   x (number of entries in the rank.txt)
 *      Several servers on the same IP count as one, so a single machine cannot
 *      inflate the score by being listed many times.
 *   4. The group with the most points wins. On equal points the group with
 *      more different IPs wins, and if that is still equal the group whose
 *      first server appears earlier in servers.txt wins.
 *   5. Every server of the winning group is written to sync_servers.txt, one
 *      URL per line (same format as servers.txt).
 *
 * Servers that are unreachable, have no rank.txt, or whose rank.txt has no
 * valid entries do not take part. If no server takes part, sync_servers.txt is
 * left untouched.
 *
 * Usage:
 *   javac MeshareSync.java
 *   java MeshareSync
 *
 * Run it from the folder that contains servers.txt.
 */
public class MeshareSync {

    private static final String SERVERS_FILE = "servers.txt";
    private static final String SYNC_FILE    = "sync_servers.txt";
    private static final String RANK_NAME    = "rank.txt";

    private static final int CONNECT_TIMEOUT_MS = 10000;
    private static final int READ_TIMEOUT_MS    = 30000;

    // ===================================================================
    //                          model
    // ===================================================================
    /** All servers that published exactly the same rank.txt. */
    private static class Group {
        final int entries;                                        // entries in the rank.txt
        final int firstIndex;                                     // position of its first server in servers.txt
        final List<String> servers = new ArrayList<String>();     // servers.txt order
        final Set<String>  ips     = new LinkedHashSet<String>(); // different IPs

        Group(int entries, int firstIndex) {
            this.entries = entries;
            this.firstIndex = firstIndex;
        }

        long score() { return (long) ips.size() * entries; }
    }

    // ===================================================================
    //                          main
    // ===================================================================
    public static void main(String[] args) {
        File serversFile = new File(SERVERS_FILE);
        if (!serversFile.isFile()) {
            System.err.println("ERROR: " + SERVERS_FILE + " not found in " + new File(".").getAbsolutePath());
            System.exit(1);
            return;
        }

        List<String> rawLines;
        try {
            rawLines = Files.readAllLines(serversFile.toPath(), StandardCharsets.UTF_8);
        } catch (IOException e) {
            System.err.println("ERROR: cannot read " + SERVERS_FILE + ": " + e.getMessage());
            System.exit(1);
            return;
        }

        // Unique, trimmed server entries (blank lines and "#" comments ignored).
        Set<String> servers = new LinkedHashSet<String>();
        for (String line : rawLines) {
            String s = stripBom(line).trim();
            if (s.isEmpty() || s.startsWith("#")) continue;
            servers.add(s);
        }

        if (servers.isEmpty()) {
            System.out.println("No servers found in " + SERVERS_FILE + ". Nothing to do.");
            return;
        }

        System.out.println("Meshare sync - " + servers.size() + " server(s) to read");
        System.out.println("==============================================");

        Map<String, Group> groups = new LinkedHashMap<String, Group>();
        Set<String> allIps = new LinkedHashSet<String>();
        int index = 0;
        for (String server : servers) {
            index++;
            System.out.print("[" + index + "/" + servers.size() + "] " + server + " ... ");

            String baseUrl = normalizeServer(server);
            String content;
            try {
                content = httpGetText(baseUrl + RANK_NAME);
            } catch (IOException e) {
                System.out.println("SKIPPED (" + e.getMessage() + ")");
                continue;
            }

            Set<String> entries = parseRank(content);
            if (entries.isEmpty()) {
                System.out.println("SKIPPED (" + RANK_NAME + " has no valid entries)");
                continue;
            }

            String ip = resolveIp(baseUrl);
            String key = joinLines(entries);
            Group g = groups.get(key);
            if (g == null) {
                g = new Group(entries.size(), index);
                groups.put(key, g);
            }
            g.servers.add(server);
            g.ips.add(ip);
            allIps.add(ip);
            System.out.println(entries.size() + " entries, IP " + ip);
        }

        if (groups.isEmpty()) {
            System.err.println();
            System.err.println("ERROR: no server published a usable " + RANK_NAME
                    + ". " + SYNC_FILE + " was NOT changed.");
            System.exit(1);
            return;
        }

        // Best group first: points, then more different IPs, then earlier in servers.txt.
        List<Group> ranked = new ArrayList<Group>(groups.values());
        Collections.sort(ranked, new Comparator<Group>() {
            @Override public int compare(Group a, Group b) {
                int byScore = Long.compare(b.score(), a.score());
                if (byScore != 0) return byScore;
                int byIps = Integer.compare(b.ips.size(), a.ips.size());
                if (byIps != 0) return byIps;
                return Integer.compare(a.firstIndex, b.firstIndex);
            }
        });

        System.out.println();
        System.out.println("==============================================");
        System.out.println("Groups of identical " + RANK_NAME + " (best first):");
        int pos = 0;
        for (Group g : ranked) {
            pos++;
            System.out.println("  #" + pos + "  " + g.score() + " points = "
                    + g.ips.size() + " different IP(s) x " + g.entries + " entries"
                    + "  [" + g.servers.size() + " server(s)]");
            for (String s : g.servers) System.out.println("        " + s);
        }

        Group winner = ranked.get(0);
        try {
            writeServers(new File(SYNC_FILE), winner.servers);
        } catch (IOException e) {
            System.err.println("ERROR: could not write " + SYNC_FILE + ": " + e.getMessage());
            System.exit(1);
            return;
        }

        System.out.println();
        System.out.println("Winner: group #1 with " + winner.score() + " points, held by "
                + winner.ips.size() + " of " + allIps.size() + " different IP(s) that published a "
                + RANK_NAME + ".");
        System.out.println(winner.servers.size() + " server(s) written to " + SYNC_FILE);
    }

    // ===================================================================
    //                          rank.txt handling
    // ===================================================================
    /** Valid entries in file order; repeated entries keep only their first position. */
    private static Set<String> parseRank(String content) {
        Set<String> entries = new LinkedHashSet<String>();
        for (String line : content.split("\\r?\\n")) {
            String name = canonicalName(stripBom(line));
            if (name != null) entries.add(name);
        }
        return entries;
    }

    private static String joinLines(Set<String> entries) {
        StringBuilder sb = new StringBuilder();
        for (String e : entries) sb.append(e).append('\n');
        return sb.toString();
    }

    /** The IP address the server URL resolves to (falls back to the host text if it cannot be resolved). */
    private static String resolveIp(String baseUrl) {
        String host = baseUrl;
        try {
            host = new URL(baseUrl).getHost();
            return InetAddress.getByName(host).getHostAddress();
        } catch (Exception e) {
            return host;
        }
    }

    // ===================================================================
    //                          output
    // ===================================================================
    /** Writes one server per line, via a temp file so a failure cannot leave a half-written file. */
    private static void writeServers(File file, List<String> servers) throws IOException {
        File tmp = new File(file.getAbsolutePath() + ".tmp");
        Files.write(tmp.toPath(), servers, StandardCharsets.UTF_8);
        Files.move(tmp.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING);
    }

    // ===================================================================
    //                          HTTP
    // ===================================================================
    private static String httpGetText(String urlStr) throws IOException {
        HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection();
        conn.setRequestMethod("GET");
        conn.setConnectTimeout(CONNECT_TIMEOUT_MS);
        conn.setReadTimeout(READ_TIMEOUT_MS);
        conn.setRequestProperty("User-Agent", "Meshare/1.0");
        conn.setRequestProperty("Accept", "*/*");
        conn.setInstanceFollowRedirects(true);
        BufferedReader r = null;
        try {
            int code = conn.getResponseCode();
            if (code < 200 || code >= 300) throw new IOException("HTTP " + code);
            r = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            char[] buf = new char[8192];
            int n;
            while ((n = r.read(buf)) >= 0) sb.append(buf, 0, n);
            return sb.toString();
        } finally {
            if (r != null) try { r.close(); } catch (IOException e) { /* ignore */ }
            conn.disconnect();
        }
    }

    // ===================================================================
    //                          helpers (same rules as Meshare)
    // ===================================================================
    private static String stripBom(String s) {
        if (s != null && !s.isEmpty() && s.charAt(0) == '\uFEFF') return s.substring(1);
        return s;
    }

    private static String normalizeServer(String url) {
        url = url.trim();
        if (url.isEmpty()) return url;
        if (url.endsWith("/")) return url;
        int q = url.indexOf('?');
        int h = url.indexOf('#');
        int cut = -1;
        if (q >= 0) cut = q;
        if (h >= 0 && (cut < 0 || h < cut)) cut = h;
        if (cut >= 0) return url;
        return url + "/";
    }

    /**
     * Returns "<lowercase hash>.<ext>" for a valid "<sha256>.<ext>" line, or
     * null if the line is blank or not a valid entry.
     */
    private static String canonicalName(String line) {
        if (line == null) return null;
        line = line.trim();
        if (line.isEmpty()) return null;
        int dot = line.lastIndexOf('.');
        if (dot <= 0 || dot >= line.length() - 1) return null;
        String hash = line.substring(0, dot);
        String ext  = line.substring(dot + 1);
        if (!isValidSha256(hash)) return null;
        if (ext.indexOf('/') >= 0 || ext.indexOf('\\') >= 0) return null;
        if (ext.length() > 16) return null;
        return hash.toLowerCase() + "." + ext;
    }

    private static boolean isValidSha256(String s) {
        if (s == null || s.length() != 64) return false;
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            boolean ok = (c >= '0' && c <= '9')
                      || (c >= 'a' && c <= 'f')
                      || (c >= 'A' && c <= 'F');
            if (!ok) return false;
        }
        return true;
    }
}
