import asyncio
import concurrent
import copy
import inspect
import json
import logging
import re
from collections import defaultdict
from types import SimpleNamespace
from typing import Any, Callable, Dict, List, Optional, Tuple
from urllib.parse import urlencode, urlparse

_logger = logging.getLogger(__name__)


def _is_basic_type(obj):
    if isinstance(obj, (int, float, str, bool)):
        return True


def _serialize_recursive(obj):
    if _is_basic_type(obj):
        return obj
    if hasattr(obj, "model_dump"):
        return obj.model_dump(mode="json")
    if isinstance(obj, dict):
        return {k: _serialize_recursive(v) for k, v in obj.items()}
    elif isinstance(obj, list):
        return [_serialize_recursive(v) for v in obj]
    return obj


def snake_to_header(name: str) -> str:
    """
    Converts snake_case to Header-Case.

    Args:
        name (str): The snake_case string.

    Returns:
        str: The Header-Case string.
    """
    return "-".join(word.capitalize() for word in name.split("_"))


def canoncialize_header_name(name: str) -> str:
    return name.lower()


def snake_to_query_param(name: str) -> str:
    return name


# Response Classes
class JSONResponse:
    def __init__(
        self, content: Any, status_code: int = 200, headers: Dict[str, str] = None
    ):
        self.status_code = status_code
        self.content = json.dumps(content)
        self.headers = headers or {"Content-Type": "application/json"}

    def to_lambda_response(self):
        _logger.info(
            f"Converting JSONResponse to Lambda Response, status code: {self.status_code}"
        )
        return {
            "statusCode": self.status_code,
            "body": self.content,
            "headers": self.headers,
        }


class StreamingResponse:
    def __init__(
        self,
        generator: Callable,
        media_type: str = "application/json",
        headers: Dict[str, str] = None,
    ):
        self.generator = generator
        self.media_type = media_type
        self.headers = headers or {"Content-Type": media_type}

    def to_lambda_response(self):
        # AWS Lambda expects the entire body to be sent at once.
        # For true streaming, consider AWS services like API Gateway WebSockets or AWS App Runner.
        # Here, we'll concatenate all chunks for simplicity.
        body = ""
        for chunk in self.generator():
            body += chunk
        return {
            "statusCode": 200,
            "body": body,
            "headers": self.headers,
        }


class RedirectResponse:
    def __init__(
        self, url: str, status_code: int = 307, headers: Dict[str, str] = None
    ):
        self.url = url
        self.status_code = status_code
        self.headers = headers or {}

    def to_lambda_response(self):
        return {
            "statusCode": self.status_code,
            "headers": {**self.headers, **{"Location": self.url}},
        }


# Exception Classes
class HTTPException(Exception):
    def __init__(self, status_code: int, detail: Any = None):
        self.status_code = int(status_code)
        self.detail = detail


class Depends:
    def __init__(self, dependency: Callable):
        _logger.info(f"Constructing Depends with {dependency}")
        self.dependency = dependency

        # Check if the dependency is a function
        if not inspect.isfunction(dependency):
            raise ValueError("Depends requires a function")


class Header:
    def __init__(self, default: Any = ..., alias: str = None):
        """
        Represents a header parameter.

        Args:
            default (Any, optional): The default value if the header is not provided.
            alias (str, optional): The actual header name in the HTTP request.
        """
        self.default = default
        self.alias = alias


class Query:
    def __init__(self, default: Any = ..., alias: str = None):
        """
        Represents a query parameter.

        Args:
            default (Any, optional): The default value if the query parameter is not provided.
            alias (str, optional): The actual query parameter name in the HTTP request.
        """
        self.default = default
        self.alias = alias


