package demo.server;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Executors;

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;

import demo.routing.Routing;
import es.redsys.lib.Utils;

public class Server {
	
    public static void start() throws Exception {
        int port = availablePort(8080);
        
		HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", port), 0);

        // Pool de hilos
        server.setExecutor(Executors.newFixedThreadPool(
                Runtime.getRuntime().availableProcessors()));    

        // Rutas
        Routing.routing(server);
        
        System.out.println("Servidor escuchando en http://localhost:" + port + " - http://127.0.0.1:" + port);
        server.start();
    }
    
    private static int availablePort(int startingPort) {
        int port = startingPort;

        while (true) {
            try (java.net.ServerSocket serverSocket = new java.net.ServerSocket(port)) {
                return port; // Si llega aquí, el puerto está libre
            } catch (Exception e) {
                port++; // Puerto ocupado → probar el siguiente
            }
        }
    }
    
	public static void healtcheck(HttpExchange exchange) throws IOException {
		if ("GET".equalsIgnoreCase(exchange.getRequestMethod())) {
		    textResponse(exchange, "ok");
		}
	}
	
	public static void textResponse(HttpExchange exchange, String text) throws IOException {
		byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
	    exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=UTF-8");
	    exchange.sendResponseHeaders(200, bytes.length);
	    try (OutputStream os = exchange.getResponseBody()) {
	        os.write(bytes);
	    }
	}
	
	public static void serverRedirect(HttpExchange exchange, String location, int status) throws IOException {
	    exchange.getResponseHeaders().set("Location", location);
	    exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=UTF-8");
	    exchange.sendResponseHeaders(status, 0);
	    exchange.close();
	}
	
	public static void renderHtml(HttpExchange exchange, String html) throws IOException {
		byte[] bytes = html.getBytes(StandardCharsets.UTF_8);
	    exchange.getResponseHeaders().add("Content-Type", "text/html; charset=UTF-8");
	    exchange.sendResponseHeaders(200, bytes.length);
	    try (OutputStream os = exchange.getResponseBody()) {
	        os.write(bytes);
	    }
	}
	
	public static void jsonResponse(HttpExchange exchange, int statusCode, String json) throws IOException {
        byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
        exchange.getResponseHeaders().set("Content-Type", "application/json; charset=UTF-8");
        exchange.sendResponseHeaders(statusCode, bytes.length);
        
        try (OutputStream os = exchange.getResponseBody()) {
            os.write(bytes);
        }
	}

