# coding:utf-8

from functools import cached_property

from makit.web.context import Context
from makit.web.middleware.base import Middleware

ALL_METHODS = ("DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT")
SAFE_LISTED_HEADERS = {"Accept", "Accept-Language", "Content-Language", "Content-Type"}


class CORSMiddleware(Middleware):
    def __init__(self, allow_origins=None, allow_credentials=False, allow_methods=None, allow_headers=None):
        super().__init__()
        self._config = dict(
            allow_origins=allow_origins,
            allow_credentials=allow_credentials,
            allow_methods=allow_methods,
            allow_headers=allow_headers
        )

    @cached_property
    def config(self):
        return self.app.config.CORS

    @cached_property
    def allow_all_origins(self):
        return "*" in self.config.allow_origins

    @cached_property
    def allow_all_headers(self):
        return '*' in self.config.allow_headers

    @property
    def preflight_explicit_allow_origin(self):
        return self.allow_all_origins or self.config.allow_credentials

    async def __call__(self, c: Context):
        scope = c.scope
        self.app = c.app
        if scope["type"] != "http":
            return

        method = scope["method"]
        origin = c.headers.get("origin")

        if origin is None:
            return await c.next()

        if method == "OPTIONS" and "access-control-request-method" in c.headers:
            await self.preflight_response(c)
            return

        await c.next()

    def is_allowed_origin(self, origin: str) -> bool:
        if self.allow_all_origins:
            return True
        origin_regex = self.config.allow_origin_regex
        if origin_regex is not None and origin_regex.fullmatch(origin):
            return True
        return origin in self.config.allow_origins

    def is_allowed_method(self, method):
        if '*' in self.config.allow_methods:
            return True
        return method in self.config.allow_methods

    @cached_property
    def preflight_headers(self):
        if '*' in self.config.allow_methods:
            allow_methods = ALL_METHODS
        else:
            allow_methods = self.config.allow_methods
        headers = {
            "Access-Control-Allow-Methods": ", ".join(allow_methods),
            "Access-Control-Max-Age": str(self.config.max_age),
        }
        if "*" in self.config.allow_origins or self.config.allow_credentials:
            headers["Vary"] = "Origin"
        else:
            headers["Access-Control-Allow-Origin"] = "*"
        allow_all_headers = "*" in self.config.allow_headers
        allow_headers = sorted(SAFE_LISTED_HEADERS | set(self.config.allow_headers))
        if allow_headers and not allow_all_headers:
            headers["Access-Control-Allow-Headers"] = ", ".join(allow_headers)
        if self.config.allow_credentials:
            headers["Access-Control-Allow-Credentials"] = "true"

        return headers

    async def preflight_response(self, c: Context):
        headers = c.headers
        requested_origin = headers["origin"]
        requested_method = headers["access-control-request-method"]
        requested_headers = headers.get("access-control-request-headers")

        headers = dict(self.preflight_headers)
        failures = []

        if self.is_allowed_origin(origin=requested_origin):
            if self.preflight_explicit_allow_origin:
                headers["Access-Control-Allow-Origin"] = requested_origin
        else:
            failures.append("origin")

        if not self.is_allowed_method(requested_method):
            failures.append("method")

        if self.allow_all_headers and requested_headers is not None:
            headers["Access-Control-Allow-Headers"] = requested_headers
        elif requested_headers is not None:
            for header in [h.lower() for h in requested_headers.split(",")]:
                if header.strip() not in self.config.allow_headers:
                    failures.append("headers")
                    break

        if failures:
            failure_text = "Disallowed CORS " + ", ".join(failures)
            return await c.respond_text(failure_text, status_code=400, headers=headers)

        return await c.respond_text("OK", status_code=200, headers=headers)

    @staticmethod
    def allow_explicit_origin(headers, origin: str) -> None:
        headers["Access-Control-Allow-Origin"] = origin
        headers.add_vary_header("Origin")