class Request:
    def __init__(
        self,
        method: str,
        path: str,
        query_params: Dict[str, Any],
        headers: Dict[str, Any],
        body: Any,
    ):
        self.method = method
        self.path = path
        self.query_params = query_params or {}
        self.headers = copy.deepcopy(headers)
        self._body = body

        # Construct a FastAPI-like `request.url` object
        self.url = SimpleNamespace(
            path=self.path,
            query=self._construct_query_string(),
            full_url=self._construct_full_url(),
        )

        # Extract cookies from headers
        self.cookies = {}
        cookie_header = headers.get("cookie")
        if cookie_header:
            del headers["cookie"]
            for cookie in cookie_header.split(";"):
                key, value = cookie.split("=")
                self.cookies[key.strip()] = value.strip()

    def body(self):
        return self._body

    def _construct_query_string(self) -> str:
        """Constructs the query string from query parameters."""
        return urlencode(self.query_params, doseq=True)

    def _construct_full_url(self) -> str:
        """Constructs a full URL representation (without domain)."""
        query_string = self._construct_query_string()
        return f"{self.path}?{query_string}" if query_string else self.path


# Assume these helper functions and classes are already defined:
#   - canoncialize_header_name
#   - Depends, Header, Query, Request


class HandlingFunction:
    """
    A helper class to extract injection metadata from a function signature.

    Attributes:
        func: The callable (route handler or dependency function).
        route_param_names: Parameter names coming from the route (e.g. extracted from the URL path).
        signature: The inspected signature of the function.
        dependencies: A dict mapping parameter names to dependency callables (from Depends).
        header_params: A dict mapping canonical header parameter names to Header instances.
        request_params: A dict mapping parameter names (annotated as Request) to their defaults.
        query_params: A dict mapping parameter names to Query instances.
        body_params: A dict mapping parameter names to their inspect.Parameter objects that are meant for the body.
    """

    def __init__(self, func: Callable, route_param_names: Optional[List[str]] = None):
        self.func = func
        self.route_param_names = route_param_names or []
        self.signature = inspect.signature(func)

        self.dependencies: Dict[str, Callable] = self._extract_dependencies()
        self.header_params: Dict[str, Header] = self._extract_header_params()
        self.request_params: Dict[str, Request] = self._extract_request_params()
        self.query_params: Dict[str, Query] = self._extract_query_params()
        self.body_params: Dict[str, inspect.Parameter] = self._extract_body_params()

    def _extract_dependencies(self) -> Dict[str, Callable]:
        """
        Extracts dependencies based on parameters that have a default instance of Depends.
        """
        dependencies = {}
        for name, param in self.signature.parameters.items():
            if isinstance(param.default, Depends):
                # Record the dependency function itself
                dependencies[name] = param.default.dependency
        return dependencies

    def _extract_header_params(self) -> Dict[str, Header]:
        """
        Extracts header parameters based on parameters with a default instance of Header.
        Uses a canonical header name (lowercase) as the key.
        """
        header_params = {}
        for name, param in self.signature.parameters.items():
            canonical_name = canoncialize_header_name(name)
            if isinstance(param.default, Header):
                header_params[canonical_name] = param.default
        return header_params

    def _extract_request_params(self) -> Dict[str, Request]:
        """
        Extracts request parameters by checking for parameters annotated as Request.
        """
        request_params = {}
        for name, param in self.signature.parameters.items():
            if param.annotation == Request:
                request_params[name] = (
                    param.default
                )  # Could be None or a default instance
        return request_params

    def _extract_query_params(self) -> Dict[str, Query]:
        """
        Extracts query parameters based on parameters with a default instance of Query.
        """
        query_params = {}
        for name, param in self.signature.parameters.items():
            if isinstance(param.default, Query):
                query_params[name] = param.default
        return query_params

    def _extract_body_params(self) -> Dict[str, inspect.Parameter]:
        """
        Extracts body parameters as those parameters that are not:
            - Route path parameters
            - Dependencies
            - Header parameters
            - Query parameters
            - Request parameters (based on annotation)
        And whose default is not a Depends, Header, or Query.
        """
        body_params = {}
        for name, param in self.signature.parameters.items():
            if (
                name in self.dependencies
                or canoncialize_header_name(name) in self.header_params
                or name in self.query_params
                or name in self.request_params
                or name in self.route_param_names
            ):
                continue
            # We only consider parameters that are explicitly positional/keyword
            if not isinstance(
                param.default, (Depends, Header, Query)
            ) and param.kind in (
                inspect.Parameter.POSITIONAL_OR_KEYWORD,
                inspect.Parameter.KEYWORD_ONLY,
            ):
                body_params[name] = param
        return body_params

    def get_injection_kwargs(self, available_values: Dict[str, Any]) -> Dict[str, Any]:
        """
        Given a dictionary of already extracted values (e.g. from the headers, query, and path),
        returns a dictionary of arguments to be injected when calling self.func.

        This method can be extended to also resolve dependencies by calling their functions.
        """
        injection_kwargs = {}
        # Inject direct values (headers, query, path, etc.)
        for name in self.signature.parameters:
            if name in available_values:
                injection_kwargs[name] = available_values[name]

        # For dependencies, we can attempt to resolve them by inspecting their signature
        for dep_name, dep_func in self.dependencies.items():
            dep_sig = inspect.signature(dep_func)
            dep_kwargs = {}
            # For each parameter in the dependency, try to provide a value if available
            for pname in dep_sig.parameters:
                if pname in available_values:
                    dep_kwargs[pname] = available_values[pname]
                elif pname == "request" and "request" in available_values:
                    dep_kwargs["request"] = available_values["request"]
            # Call the dependency function and store its result
            injection_kwargs[dep_name] = dep_func(**dep_kwargs)
        return injection_kwargs

    def __repr__(self):
        return (
            f"HandlingFunction(func={self.func.__name__}, "
            f"dependencies={list(self.dependencies.keys())}, "
            f"header_params={list(self.header_params.keys())}, "
            f"query_params={list(self.query_params.keys())}, "
            f"request_params={list(self.request_params.keys())}, "
            f"body_params={list(self.body_params.keys())})"
        )


