package com.foxx.androidcast.remoteaccess.control;

import android.content.Context;
import android.content.res.AssetManager;
import android.util.Log;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;

/** Template HTTP server for RA control UI, JSON API, and WebSocket shell. */
final class RemoteAccessControlHttpServer implements Runnable {
    private static final String TAG = "RaControlHttp";
    private static final String ASSET_PREFIX = "ra_control/";

    interface HostBridge {
        String getSettingsJson();

        String applySettingsJson(String body);

        String getInternalsJson();
    }

    private final Context context;
    private final AtomicReference<String> authToken = new AtomicReference<>("");
    private final HostBridge hostBridge;
    private final ExecutorService clients = Executors.newCachedThreadPool(r -> {
        Thread t = new Thread(r, "RaControlHttpClient");
        t.setDaemon(true);
        return t;
    });

    private volatile ServerSocket serverSocket;
    private volatile Thread acceptThread;
    private volatile int boundPort;

    RemoteAccessControlHttpServer(Context context, HostBridge hostBridge) {
        this.context = context.getApplicationContext();
        this.hostBridge = hostBridge;
    }

    synchronized int start(int preferredPort) throws IOException {
        stopInternal();
        IOException last = null;
        for (int attempt = 0; attempt < 32; attempt++) {
            int port = attempt == 0 && preferredPort > 0 ? preferredPort : pickRandomPort();
            try (ServerSocket probe = new ServerSocket()) {
                probe.setReuseAddress(true);
                probe.bind(new InetSocketAddress(port));
            } catch (IOException e) {
                last = e;
                continue;
            }
            ServerSocket socket = new ServerSocket();
            socket.setReuseAddress(true);
            socket.bind(new InetSocketAddress(port));
            serverSocket = socket;
            boundPort = port;
            acceptThread = new Thread(this, "RaControlHttp");
            acceptThread.setDaemon(true);
            acceptThread.start();
            Log.i(TAG, "listening on :" + port);
            return port;
        }
        if (last != null) {
            throw last;
        }
        throw new IOException("failed to bind RA control HTTP port");
    }

    synchronized void stop() {
        stopInternal();
    }

    int getBoundPort() {
        return boundPort;
    }

    void setAuthToken(String token) {
        authToken.set(token != null ? token : "");
    }

    @Override
    public void run() {
        ServerSocket socket = serverSocket;
        if (socket == null) {
            return;
        }
        while (!Thread.currentThread().isInterrupted()) {
            try {
                Socket client = socket.accept();
                clients.execute(() -> handleClient(client));
            } catch (IOException e) {
                if (!Thread.currentThread().isInterrupted()) {
                    Log.d(TAG, "accept stopped: " + e.getMessage());
                }
                break;
            }
        }
    }

    private void handleClient(Socket client) {
        try {
            client.setSoTimeout(120_000);
            byte[] headerBytes = readHeaders(client.getInputStream());
            if (headerBytes == null) {
                return;
            }
            String headers = new String(headerBytes, StandardCharsets.US_ASCII);
            HttpRequest request = parseRequest(headers, client.getInputStream());
            if (request == null) {
                writeResponse(client, 400, "text/plain", "bad request");
                return;
            }
            route(client, request, headers);
        } catch (Exception e) {
            Log.d(TAG, "client error: " + e.getMessage());
        } finally {
            try {
                client.close();
            } catch (IOException ignored) {
            }
        }
    }

