package org.openautonomousconnection.webserver;

import org.openautonomousconnection.protocol.annotations.ProtocolInfo;
import org.openautonomousconnection.protocol.packets.v1_0_0.beta.*;
import org.openautonomousconnection.protocol.side.web.ConnectedWebClient;
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
import org.openautonomousconnection.protocol.side.web.managers.RuleManager;
import org.openautonomousconnection.protocol.side.web.managers.SessionManager;
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
import org.openautonomousconnection.webserver.api.SessionContext;
import org.openautonomousconnection.webserver.runtime.JavaPageDispatcher;

import java.io.*;
import java.nio.file.Files;
import java.util.*;

@ProtocolInfo(protocolSide = ProtocolVersion.ProtocolSide.WEB)
public final class WebServer extends ProtocolWebServer {

    private static final int STREAM_CHUNK_SIZE = 64 * 1024;
    private static final long STREAM_THRESHOLD = 2L * 1024 * 1024;

    public WebServer(File configFile, File authFile, File rulesFile) throws Exception {
        super(configFile, authFile, rulesFile);
        Main.getProtocolBridge().getProtocolSettings().port = getConfigurationManager().getInt("port.webserver");
    }

    @Override
    public WebResponsePacket onWebRequest(ConnectedWebClient client, WebRequestPacket request) {
        try {
            String path = request.getPath() == null ? "/" : request.getPath();

            if (RuleManager.isDenied(path)) return new WebResponsePacket(403, "text/plain", Map.of(), "Forbidden".getBytes());

            if (RuleManager.requiresAuth(path)) {
                SessionContext ctx = SessionContext.from(client, this, request.getHeaders());
                if (!ctx.isValid()) return new WebResponsePacket(401, "text/plain", Map.of(), "Authentication required".getBytes());
            }

            WebResponsePacket javaResp = JavaPageDispatcher.dispatch(client, this, request);
            if (javaResp != null) return javaResp;

            return serveFile(client, path);

        } catch (Exception e) {
            return new WebResponsePacket(500, "text/plain", Map.of(), ("Internal Error: " + e.getMessage()).getBytes());
        }
    }

    private WebResponsePacket serveFile(ConnectedWebClient client, String path) throws Exception {
        if (path.startsWith("/")) path = path.substring(1);
        if (path.isEmpty()) path = "index.html";

        File root = getContentFolder().getCanonicalFile();
        File file = new File(root, path).getCanonicalFile();

        if (!file.getPath().startsWith(root.getPath())) return new WebResponsePacket(403, "text/plain", Map.of(), "Forbidden".getBytes());
        if (!file.exists() || !file.isFile()) return new WebResponsePacket(404, "text/plain", Map.of(), "Not found".getBytes());

        String contentType = ContentTypeResolver.resolve(file.getName());
        long size = file.length();

        if (size >= STREAM_THRESHOLD) {
            streamFile(client, file, contentType);
            return null;
        }

        byte[] data = Files.readAllBytes(file.toPath());
        return new WebResponsePacket(200, contentType, Map.of(), data);
    }

    private void streamFile(ConnectedWebClient client, File file, String contentType) throws IOException {
        long total = file.length();

        client.streamStart(new WebStreamStartPacket(200, contentType, Map.of("name", file.getName()), total));

        try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
            byte[] buf = new byte[STREAM_CHUNK_SIZE];
            int seq = 0;
            int r;
            while ((r = in.read(buf)) != -1) {
                byte[] chunk = (r == buf.length) ? buf : Arrays.copyOf(buf, r);
                client.streamChunk(new WebStreamChunkPacket(seq++, chunk));
            }
        }

        client.streamEnd(new WebStreamEndPacket(true));
    }
}