class InjectedDependency:
    """
    Wraps a dependency function and precomputes an injection map.

    The injection map maps each parameter name in the dependency function's
    signature to a key that is expected in the available values dictionary.

    For example, if a parameter has a default value of type Header, its lookup
    key is determined from the header alias (or using snake_to_header).
    """

    def __init__(self, dep_func: Callable):
        self.dep_func = dep_func
        # Use HandlingFunction to analyze the dependency function
        self.hf = HandlingFunction(dep_func)
        self.injection_map = self._compute_injection_map()
        _logger.info(
            f"Analyzed dependency {dep_func.__name__}: injection_map={self.injection_map}"
        )

    def _compute_injection_map(self) -> Dict[str, str]:
        mapping = {}
        for param_name, param in self.hf.signature.parameters.items():
            # For a Request parameter, use the key "request"
            if param.annotation == Request:
                mapping[param_name] = "request"
            # For a Header parameter, use the canonical header name.
            elif isinstance(param.default, Header):
                header_alias = param.default.alias or snake_to_header(param_name)
                mapping[param_name] = canoncialize_header_name(header_alias)
            # For a Query parameter, use its alias if provided, else the parameter name.
            elif isinstance(param.default, Query):
                mapping[param_name] = param.default.alias or param_name
            else:
                # Fallback: use the parameter name directly
                mapping[param_name] = param_name
        return mapping

    def resolve(self, available_values: Dict[str, Any]) -> Any:
        """
        Resolves the dependency by building a kwargs dict based on the injection map
        and then calling the dependency function.
        """
        injection_kwargs = {}
        for param_name, lookup_key in self.injection_map.items():
            if lookup_key in available_values:
                injection_kwargs[param_name] = available_values[lookup_key]
            else:
                # If the parameter has a default value, use it.
                param = self.hf.signature.parameters[param_name]
                _logger.info(f"{param_name}: Param: {param}")
                if param.default != inspect.Parameter.empty:
                    param_default_value = param.default
                    if isinstance(param_default_value, Depends):
                        # Resolve the dependency function
                        param_default_value = param_default_value.dependency()
                    elif isinstance(param_default_value, Header):
                        # Use the default value of the header
                        param_default_value = param_default_value.default
                    elif isinstance(param_default_value, Query):
                        param_default_value = param_default_value.default

                    injection_kwargs[param_name] = param_default_value
                else:
                    raise Exception(
                        f"Missing injection for dependency parameter '{param_name}' (lookup key '{lookup_key}')"
                    )
        _logger.info(
            f"Resolved dependency {self.dep_func.__name__} with kwargs {injection_kwargs}"
        )
        return self.dep_func(**injection_kwargs)