    private void route(Socket client, HttpRequest request, String rawHeaders) throws IOException {
        String expected = authToken.get();
        String queryToken = RemoteAccessControlAuth.extractQueryToken(request.path, request.query);
        String authHeader = request.headers.getOrDefault("authorization", "");
        boolean authorized = RemoteAccessControlAuth.isAuthorized(expected, authHeader, queryToken);

        String path = RemoteAccessControlAuth.normalizePath(request.path);
        if ("/ws/shell".equals(path)) {
            if (!authorized) {
                writeResponse(client, 401, "text/plain", "unauthorized");
                return;
            }
            if (!"GET".equalsIgnoreCase(request.method)) {
                writeResponse(client, 405, "text/plain", "method not allowed");
                return;
            }
            if (!rawHeaders.toLowerCase(Locale.US).contains("upgrade: websocket")) {
                writeResponse(client, 400, "text/plain", "expected websocket upgrade");
                return;
            }
            new RemoteAccessControlShellHandler(client, rawHeaders).run();
            return;
        }

        if (path.startsWith("/static/")) {
            serveAsset(client, path.substring("/static/".length()), "GET".equalsIgnoreCase(request.method));
            return;
        }

        if (!authorized && !"/".equals(path) && !path.endsWith(".css")) {
            writeResponse(client, 401, "application/json", "{\"ok\":false,\"error\":\"unauthorized\"}");
            return;
        }

        if ("/api/settings".equals(path)) {
            handleSettingsApi(client, request);
            return;
        }
        if ("/api/internals".equals(path)) {
            handleInternalsApi(client, request);
            return;
        }
        if ("/shell".equals(path) && "GET".equalsIgnoreCase(request.method)) {
            serveAsset(client, "shell.html", true, expected);
            return;
        }
        if (("/".equals(path) || "/index.html".equals(path)) && "GET".equalsIgnoreCase(request.method)) {
            serveAsset(client, "index.html", true, expected);
            return;
        }
        writeResponse(client, 404, "text/plain", "not found");
    }

    private void handleSettingsApi(Socket client, HttpRequest request) throws IOException {
        if ("GET".equalsIgnoreCase(request.method)) {
            String json = hostBridge.getSettingsJson();
            writeResponse(client, 200, "application/json; charset=utf-8", json != null ? json : "{}");
            return;
        }
        if ("POST".equalsIgnoreCase(request.method)) {
            String body = request.body != null ? request.body : "";
            String json = hostBridge.applySettingsJson(body);
            writeResponse(client, 200, "application/json; charset=utf-8", json != null ? json : "{}");
            return;
        }
        writeResponse(client, 405, "text/plain", "method not allowed");
    }

    private void handleInternalsApi(Socket client, HttpRequest request) throws IOException {
        if (!"GET".equalsIgnoreCase(request.method)) {
            writeResponse(client, 405, "text/plain", "method not allowed");
            return;
        }
        String json = hostBridge.getInternalsJson();
        writeResponse(client, 200, "application/json; charset=utf-8", json != null ? json : "{}");
    }

    private void serveAsset(Socket client, String assetName, boolean getOnly) throws IOException {
        serveAsset(client, assetName, getOnly, authToken.get());
    }

    private void serveAsset(Socket client, String assetName, boolean getOnly, String token) throws IOException {
        if (!getOnly) {
            writeResponse(client, 405, "text/plain", "method not allowed");
            return;
        }
        String safeName = assetName.replace("..", "").replace('\\', '/');
        if (safeName.startsWith("/")) {
            safeName = safeName.substring(1);
        }
        AssetManager assets = context.getAssets();
        String assetPath = ASSET_PREFIX + safeName;
        try (InputStream in = assets.open(assetPath)) {
            byte[] bytes = readAll(in);
            String contentType = contentTypeFor(safeName);
            if (safeName.endsWith(".html")) {
                String html = new String(bytes, StandardCharsets.UTF_8);
                html = html.replace("__AUTH_TOKEN__", token != null ? token : "");
                html = html.replace("__LISTEN_PORT__", String.valueOf(boundPort));
                bytes = html.getBytes(StandardCharsets.UTF_8);
            }
            writeResponseBytes(client, 200, contentType, bytes);
        } catch (IOException e) {
            writeResponse(client, 404, "text/plain", "not found");
        }
    }

    private static int pickRandomPort() {
        int base = 32768;
        int span = 65535 - base;
        return base + (int) (Math.random() * span);
    }

    private synchronized void stopInternal() {
        Thread accept = acceptThread;
        acceptThread = null;
        ServerSocket socket = serverSocket;
        serverSocket = null;
        boundPort = 0;
        if (socket != null) {
            try {
                socket.close();
            } catch (IOException ignored) {
            }
        }
        if (accept != null) {
            accept.interrupt();
        }
    }

    private static byte[] readHeaders(InputStream in) throws IOException {
        ByteArrayOutputStream buf = new ByteArrayOutputStream(2048);
        int state = 0;
        while (true) {
            int b = in.read();
            if (b < 0) {
                return null;
            }
            buf.write(b);
            switch (state) {
                case 0:
                    state = b == '\r' ? 1 : 0;
                    break;
                case 1:
                    state = b == '\n' ? 2 : 0;
                    break;
                case 2:
                    state = b == '\r' ? 3 : 0;
                    break;
                case 3:
                    if (b == '\n') {
                        return buf.toByteArray();
                    }
                    state = 0;
                    break;
                default:
                    state = 0;
            }
            if (buf.size() > 65536) {
                return null;
            }
        }
    }

