
import socket
import threading
import time
from typing import Dict, Optional, Tuple
from urllib.parse import unquote

# Configuration
BACKEND_HOST = "127.0.0.1"
BACKEND_PORT = 5000
LISTEN_PORT = 9000
MAX_CONNECTIONS = 100
REQUEST_TIMEOUT = 30

# Security rules
PROTECTED_PATHS = ["/internal/"]


class SecurityGateway:

    def __init__(self):
        self.request_count = 0
        self.blocked_count = 0

    def check_access_control(self, path: str) -> bool:
        # URL decode to prevent encoding-based bypasses
        decoded_path = unquote(path)

        # Check against protected path patterns
        for protected in PROTECTED_PATHS:
            if decoded_path.startswith(protected):
                return True  # Block request

        return False  # Allow request

    def parse_http_request(self, raw_data: bytes) -> Tuple[str, str, Dict[str, str], bytes]:
        # Find end of headers
        if b"\r\n\r\n" not in raw_data:
            raise ValueError("Invalid HTTP request")

        header_end = raw_data.index(b"\r\n\r\n")
        raw_headers = raw_data[:header_end + 4]

        # Parse request line
        first_line = raw_headers.split(b"\r\n")[0].decode('utf-8', errors='replace')
        parts = first_line.split(" ")

        if len(parts) < 3:
            raise ValueError("Malformed request line")

        method = parts[0]
        path = parts[1]

        # Parse headers into dictionary
        headers = {}
        for line in raw_headers.split(b"\r\n")[1:]:
            if b":" in line:
                key, value = line.split(b":", 1)
                headers[key.decode('utf-8', errors='replace').lower().strip()] = \
                    value.decode('utf-8', errors='replace').strip()

        return method, path, headers, raw_headers

    def read_request_body(self, conn: socket.socket, headers: Dict[str, str],
                         leftover: bytes) -> bytes:
        # Determine body length from Content-Length header
        content_length = 0
        if 'content-length' in headers:
            try:
                content_length = int(headers['content-length'])
            except ValueError:
                content_length = 0

        if content_length == 0:
            return b""

        # Read body data
        body = leftover
        while len(body) < content_length:
            try:
                chunk = conn.recv(4096)
                if not chunk:
                    break
                body += chunk
            except socket.timeout:
                break

        return body[:content_length]

    def forward_to_backend(self, raw_headers: bytes, body: bytes) -> bytes:
        try:
            # Create new backend connection for each request
            backend_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            backend_sock.connect((BACKEND_HOST, BACKEND_PORT))
            backend_sock.settimeout(5)

            # Send complete request
            backend_sock.sendall(raw_headers + body)

            # Read response headers first
            response = b""
            while b"\r\n\r\n" not in response:
                chunk = backend_sock.recv(4096)
                if not chunk:
                    break
                response += chunk

            # Parse Content-Length from response
            content_length = 0
            for line in response.split(b"\r\n"):
                if line.lower().startswith(b"content-length:"):
                    try:
                        content_length = int(line.split(b":", 1)[1].strip())
                    except:
                        pass
                    break

            # Read remaining body if Content-Length specified
            if content_length > 0 and b"\r\n\r\n" in response:
                header_end = response.index(b"\r\n\r\n") + 4
                body_received = len(response) - header_end

                while body_received < content_length:
                    chunk = backend_sock.recv(4096)
                    if not chunk:
                        break
                    response += chunk
                    body_received += len(chunk)

            backend_sock.close()
            return response

        except Exception as e:
            return self.make_error_response(502, "Bad Gateway")

    def make_error_response(self, code: int, message: str) -> bytes:
        body = f'{{"error":"{message}"}}'.encode()
        return (
            f"HTTP/1.1 {code} {message}\r\n"
            f"Content-Type: application/json\r\n"
            f"Content-Length: {len(body)}\r\n"
            f"Connection: close\r\n\r\n"
        ).encode() + body

    def handle_request(self, conn: socket.socket, addr: Tuple[str, int]):
        try:
            conn.settimeout(REQUEST_TIMEOUT)

            # Read headers
            raw_data = b""
            while b"\r\n\r\n" not in raw_data:
                chunk = conn.recv(4096)
                if not chunk:
                    return
                raw_data += chunk

            # Parse request
            method, path, headers, raw_headers = self.parse_http_request(raw_data)

            # Access control check
            if self.check_access_control(path):
                self.blocked_count += 1
                response = self.make_error_response(403, "Forbidden")
                conn.sendall(response)
                return

            # Read request body
            header_end = raw_data.index(b"\r\n\r\n")
            leftover = raw_data[header_end + 4:]
            body = self.read_request_body(conn, headers, leftover)

            # Forward to backend
            self.request_count += 1
            response = self.forward_to_backend(raw_headers, body)
            conn.sendall(response)

        except Exception as e:
            try:
                conn.sendall(self.make_error_response(500, "Internal Server Error"))
            except:
                pass
        finally:
            conn.close()

    def start(self):
        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server.bind(("0.0.0.0", LISTEN_PORT))
        server.listen(MAX_CONNECTIONS)

        print(f"[SecurityGateway] Listening on 0.0.0.0:{LISTEN_PORT}")
        print(f"[SecurityGateway] Backend: {BACKEND_HOST}:{BACKEND_PORT}")
        print(f"[SecurityGateway] Protected paths: {', '.join(PROTECTED_PATHS)}")

        while True:
            try:
                conn, addr = server.accept()
                thread = threading.Thread(
                    target=self.handle_request,
                    args=(conn, addr),
                    daemon=True
                )
                thread.start()
            except KeyboardInterrupt:
                break
            except Exception as e:
                continue


if __name__ == "__main__":
    gateway = SecurityGateway()
    gateway.start()