# Route Data Structure
class Route:
    def __init__(
        self,
        methods: List[str],
        path: str,
        handler: Callable,
        dependencies: List[str] = None,
    ):
        self.methods = [method.upper() for method in methods]  # Store multiple methods
        self.path = path
        self.handler = handler
        self.dependencies = dependencies or []

        # Find all parameters, both standard and `{param:path}`
        self.param_names = re.findall(r"{(\w+)(?::path)?}", path)

        # Create a HandlingFunction instance for the handler,
        # passing route parameter names to exclude them from body parameters.
        hf = HandlingFunction(handler, route_param_names=self.param_names)

        self.dependencies = (
            {}
        )  # key: parameter name, value: InjectedDependency instance
        # Wrap each dependency function with InjectedDependency.
        for dep_name, dep_func in hf.dependencies.items():
            self.dependencies[dep_name] = InjectedDependency(dep_func)

        self.header_params = hf.header_params
        self.query_params = hf.query_params
        self.request_params = hf.request_params
        self.body_params = hf.body_params

        # Generate regex:
        #   - `{param}` -> `([^/]+)`
        #   - `{param:path}` -> `(.*)`
        regex_pattern = re.sub(r"{(\w+):path}", r"(.*)", path)  # Match entire remainder
        regex_pattern = re.sub(
            r"{(\w+)}", r"([^/]+)", regex_pattern
        )  # Match single segment

        # Final compiled regex
        self.regex = re.compile(f"^{regex_pattern}$")

    def match(self, method: str, path: str) -> Optional[Dict[str, str]]:
        """Match the route against a request method and path."""
        if method.upper() not in self.methods:  # Check if the method is allowed
            return None
        match = self.regex.match(path)
        if not match:
            return None
        params = match.groups()
        retval = dict(zip(self.param_names, params))
        return retval


# Router Class
class APIRouter:
    def __init__(self, prefix: str = ""):
        self.prefix = prefix
        self.routes: List[Route] = []

    def add_route(
        self,
        methods: List[str],
        path: str,
        handler: Callable,
        dependencies: List[Any] = None,
    ):
        full_path = self.prefix + path
        route = Route(methods, full_path, handler, dependencies)
        self.routes.append(route)

    # Decorator Factory
    def route(self, method: str, path: str, dependencies: List[str] = None):
        def decorator(func: Callable):
            self.add_route(method, path, func, dependencies)
            return func

        return decorator

    def api_route(self, path: str, methods: List[str], dependencies: List[str] = None):
        def decorator(func: Callable):
            self.add_route(methods, path, func, dependencies)
            return func

        return decorator

    def get(self, path: str, dependencies: List[str] = None):
        return self.api_route(path, ["GET"], dependencies)

    def post(self, path: str, dependencies: List[str] = None):
        return self.api_route(path, ["POST"], dependencies)

    def put(self, path: str, dependencies: List[str] = None):
        return self.api_route(path, ["PUT"], dependencies)

    def delete(self, path: str, dependencies: List[str] = None):
        return self.api_route(path, ["DELETE"], dependencies)

    def patch(self, path: str, dependencies: List[str] = None):
        return self.api_route(path, ["PATCH"], dependencies)