    static HttpRequest parseRequest(String headersBlock, InputStream bodyIn) throws IOException {
        int headerEnd = headersBlock.indexOf("\r\n\r\n");
        if (headerEnd < 0) {
            return null;
        }
        String headerSection = headersBlock.substring(0, headerEnd);
        String body = headersBlock.length() > headerEnd + 4
                ? headersBlock.substring(headerEnd + 4)
                : "";
        String[] lines = headerSection.split("\r\n");
        if (lines.length == 0) {
            return null;
        }
        String[] parts = lines[0].split(" ");
        if (parts.length < 2) {
            return null;
        }
        String method = parts[0];
        String rawPath = parts[1];
        Map<String, String> query = new HashMap<>();
        String path = rawPath;
        int q = rawPath.indexOf('?');
        if (q >= 0) {
            path = rawPath.substring(0, q);
            for (String pair : rawPath.substring(q + 1).split("&")) {
                int eq = pair.indexOf('=');
                if (eq > 0) {
                    query.put(pair.substring(0, eq), pair.substring(eq + 1));
                }
            }
        }
        Map<String, String> headers = new HashMap<>();
        for (int i = 1; i < lines.length; i++) {
            int colon = lines[i].indexOf(':');
            if (colon > 0) {
                headers.put(lines[i].substring(0, colon).trim().toLowerCase(Locale.US),
                        lines[i].substring(colon + 1).trim());
            }
        }
        int contentLength = 0;
        if (headers.containsKey("content-length")) {
            try {
                contentLength = Integer.parseInt(headers.get("content-length"));
            } catch (NumberFormatException ignored) {
            }
        }
        if (contentLength > 0) {
            byte[] bodyBytes = new byte[contentLength];
            int already = Math.min(body.getBytes(StandardCharsets.UTF_8).length, contentLength);
            if (already > 0) {
                byte[] prefix = body.getBytes(StandardCharsets.UTF_8);
                System.arraycopy(prefix, 0, bodyBytes, 0, already);
            }
            int off = already;
            while (off < contentLength) {
                int read = bodyIn.read(bodyBytes, off, contentLength - off);
                if (read < 0) {
                    return null;
                }
                off += read;
            }
            body = new String(bodyBytes, StandardCharsets.UTF_8);
        }
        return new HttpRequest(method, path, query, headers, body);
    }

    static final class HttpRequest {
        final String method;
        final String path;
        final Map<String, String> query;
        final Map<String, String> headers;
        final String body;

        HttpRequest(
                String method,
                String path,
                Map<String, String> query,
                Map<String, String> headers,
                String body) {
            this.method = method;
            this.path = path;
            this.query = query;
            this.headers = headers;
            this.body = body;
        }
    }

    private static void writeResponse(Socket client, int code, String contentType, String body)
            throws IOException {
        writeResponseBytes(client, code, contentType, body.getBytes(StandardCharsets.UTF_8));
    }

    private static void writeResponseBytes(Socket client, int code, String contentType, byte[] bytes)
            throws IOException {
        String status = code == 200 ? "200 OK"
                : code == 401 ? "401 Unauthorized"
                : code == 404 ? "404 Not Found"
                : code == 405 ? "405 Method Not Allowed"
                : "400 Bad Request";
        String headers = "HTTP/1.1 " + status + "\r\n"
                + "Content-Type: " + contentType + "\r\n"
                + "Content-Length: " + bytes.length + "\r\n"
                + "Connection: close\r\n\r\n";
        OutputStream out = client.getOutputStream();
        out.write(headers.getBytes(StandardCharsets.US_ASCII));
        out.write(bytes);
        out.flush();
    }

    private static String contentTypeFor(String name) {
        if (name.endsWith(".css")) {
            return "text/css; charset=utf-8";
        }
        if (name.endsWith(".js")) {
            return "application/javascript; charset=utf-8";
        }
        if (name.endsWith(".html")) {
            return "text/html; charset=utf-8";
        }
        return "application/octet-stream";
    }

    private static byte[] readAll(InputStream in) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] buf = new byte[4096];
        int read;
        while ((read = in.read(buf)) >= 0) {
            out.write(buf, 0, read);
        }
        return out.toByteArray();
    }
}
