# Copyright (C) 2021-2023 Cochise Ruhulessin
#
# All rights reserved. No warranty, explicit or implicit, provided. In
# no event shall the author(s) be liable for any claim or damages.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import functools
import logging
from datetime import datetime
from datetime import timezone
from typing import Any
from typing import Awaitable
from typing import Callable
from typing import Literal
from typing import TypeVar

import aorta
import fastapi

from .abortable import Abortable
from .iauthorizationcontextfactory import IAuthorizationContextFactory
from .irequestprincipal import IRequestPrincipal
from .iroutable import IRoutable
from .isessionmanager import ISessionManager
from .forbidden import Forbidden
from .notauthorized import NotAuthorized
from .request import Request
from .requestlanguage import RequestLanguage


T = TypeVar('T', bound='IEndpoint')


class IEndpoint:
    __module__: str = 'cbra.types'
    allowed_http_methods: list[str]
    autodiscover: bool = True
    context_factory: IAuthorizationContextFactory
    csrf_protect: bool = False
    handlers: list[IRoutable]
    include_in_schema: bool = True
    name: str | None = None
    languages: RequestLanguage = RequestLanguage.depends()
    logger: logging.Logger = logging.getLogger('cbra.endpoint')
    response_model_by_alias: bool = False
    response_model_exclude: set[str] | None = None
    response_model_exclude_none: bool = False
    session: ISessionManager[Any]
    timestamp: datetime
    publisher: aorta.types.IPublisher
    with_options: bool = True

    #: The ``If-Match`` value provided by the current request.
    etag: set[str] = set()

    #: The set of permissions supported by this endpoint. These must be
    #: defined beforehand to limit the number of calls to remote IAM
    #: systems.
    permissions: set[str]

    #: The list of subjects that may access this endpoint. Override this
    #: property if an endpoint needs to hardcode the allowed subjects
    #: that may invoke it, for example when expecting a request from
    #: Google using a priorly known service account.
    allowed_subjects: set[str] = set()

    #: The set of trusted authorization servers. This will override the
    #: :attr:`cbra.core.conf.settings.TRUSTED_AUTHORIZATION_SERVERS`
    #: setting.
    trusted_providers: set[str] = set()

    #: Indicates if this endpoint uses versioning using the ``ETag``
    #: and friends headers.
    versioned: bool

    principal: IRequestPrincipal
    request: Request
    response: fastapi.Response
    router: fastapi.APIRouter

    #: Indicates if all requests to the endpoint must be authenticated.
    require_authentication: bool = False
    status_code: int = 200
    summary: str | None = None
    tags: list[str] = []

    #: The current messaging transaction.
    transaction: aorta.Transaction

    #: Indicates that this endpoint is in testing mode and should not catch
    #: any abortable errors.
    test: bool = False

    @staticmethod
    def require_permission(name: str) -> Callable[..., Any]:
        """Decorate a method on an :class:`~cbra.types.IEndpoint` implementation
        to require the given permission. If the request does not have permission
        `name`, then :class:`~cbra.types.Forbidden` is raised.
        """
        def decorator_factory(
            func: Callable[..., Any]
        ) -> Callable[['IEndpoint'], Awaitable[Any]]:
            @functools.wraps(func)
            async def f(self: 'IEndpoint', *args: Any, **kwargs: Any) -> Any:
                if not await self.is_authorized(name):
                    raise Forbidden
                return await func(self, *args, **kwargs)
            return f
        return decorator_factory

    @classmethod
    def configure(cls: type[T], overrides: dict[str, Any]) -> type[T]:
        return type(cls.__name__, (cls,), overrides) # type: ignore

    async def get_allowed_subjects(self) -> set[str]:
        """Return the set of allowed subjects that may access this resource.
        By default, returns :attr:`allowed_subjects`.
        """
        return self.allowed_subjects

    def get_success_headers(self, data: Any) -> dict[str, Any]:
        """Return a mapping holding the headers to add on a successful
        request based on the return value of the request handler.
        """
        return {}

    async def run_handler(
        self,
        func: Callable[..., Any],
        *args: Any,
        **kwargs: Any
    ):
        try:
            async with aorta.Transaction(publisher=self.publisher) as tx:
                self.transaction = tx
                await self.authenticate()
                if self.must_authenticate() and not self.ctx.is_authenticated():
                    raise NotAuthorized
                return await func(self, *args, **kwargs)
        except Abortable as exc:
            return await exc.as_response()

    async def authenticate(self) -> None:
        self.ctx = await self.context_factory.authenticate(
            request=self.request,
            principal=self.principal,
            providers=self.trusted_providers,
            subjects=await self.get_allowed_subjects()
        )

    async def is_authorized(self, name: str) -> bool:
        """Return a boolean if the given authorization context has a
        certain permission.
        """
        await self.ctx.authorize()
        return self.has_permission(name)
    
    def has_permission(self, name: str) -> bool:
        """Return a boolean if the request has the given permission."""
        return self.ctx.has_permission(name)

    def is_authenticated(self) -> bool:
        """Return a boolean indicating if the request is authenticated."""
        return self.ctx.is_authenticated()

    def must_authenticate(self) -> bool:
        """Return a boolean if a client must authenticate the request."""
        return self.require_authentication

    def set_cookie(
        self,
        key: str,
        value: str = "",
        max_age: int | None = None,
        expires: datetime | str| int | None = None,
        path: str = "/",
        domain: str | None = None,
        secure: bool = False,
        httponly: bool = False,
        samesite: Literal["lax", "strict", "none"] | None = "lax",
    ) -> None:
        self.response.set_cookie(
            key=key,
            value=value,
            max_age=max_age,
            expires=expires,
            path=path,
            domain=domain,
            secure=secure,
            httponly=httponly,
            samesite=samesite
        )

    def delete_cookie(self, key: str, path: str = "/") -> None:
        return self.set_cookie(
            key,
            expires=datetime(1970, 1, 1, tzinfo=timezone.utc),
            max_age=0,
            path=path
        )
    
    def publish(
        self,
        Message: type[aorta.Command | aorta.Event],
        **params: Any
    ) -> None:
        self.transaction.publish(Message.parse_obj(params))