    public static void handleStatic(HttpExchange exchange) throws IOException {
        addCors(exchange);
        if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
            sendNoContent(exchange);
            return;
        }
        if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
            sendMethodNotAllowed(exchange);
            return;
        }

	    String path = exchange.getRequestURI().getPath();
	    String relativePath = path.substring(1);
	
	    if (Arrays.asList("frontend/", "frontend").contains(relativePath)) {
            exchange.getResponseHeaders().set("Location", "/index.html");
            exchange.sendResponseHeaders(302, -1);
            exchange.close();
            return;
	    }

        Path file = Paths.get("static", path.substring(1));
        if (!Files.exists(file) || !Files.isRegularFile(file)) {
            sendNotFound(exchange);
            return;
        }

        String mime = guessMime(file);
        byte[] bytes = Files.readAllBytes(file);
        exchange.getResponseHeaders().set("Content-Type", mime);
        exchange.sendResponseHeaders(200, bytes.length);
        try (OutputStream os = exchange.getResponseBody()) {
            os.write(bytes);
        }
    }

    // Utilidades

    public static void addCors(HttpExchange exchange) {
        Headers h = exchange.getResponseHeaders();
        h.set("Access-Control-Allow-Origin", "*");
        h.set("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
        h.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
    }

    public static void sendNoContent(HttpExchange ex) throws IOException {
        ex.sendResponseHeaders(204, -1);
        ex.close();
    }

    public static void sendMethodNotAllowed(HttpExchange ex) throws IOException {
        byte[] msg = "Method Not Allowed".getBytes(StandardCharsets.UTF_8);
        ex.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
        ex.sendResponseHeaders(405, msg.length);
        try (OutputStream os = ex.getResponseBody()) { os.write(msg); }
    }

    public static void sendNotFound(HttpExchange ex) throws IOException {
        byte[] msg = "Not Found".getBytes(StandardCharsets.UTF_8);
        ex.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
        ex.sendResponseHeaders(404, msg.length);
        try (OutputStream os = ex.getResponseBody()) { os.write(msg); }
    }

    public static byte[] readAllBytes(InputStream in) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        byte[] buf = new byte[8192];
        int r;
        while ((r = in.read(buf)) != -1) bos.write(buf, 0, r);
        return bos.toByteArray();
    }

    public static String guessMime(Path file) {
        String name = file.getFileName().toString().toLowerCase(Locale.ROOT);
        if (name.endsWith(".html")) return "text/html; charset=utf-8";
        if (name.endsWith(".js"))   return "application/javascript; charset=utf-8";
        if (name.endsWith(".css"))  return "text/css; charset=utf-8";
        if (name.endsWith(".json")) return "application/json; charset=utf-8";
        if (name.endsWith(".png"))  return "image/png";
        if (name.endsWith(".jpg") || name.endsWith(".jpeg")) return "image/jpeg";
        return "application/octet-stream";
    }

	public static Map<String, String> getQueryParams(HttpExchange exchange) throws UnsupportedEncodingException {
	    Map<String, String> params = new LinkedHashMap<>();
	    URI uri = exchange.getRequestURI();
	    String query = uri.getRawQuery();
	
	    if (query != null && !query.isEmpty()) {
	        for (String pair : query.split("&")) {
	            int idx = pair.indexOf('=');
	            String key = idx > 0 ? pair.substring(0, idx) : pair;
	            String value = idx > 0 ? pair.substring(idx + 1) : "";
	            key = URLDecoder.decode(key, StandardCharsets.UTF_8.name());
	            value = URLDecoder.decode(value, StandardCharsets.UTF_8.name());
	            params.put(key, value);
	        }
	    }
	    return params;
	}

	public static Map<String, Object> getFormParams(HttpExchange exchange) throws IOException {
	    // Solo procesa si es POST (o PUT) y el content-type es form-urlencoded
	    String method = exchange.getRequestMethod();
	    if (!"POST".equalsIgnoreCase(method) && !"PUT".equalsIgnoreCase(method)) {
	        return Collections.emptyMap();
	    }
	
	    String contentType = Optional.ofNullable(exchange.getRequestHeaders().getFirst("Content-Type"))
	                                 .orElse("");
	    // Ejemplos de cabecera: "application/x-www-form-urlencoded; charset=UTF-8"
	    boolean isForm = contentType.toLowerCase(Locale.ROOT).startsWith("application/x-www-form-urlencoded");
	
	    if (!isForm) {
	        return Collections.emptyMap(); // o lanzar excepción, según tu caso
	    }
	
	    Charset charset = extractCharset(contentType).orElse(StandardCharsets.UTF_8);
	    String body = readBody(exchange.getRequestBody(), charset);
	
	    Map<String, Object> params = new LinkedHashMap<>();
	    if (!body.isEmpty()) {
	        for (String pair : body.split("&")) {
	            int idx = pair.indexOf('=');
	            String key = idx > 0 ? pair.substring(0, idx) : pair;
	            String value = idx > 0 ? pair.substring(idx + 1) : "";
	            key = URLDecoder.decode(key, charset.name());
	            value = URLDecoder.decode(value.replace('+', ' '), charset.name()); // '+' es espacio en form-urlencoded
	            params.put(key, value);
	        }
	    }
	    return params;
	}
	
	private static Optional<Charset> extractCharset(String contentType) {
	    // Busca charset=... en el content-type
	    for (String part : contentType.split(";")) {
	        String p = part.trim().toLowerCase(Locale.ROOT);
	        if (p.startsWith("charset=")) {
	            String cs = p.substring("charset=".length()).trim();
	            try {
	                return Optional.of(Charset.forName(cs));
	            } catch (Exception ignore) {}
	        }
	    }
	    return Optional.empty();
	}
	
	private static String readBody(InputStream is, Charset charset) throws IOException {
	    try (BufferedReader br = new BufferedReader(new InputStreamReader(is, charset))) {
	        StringBuilder sb = new StringBuilder();
	        char[] buf = new char[4096];
	        int n;
	        while ((n = br.read(buf)) != -1) {
	            sb.append(buf, 0, n);
	        }
	        return sb.toString();
	    }
	}

	public static Map<String, Object> getJsonParams(HttpExchange exchange) throws IOException {
	    String method = exchange.getRequestMethod();
	    if (!"POST".equalsIgnoreCase(method) && !"PUT".equalsIgnoreCase(method)) {
	        throw new IOException("Método no soportado para JSON body: " + method);
	    }
	
	    String contentType = Optional.ofNullable(exchange.getRequestHeaders().getFirst("Content-Type"))
	                                 .orElse("");
	    if (!contentType.toLowerCase(Locale.ROOT).contains("application/json")) {
	        throw new IOException("Content-Type esperado application/json, recibido: " + contentType);
	    }
	
	    Charset charset = extractCharset(contentType).orElse(StandardCharsets.UTF_8);
	    String body = readBody(exchange.getRequestBody(), charset);
	
	    return Utils.jsonToMap(body);
	}
	
	/* Current URL */
    /** Reconstruye la URL completa teniendo en cuenta reverse proxy y cabeceras estándar. */
    public static String getCurrentUrl(HttpExchange exchange) {
        String schemeHost = getSchemeAndHost(exchange);

        URI uri = exchange.getRequestURI();
        String pathAndQuery = uri.getRawPath();
        String rawQuery = uri.getRawQuery();
        if (rawQuery != null && !rawQuery.isEmpty()) {
            pathAndQuery += "?" + rawQuery;
        }

        return schemeHost + pathAndQuery;
    }

    /** Reconstruye la URL completa teniendo en cuenta reverse proxy y cabeceras estándar. */
    public static String getSchemeAndHost(HttpExchange exchange) {
        String scheme = detectScheme(exchange);
        String host = extractHost(exchange);
        return scheme + "://" + host;
    }
    
    /** Heurística de esquema: soporta proxies que añaden X-Forwarded-Proto / Forwarded. */
    private static String detectScheme(HttpExchange exchange) {
        String xfProto = header(exchange, "X-Forwarded-Proto");
        if (xfProto != null) {
            // Puede venir "https,http" o "https"; coge el primero
            String first = xfProto.split(",")[0].trim().toLowerCase(Locale.ROOT);
            if ("https".equals(first)) return "https";
            if ("http".equals(first))  return "http";
        }

        // Cabecera estándar RFC 7239: Forwarded: proto=https; host=example.com
        String forwarded = header(exchange, "Forwarded");
        if (forwarded != null) {
            String[] parts = forwarded.split(";");
            for (String p : parts) {
                String s = p.trim().toLowerCase(Locale.ROOT);
                if (s.startsWith("proto=")) {
                    String proto = s.substring("proto=".length()).trim();
                    if ("https".equals(proto)) return "https";
                    if ("http".equals(proto))  return "http";
                }
            }
        }

        // No hay TLS en HttpServer. Si tu servidor local está en 443 y el proxy no añade cabeceras,
        // no podremos saberlo con certeza; por defecto devolvemos http.
        return "http";
    }

    /** Host preferiblemente desde cabecera Host/X-Forwarded-Host/Forwarded; incluye puerto si está. */
    public static String extractHost(HttpExchange exchange) {
        // Prioridad: Forwarded (host=...), luego X-Forwarded-Host, luego Host
        String forwarded = header(exchange, "Forwarded");
        if (forwarded != null) {
            String[] parts = forwarded.split(";");
            for (String p : parts) {
                String s = p.trim();
                if (s.toLowerCase(Locale.ROOT).startsWith("host=")) {
                    // host puede llevar puerto: ejemplo.com:8443
                    return s.substring("host=".length()).trim();
                }
            }
        }

        String xfHost = header(exchange, "X-Forwarded-Host");
        if (xfHost != null && !xfHost.isEmpty()) {
            // Puede venir con múltiples valores: "a.com, b.com"; tomar el primero
            String first = xfHost.split(",")[0].trim();
            if (!first.isEmpty()) return first;
        }

        String host = header(exchange, "Host");
        if (host != null && !host.isEmpty()) {
            return host.trim();
        }

        // Fallback: si no hay cabecera Host (no debería pasar en HTTP/1.1), construir con el local address
        String authority = exchange.getLocalAddress().getHostString() + ":" + exchange.getLocalAddress().getPort();

        // Si scheme es https y puerto es 443, puedes omitirlo; si http y puerto 80, también.
        // Aquí devolvemos authority crudo por seguridad.
        return authority;
    }

    private static String header(HttpExchange exchange, String name) {
        return exchange.getRequestHeaders().getFirst(name);
    }	
}
