import javax.swing.*;
import java.awt.*;
import java.io.*;
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.security.MessageDigest;
import java.util.*;
import java.util.List;

public class ServerCrawler extends JFrame {
    private JTextArea logArea;
    private JButton startButton;
    private JProgressBar progressBar;
    private final Set<String> visitedInMemory = new HashSet<>();
    private final String FILES_DIR = "files";
    private final String TMP_SERVERS = "tmp_servers.txt";

    public ServerCrawler() {
        setTitle("Advanced Server Crawler");
        setSize(750, 550);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLayout(new BorderLayout());

        logArea = new JTextArea();
        logArea.setEditable(false);
        logArea.setFont(new Font("Monospaced", Font.PLAIN, 12));
        add(new JScrollPane(logArea), BorderLayout.CENTER);

        JPanel southPanel = new JPanel(new BorderLayout());
        progressBar = new JProgressBar(0, 100);
        progressBar.setStringPainted(true);
        
        startButton = new JButton("Start Crawling");
        startButton.addActionListener(e -> startProcess());

        southPanel.add(progressBar, BorderLayout.NORTH);
        southPanel.add(startButton, BorderLayout.SOUTH);
        add(southPanel, BorderLayout.SOUTH);

        // Ensure local storage directory exists
        try {
            Files.createDirectories(Paths.get(FILES_DIR));
        } catch (IOException e) {
            log("Error creating files directory.");
        }
    }

    private void log(String message) {
        SwingUtilities.invokeLater(() -> logArea.append(message + "\n"));
    }

    private void updateProgress(int value) {
        SwingUtilities.invokeLater(() -> progressBar.setValue(value));
    }

    private void startProcess() {
        startButton.setEnabled(false);
        visitedInMemory.clear();
        log("--- Starting Process ---");
        
        new SwingWorker<Void, String>() {
            @Override
            protected Void doInBackground() throws Exception {
                File inputFile = new File("servers.txt");
                if (!inputFile.exists()) {
                    publish("Error: local 'servers.txt' not found in root folder.");
                    return null;
                }

                List<String> initialServers = Files.readAllLines(inputFile.toPath());
                for (String url : initialServers) {
                    processServer(url.trim());
                }
                return null;
            }

            private void processServer(String serverUrl) {
                if (serverUrl.isEmpty() || visitedInMemory.contains(serverUrl)) return;
                visitedInMemory.add(serverUrl);
                
                String cleanBaseUrl = serverUrl.replaceAll("/$", "");
                log("Exploring: " + cleanBaseUrl);
                
                appendToTmpServers(cleanBaseUrl);

                // 1. Process files.txt
                try {
                    String filesTxtUrl = cleanBaseUrl + "/files.txt";
                    List<String> lines = fetchLines(filesTxtUrl);
                    for (String line : lines) {
                        String fileUrl = line.trim();
                        if (fileUrl.isEmpty()) continue;

                        // Check if it's a full URL or just a filename
                        if (!fileUrl.toLowerCase().startsWith("http")) {
                            fileUrl = cleanBaseUrl + "/files/" + fileUrl;
                        }
                        downloadAndHashFile(fileUrl);
                    }
                } catch (Exception e) {
                    log("   (No files.txt at " + cleanBaseUrl + ")");
                }

                // 2. Process servers.txt (Recursive)
                try {
                    String remoteServersUrl = cleanBaseUrl + "/servers.txt";
                    List<String> nextServers = fetchLines(remoteServersUrl);
                    for (String nextUrl : nextServers) {
                        processServer(nextUrl.trim());
                    }
                } catch (Exception e) {
                    // No remote servers found, continue
                }
            }

            @Override
            protected void process(List<String> chunks) {
                for (String msg : chunks) log(msg);
            }

            @Override
            protected void done() {
                log("--- All Tasks Complete ---");
                updateProgress(0);
                startButton.setEnabled(true);
            }
        }.execute();
    }

    private List<String> fetchLines(String url) throws Exception {
        HttpClient client = HttpClient.newBuilder()
                .followRedirects(HttpClient.Redirect.ALWAYS)
                .build();
        HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).build();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        
        if (response.statusCode() == 200) {
            return Arrays.asList(response.body().split("\\r?\\n"));
        }
        throw new IOException("HTTP " + response.statusCode());
    }

    private void downloadAndHashFile(String fileUrl) {
        try {
            HttpClient client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build();
            HttpRequest request = HttpRequest.newBuilder().uri(URI.create(fileUrl)).build();
            HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());

            if (response.statusCode() == 200) {
                long contentLength = response.headers().firstValueAsLong("Content-Length").orElse(-1L);
                ByteArrayOutputStream buffer = new ByteArrayOutputStream();
                InputStream is = response.body();
                byte[] data = new byte[8192];
                int nRead;
                long totalRead = 0;

                while ((nRead = is.read(data, 0, data.length)) != -1) {
                    buffer.write(data, 0, nRead);
                    totalRead += nRead;
                    if (contentLength > 0) {
                        updateProgress((int) ((totalRead * 100) / contentLength));
                    }
                }

                byte[] fileBytes = buffer.toByteArray();
                String hash = bytesToHex(MessageDigest.getInstance("SHA-256").digest(fileBytes));
                String ext = getExtension(fileUrl);
                File target = new File(FILES_DIR, hash + ext);

                if (target.exists()) {
                    log("   Skipping (exists): " + target.getName());
                } else {
                    Files.write(target.toPath(), fileBytes);
                    log("   Downloaded: " + target.getName() + " (" + fileUrl + ")");
                }
                updateProgress(0);
            }
        } catch (Exception e) {
            log("   Failed download: " + fileUrl);
        }
    }

    private synchronized void appendToTmpServers(String url) {
        try {
            Path path = Paths.get(TMP_SERVERS);
            boolean existsInFile = false;
            
            if (Files.exists(path)) {
                List<String> lines = Files.readAllLines(path);
                if (lines.contains(url)) {
                    existsInFile = true;
                }
            }

            if (!existsInFile) {
                Files.write(path, (url + System.lineSeparator()).getBytes(), 
                            StandardOpenOption.CREATE, StandardOpenOption.APPEND);
            }
        } catch (IOException e) {
            log("Error logging to " + TMP_SERVERS);
        }
    }

    private String getExtension(String url) {
        String path = URI.create(url).getPath();
        int dotIndex = path.lastIndexOf('.');
        return (dotIndex == -1) ? "" : path.substring(dotIndex);
    }

    private String bytesToHex(byte[] hash) {
        StringBuilder hexString = new StringBuilder();
        for (byte b : hash) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) hexString.append('0');
            hexString.append(hex);
        }
        return hexString.toString();
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new ServerCrawler().setVisible(true));
    }
}