class LambdaApiLight:
    def __init__(self):
        self.routers: List[APIRouter] = []
        self.event_handlers: Dict[str, List[Callable]] = {}

        self.logger = logging.getLogger("lightweight_framework")
        self.logger.setLevel(logging.INFO)
        if not self.logger.handlers:
            handler = logging.StreamHandler()
            formatter = logging.Formatter(
                "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
            )
            handler.setFormatter(formatter)
            self.logger.addHandler(handler)

    def include_router(self, router: APIRouter):
        self.routers.append(router)

    def on_startup(self, func: Callable):
        self.on_startup_handlers.append(func)
        return func

    async def execute_event_handlers(self, event_type: str):
        handlers = self.event_handlers.get(event_type, [])
        for handler in handlers:
            if inspect.iscoroutinefunction(handler):
                await handler()
            else:
                handler()

    def on_event(self, event_type: str):
        """
        Decorator to register an event handler for a specific event type.
        Usage:
            @app.on_event("startup")
            async def startup_event():
                ...
        """

        def decorator(func: Callable):
            if event_type not in self.event_handlers:
                self.event_handlers[event_type] = []
            self.event_handlers[event_type].append(func)
            return func

        return decorator

    def execute_startup(self):
        for handler in self.on_startup_handlers:
            handler()

    def find_route(
        self, method: str, path: str
    ) -> Tuple[Optional[Route], Optional[Dict[str, str]]]:
        """Find a matching route by method and path."""
        for router in self.routers:
            for route in router.routes:
                params = route.match(method, path)
                if params is not None:
                    return route, params
        return None, None

    def handle_request(self, event: Dict[str, Any], context: Any) -> Dict[str, Any]:
        method = event.get("httpMethod")
        path = event.get("path")
        query_params = event.get("queryStringParameters") or {}
        headers = event.get("headers") or {}
        headers = {canoncialize_header_name(k): v for k, v in headers.items()}
        request_object = Request(
            method=method,
            path=path,
            query_params=query_params,
            headers=headers,
            body=event.get("body"),
        )

        body = event.get("body")
        is_base64_encoded = event.get("isBase64Encoded", False)

        route, path_params = self.find_route(method, path)
        if not route:
            return JSONResponse(
                {"error": "Not Found"}, status_code=404
            ).to_lambda_response()
        try:
            # Parse body
            if body:
                if is_base64_encoded:
                    import base64

                    body = base64.b64decode(body).decode("utf-8")
                try:
                    body = json.loads(body)
                except json.JSONDecodeError:
                    pass  # Keep as string if not JSON

            # Handle dependencies
            kwargs = {}

            # Handle header parameters
            for name, header in route.header_params.items():

                header_name = canoncialize_header_name(
                    header.alias or snake_to_header(name)
                )

                header_value = headers.get(header_name)  # Extract the header value

                if header_value is None:  # Header not provided
                    if header.default is ...:  # Explicitly required header
                        raise HTTPException(
                            400, f"Missing required header: {header_name}"
                        )
                    else:  # Optional header with a default value
                        header_value = header.default

                kwargs[name] = header_value  # Populate the route function parameters
                _logger.info(f"Header Parameter: {name} = {header_value}")

            # Include body if present
            if body:
                kwargs["body"] = body

            # Build an available values dictionary.
            # Note that keys should correspond to the ones expected by dependency injection.
            available = {"request": request_object}
            # Insert headers with their canonical keys.
            available.update(headers)
            # Insert query parameters.
            available.update(query_params)
            # Also include any path parameters.
            available.update(path_params or {})

            # Resolve dependencies using the preanalyzed metadata.
            for dep_name, injected_dep in route.dependencies.items():
                kwargs[dep_name] = injected_dep.resolve(available)

            # Extract query parameters and path parameters
            if path_params:
                for k, v in path_params.items():
                    if k not in kwargs:
                        kwargs[k] = v

            if query_params:
                for k, v in query_params.items():
                    if k not in kwargs:
                        kwargs[k] = v

            # Handle query parameters
            for name, query in route.query_params.items():
                query_name = query.alias or snake_to_query_param(name)
                query_value = query_params.get(query_name)
                if query_value is None:
                    if query.default is ...:
                        raise HTTPException(
                            400, f"Missing required query parameter: {query_name}"
                        )
                    else:
                        query_value = query.default
                if name not in kwargs:
                    kwargs[name] = query_value
                _logger.info(f"Query Parameter: {name} = {query_value}")

            # Handle body parameters
            for name, param in route.body_params.items():
                if body:

                    if hasattr(param.annotation, "model_validate"):
                        # Assume it's a Pydantic model
                        try:
                            kwargs[name] = param.annotation.model_validate(body)
                            _logger.info(
                                f"Parsed body parameter '{name}' into {param.annotation}"
                            )
                        except Exception as e:
                            _logger.warning(
                                f"Failed to parse body parameter '{name}' into {param.annotation}: {e}"
                            )
                            raise HTTPException(
                                400, f"Invalid body for parameter '{name}': {e}"
                            )
                    else:
                        if name not in kwargs:
                            kwargs[name] = body
                            _logger.info(f"Body Parameter: {name} = {body}")
                elif param.default != inspect.Parameter.empty:
                    if name not in kwargs:
                        kwargs[name] = param.default
                else:
                    raise HTTPException(400, f"Missing required body parameter: {name}")

            for name, _ in route.request_params.items():
                if name not in kwargs:
                    kwargs[name] = request_object

            sig = inspect.signature(route.handler)
            bound_args = {}
            for name, param in sig.parameters.items():
                if name in kwargs:
                    bound_args[name] = kwargs[name]
                elif param.default != inspect.Parameter.empty:
                    bound_args[name] = param.default
                else:
                    raise Exception(f"Missing required parameter: {name}")

            result = route.handler(**bound_args)

            # Check if the result is a coroutine and handle it
            if inspect.iscoroutine(result):
                try:
                    # Attempt to run the coroutine using asyncio.run
                    result = asyncio.run(result)
                except RuntimeError:
                    # If there's already a running event loop (e.g., during testing), use a new thread
                    loop = asyncio.new_event_loop()
                    asyncio.set_event_loop(loop)
                    with concurrent.futures.ThreadPoolExecutor() as pool:
                        future = pool.submit(loop.run_until_complete, result)
                        result = future.result()
                    loop.close()
            result_class_name = result.__class__.__name__
            if result_class_name in [
                "JSONResponse",
                "StreamingResponse",
                "RedirectResponse",
            ]:
                _logger.info(
                    f"Returning JSONResponse or StreamingResponse or RedirectResponse converted to Lambda Response"
                )
                return result.to_lambda_response()
            elif isinstance(result, dict):
                _logger.info(f"JSONResponse converting dict to json")
                return JSONResponse(result).to_lambda_response()
            elif isinstance(result, list):
                _logger.info(f"JSONResponse converting list to json")
                return JSONResponse(_serialize_recursive(result)).to_lambda_response()
            elif hasattr(result, "model_dump"):
                _logger.info(f"JSONResponse converting model to json")
                return JSONResponse(result.model_dump(mode="json")).to_lambda_response()

            else:
                _logger.info(f"Returning result as string")
                # Assume string
                return {
                    "statusCode": 200,
                    "body": str(result),
                    "headers": {"Content-Type": "text/plain"},
                }

        except HTTPException as he:
            return {
                "statusCode": he.status_code,
                "body": json.dumps({"detail": he.detail}),
                "headers": {"Content-Type": "application/json"},
            }
        except Exception as e:
            self.logger.error(f"Unhandled exception: {e}", exc_info=True)
            return {
                "statusCode": 500,
                "body": json.dumps({"error": "Internal Server Error"}),
                "headers": {"Content-Type": "application/json"},
            }

    @property
    def routes(self):
        route_info = []
        for router in self.routers:
            by_path = defaultdict(list)
            for route in router.routes:
                by_path[route.path].append(route)

            for path, routes in by_path.items():
                method_list = []
                for route in routes:
                    method_list.extend(route.methods)
                method_list = sorted(set(method_list))
                methods = ",".join(method_list)
                route_info.append(SimpleNamespace(path=path, methods=methods))

        return route_info
