import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
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.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * MeshareRank - console tool (Java 8) that builds "rank.txt" from the servers
 * listed in "servers.txt".
 *
 * For each server it downloads "files.txt" and counts, for every
 * "<sha256>.<ext>" entry, in how many servers it appears (a server counts
 * once per file, even if the file is listed twice there).
 *
 * Only files present in the MAJORITY of the servers are kept, i.e. in more
 * than half of the servers whose files.txt could be fetched. Servers that are
 * unreachable cannot vote, so they are not counted in that total.
 *
 * rank.txt contains just the file names ("<hash>.<ext>"), one per line,
 * ordered by number of servers (highest first). When two files are in the same
 * number of servers, the one whose name comes first alphabetically goes first.
 *
 * Usage:
 *   javac MeshareRank.java
 *   java MeshareRank
 *
 * Run it from the folder that contains servers.txt.
 */
public class MeshareRank {

    private static final String SERVERS_FILE = "servers.txt";
    private static final String RANK_FILE    = "rank.txt";

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

    // ===================================================================
    //                          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 rank - " + servers.size() + " server(s) to read");
        System.out.println("==============================================");

        Map<String, Integer> counts = new HashMap<String, Integer>();
        int reachable = 0;
        int n = 0;
        for (String server : servers) {
            n++;
            System.out.print("[" + n + "/" + servers.size() + "] " + server + " ... ");
            String content;
            try {
                content = httpGetText(normalizeServer(server) + "files.txt");
            } catch (IOException e) {
                System.out.println("SKIPPED (" + e.getMessage() + ")");
                continue;
            }
            reachable++;

            // Each server votes once per file.
            Set<String> names = new LinkedHashSet<String>();
            for (String line : content.split("\\r?\\n")) {
                String name = canonicalName(stripBom(line));
                if (name != null) names.add(name);
            }
            for (String name : names) {
                Integer c = counts.get(name);
                counts.put(name, c == null ? 1 : c + 1);
            }
            System.out.println(names.size() + " file(s)");
        }

        if (reachable == 0) {
            System.err.println();
            System.err.println("ERROR: no server could be reached. " + RANK_FILE + " was NOT changed.");
            System.exit(1);
            return;
        }

        // Majority = more than half of the servers that answered.
        final Map<String, Integer> finalCounts = counts;
        List<String> ranked = new ArrayList<String>();
        for (Map.Entry<String, Integer> e : counts.entrySet()) {
            if (e.getValue() * 2 > reachable) ranked.add(e.getKey());
        }
        Collections.sort(ranked, new Comparator<String>() {
            @Override public int compare(String a, String b) {
                int byCount = finalCounts.get(b).compareTo(finalCounts.get(a)); // most servers first
                if (byCount != 0) return byCount;
                return a.compareTo(b);                                          // tie: name order
            }
        });

        try {
            writeRank(new File(RANK_FILE), ranked);
        } catch (IOException e) {
            System.err.println("ERROR: could not write " + RANK_FILE + ": " + e.getMessage());
            System.exit(1);
            return;
        }

        System.out.println();
        System.out.println("==============================================");
        System.out.println(reachable + " of " + servers.size() + " server(s) reached; majority = at least "
                + (reachable / 2 + 1) + " server(s).");
        System.out.println(ranked.size() + " file(s) written to " + RANK_FILE);
    }

    // ===================================================================
    //                          output
    // ===================================================================
    /** Writes one name per line, via a temp file so a failure cannot leave a half-written rank.txt. */
    private static void writeRank(File file, List<String> names) throws IOException {
        File tmp = new File(file.getAbsolutePath() + ".tmp");
        Files.write(tmp.toPath(), names, 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. The hash is lowercased
     * so the same file written in upper and lower case on different servers
     * is counted as one file.
     */
    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;
    }
}
