# This file is part of Indico.
# Copyright (C) 2002 - 2021 CERN
#
# Indico is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see the
# LICENSE file for more details.

from uuid import uuid4

from sqlalchemy.dialects.postgresql import ARRAY, UUID
from sqlalchemy.ext.declarative import declared_attr
from werkzeug.urls import url_parse

from indico.core.db import db
from indico.core.db.sqlalchemy import PyIntEnum
from indico.modules.oauth import logger
from indico.util.i18n import _
from indico.util.struct.enum import IndicoEnum


SCOPES = {'read:user': _("User information (read only)"),
          'read:legacy_api': _('Legacy API (read only)'),
          'write:legacy_api': _('Legacy API (write only)'),
          'registrants': _('Event registrants')}


class SystemAppType(int, IndicoEnum):
    none = 0
    checkin = 1
    flower = 2

    __enforced_data__ = {
        checkin: {'default_scopes': {'registrants'},
                  'redirect_uris': ['http://localhost'],
                  'is_enabled': True},
        flower: {'default_scopes': {'read:user'},
                 'is_enabled': True}
    }

    __default_data__ = {
        checkin: {'is_trusted': True,
                  'name': 'Checkin App',
                  'description': 'The checkin app for mobile devices allows scanning ticket QR codes and '
                                 'checking-in event participants.'},
        flower: {'is_trusted': True,
                 'name': 'Flower',
                 'description': 'Flower allows monitoring Celery tasks. If flower is installed, this app is used to '
                                'restrict access to Indico administrators.'}
    }

    @property
    def enforced_data(self):
        return self.__enforced_data__.get(self, {})

    @property
    def default_data(self):
        return dict(self.__default_data__.get(self, {}), **self.enforced_data)


class OAuthApplication(db.Model):
    """OAuth applications registered in Indico."""

    __tablename__ = 'applications'

    @declared_attr
    def __table_args__(cls):
        return (db.Index('ix_uq_applications_name_lower', db.func.lower(cls.name), unique=True),
                db.Index(None, cls.system_app_type, unique=True,
                         postgresql_where=db.text(f'system_app_type != {SystemAppType.none.value}')),
                {'schema': 'oauth'})

    #: the unique id of the application
    id = db.Column(
        db.Integer,
        primary_key=True
    )
    #: human readable name
    name = db.Column(
        db.String,
        nullable=False
    )
    #: human readable description
    description = db.Column(
        db.Text,
        nullable=False,
        default=''
    )
    #: the OAuth client_id
    client_id = db.Column(
        UUID,
        unique=True,
        nullable=False,
        default=lambda: str(uuid4())
    )
    #: the OAuth client_secret
    client_secret = db.Column(
        UUID,
        nullable=False,
        default=lambda: str(uuid4())
    )
    #: the OAuth default scopes the application may request access to
    default_scopes = db.Column(
        ARRAY(db.String),
        nullable=False
    )
    #: the OAuth absolute URIs that a application may use to redirect to after authorization
    redirect_uris = db.Column(
        ARRAY(db.String),
        nullable=False,
        default=[]
    )
    #: whether the application is enabled or disabled
    is_enabled = db.Column(
        db.Boolean,
        nullable=False,
        default=True
    )
    #: whether the application can access user data without asking for permission
    is_trusted = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: the type of system app (if any). system apps cannot be deleted
    system_app_type = db.Column(
        PyIntEnum(SystemAppType),
        nullable=False,
        default=SystemAppType.none
    )

    # relationship backrefs:
    # - tokens (OAuthToken.application)

    @property
    def client_type(self):
        return 'public'

    @property
    def default_redirect_uri(self):
        return self.redirect_uris[0] if self.redirect_uris else None

    @property
    def locator(self):
        return {'id': self.id}

    def __repr__(self):  # pragma: no cover
        return f'<OAuthApplication({self.id}, {self.name}, {self.client_id})>'

    def reset_client_secret(self):
        self.client_secret = str(uuid4())
        logger.info("Client secret for %s has been reset.", self)

    def validate_redirect_uri(self, redirect_uri):
        """Called by flask-oauthlib to validate the redirect_uri.

        Uses a logic similar to the one at GitHub, i.e. protocol and
        host/port must match exactly and if there is a path in the
        whitelisted URL, the path of the redirect_uri must start with
        that path.
        """
        uri_data = url_parse(redirect_uri)
        for valid_uri_data in map(url_parse, self.redirect_uris):
            if (uri_data.scheme == valid_uri_data.scheme and uri_data.netloc == valid_uri_data.netloc and
                    uri_data.path.startswith(valid_uri_data.path)):
                return True
        return False

[(21, 'indico.util.i18n._', '_', (['"""User information (read only)"""'], {}), False, 'from indico.util.i18n import _\n'), (22, 'indico.util.i18n._', '_', (['"""Legacy API (read only)"""'], {}), False, 'from indico.util.i18n import _\n'), (23, 'indico.util.i18n._', '_', (['"""Legacy API (write only)"""'], {}), False, 'from indico.util.i18n import _\n'), (24, 'indico.util.i18n._', '_', (['"""Event registrants"""'], {}), False, 'from indico.util.i18n import _\n'), (73, 'indico.core.db.db.Column', 'db.Column', (['db.Integer'], {'primary_key': '(True)'}), False, 'from indico.core.db import db\n'), (78, 'indico.core.db.db.Column', 'db.Column', (['db.String'], {'nullable': '(False)'}), False, 'from indico.core.db import db\n'), (83, 'indico.core.db.db.Column', 'db.Column', (['db.Text'], {'nullable': '(False)', 'default': '""""""'}), False, 'from indico.core.db import db\n'), (113, 'indico.core.db.db.Column', 'db.Column', (['db.Boolean'], {'nullable': '(False)', 'default': '(True)'}), False, 'from indico.core.db import db\n'), (119, 'indico.core.db.db.Column', 'db.Column', (['db.Boolean'], {'nullable': '(False)', 'default': '(False)'}), False, 'from indico.core.db import db\n'), (103, 'sqlalchemy.dialects.postgresql.ARRAY', 'ARRAY', (['db.String'], {}), False, 'from sqlalchemy.dialects.postgresql import ARRAY, UUID\n'), (108, 'sqlalchemy.dialects.postgresql.ARRAY', 'ARRAY', (['db.String'], {}), False, 'from sqlalchemy.dialects.postgresql import ARRAY, UUID\n'), (126, 'indico.core.db.sqlalchemy.PyIntEnum', 'PyIntEnum', (['SystemAppType'], {}), False, 'from indico.core.db.sqlalchemy import PyIntEnum\n'), (151, 'indico.modules.oauth.logger.info', 'logger.info', (['"""Client secret for %s has been reset."""', 'self'], {}), False, 'from indico.modules.oauth import logger\n'), (161, 'werkzeug.urls.url_parse', 'url_parse', (['redirect_uri'], {}), False, 'from werkzeug.urls import url_parse\n'), (150, 'uuid.uuid4', 'uuid4', ([], {}), False, 'from uuid import uuid4\n'), (67, 'indico.core.db.db.func.lower', 'db.func.lower', (['cls.name'], {}), False, 'from indico.core.db import db\n'), (69, 'indico.core.db.db.text', 'db.text', (['f"""system_app_type != {SystemAppType.none.value}"""'], {}), False, 'from indico.core.db import db\n'), (93, 'uuid.uuid4', 'uuid4', ([], {}), False, 'from uuid import uuid4\n'), (99, 'uuid.uuid4', 'uuid4', ([], {}), False, 'from uuid import uuid4\n')]