# BFH Science Self-Service Portal
# Copyright (C) 2025-2026 IKUS Software
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
import logging
from collections import namedtuple

import cherrypy
from cherrypy_foundation.form import CherryForm
from cherrypy_foundation.plugins.ldap import first_attribute
from cherrypy_foundation.tools.i18n import gettext_lazy as _
from ldap3.core.exceptions import LDAPOperationResult
from wtforms.fields import HiddenField, RadioField, SelectField, StringField, TextAreaField
from wtforms.validators import DataRequired, InputRequired, Length, Regexp, ValidationError

from bfh_science_ssp.core.const import LNI_STORAGE
from bfh_science_ssp.core.user import User

logger = logging.getLogger(__name__)

# Storage Name only support [a-zA-Z0-9\-]
STORAGE_REGEXP = r'^[a-zA-Z0-9\-]+$'

# Storage object
Storage = namedtuple(
    'Storage',
    'name,protocol,permission,usage,owners,mountpath_linux,mountpath_linux_managed,mountpath_win,mountpath_win_managed,group,manage_url,service',
    defaults=(LNI_STORAGE,),
)

OR = _('or')


def new_storage(ceph_dir, usage, permission, owners, group, manage_url):
    emdash = '\u2014'
    if not ceph_dir:
        # If ceph dir is not-defined it's a miss configuration.
        return Storage(
            name=emdash,
            usage=usage,
            protocol='SMB',
            permission=permission,
            owners=owners,
            mountpath_linux=emdash,
            mountpath_linux_managed=emdash,
            mountpath_win=emdash,
            mountpath_win_managed=emdash,
            group=group,
            manage_url=manage_url,
        )

    # Replace path separator.
    ceph_dir_linux = ceph_dir.replace('\\', '/')
    ceph_dir_win = ceph_dir.replace('/', '\\')
    if usage == 'user':
        # For user storage, we need to strip Users/<username> from the path.
        basename = ceph_dir_linux.split('/', 1)[1] if '/' in ceph_dir_linux else ceph_dir_linux
        mountpath_linux_managed = f"/home/{basename} {OR} /mnt/data/user/{basename}"
    else:
        mountpath_linux_managed = f"/mnt/data/group/{ceph_dir_linux}".replace('\\', '/')

    return Storage(
        name=f'LFE/{ceph_dir}',
        usage=usage,
        protocol='SMB',
        permission=permission,
        owners=owners,
        mountpath_linux=f"smb://bfh.ch/data/LFE/{ceph_dir_linux}",
        mountpath_linux_managed=mountpath_linux_managed,
        mountpath_win=f"\\\\bfh.ch\\data\\LFE\\{ceph_dir_win}",
        mountpath_win_managed=f"P:\\LFE\\{ceph_dir_win}",
        group=group,
        manage_url=manage_url,
    )


class CreateStorageForm(CherryForm):
    """
    Form used to create a new storage.
    """

    service = "LNI Storage"
    subject = _('Create Storage')

    storage_usage = SelectField(
        label=_('Storage Usage'),
        validators=[
            InputRequired(),
        ],
        choices=[
            ('user', _('User (single person)')),
            ('group', _('Group (multiple persons)')),
        ],
        validate_choice=True,
        default='',
    )

    storage_name = HiddenField(label=_('Storage name'))

    storage_user_name = StringField(
        label=_('User Storage name'),
        validators=[
            InputRequired(),
        ],
        filters=[str.strip],
        description=_('The user storage name cannot be modified.'),
        default='',
        render_kw={
            'readonly': '',
            'prepend': 'Users/',
            "data-showif-field": "storage_usage",
            "data-showif-operator": "eq",
            "data-showif-value": 'user',
        },
    )
    storage_group_name = StringField(
        label=_('Group Storage name'),
        validators=[
            InputRequired(),
            Length(max=80, message=_('Maximum length of 80.')),
            Regexp(
                STORAGE_REGEXP,
                message=_(
                    'Invalid storage name. Only letters, numbers, and dash (-) are allowed. No spaces or special characters.'
                ),
            ),
        ],
        filters=[str.strip],
        default='',
        render_kw={
            'placeholder': "group-name",
            'pattern': STORAGE_REGEXP,
            'title': _('Only letters, numbers, and dash (-) are allowed. No spaces or special characters.'),
            'prepend': '<Departement>/',
            "data-showif-field": "storage_usage",
            "data-showif-operator": "eq",
            "data-showif-value": 'group',
        },
    )
    storage_size = SelectField(
        label=_('Size Estimation'),
        validators=[
            InputRequired(),
        ],
        choices=[
            ('', _('Select a size…'), {'disabled': 1}),
            ('<1TB', _('0 to 1TB')),
            ('<10TB', _('1 to 10TB')),
            ('>10TB', _('More than 10TB')),
        ],
        validate_choice=True,
        default='',
        description=_('This is not a storage quota, but it is important for our capacity planning.'),
    )
    storage_type = SelectField(
        label=_('Storage Type'),
        validators=[
            DataRequired(),
        ],
        choices=[
            ('smb', _('SMB')),
        ],
        validate_choice=True,
        description=_('Only the SMB type is currently supported.'),
        default='smb',
        render_kw={
            'disabled': '1',
            'readonly': '1',
        },
    )

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # Configure default storage name for user & group.
        currentuser = cherrypy.serving.request.currentuser
        self.storage_user_name.data = currentuser.username
        self.storage_group_name.render_kw['prepend'] = f'{currentuser.departement}/'

    def validate(self, *args, **kwargs):
        """
        Validate storage_name.
        """
        # Validate storage_name
        currentuser = cherrypy.serving.request.currentuser
        if self.storage_usage.data == 'user':
            self.storage_name.data = f'Users/{currentuser.username}'
            del self.storage_group_name
        elif self.storage_usage.data == 'group':
            self.storage_name.data = f'{currentuser.departement}/{self.storage_group_name.data}'
            del self.storage_user_name
        else:
            self.form_errors.append(_('Invalid storage usage: %s') % self.storage_usage.data)
            return False
        return super().validate()


class RequestStorageAccessForm(CherryForm):
    """
    Form used by user to request new access or access change to existing storage.
    """

    service = "LNI Storage"
    subject = _('Request access to storage')
    storage_name = StringField(label=_('Storage name'), validators=[InputRequired()])
    permission = RadioField(
        label=_('Permission'),
        choices=[
            ('read', _('read')),
            ('write', _('write')),
            ('owner', _('owner')),
        ],
        validators=[InputRequired()],
    )
    reason = TextAreaField(
        label=_('Reason for access'),
        validators=[
            InputRequired(),
            Length(max=500, message=_('Maximum length of 500.')),
        ],
        render_kw={
            "rows": "5",
            "style": "height: 100px",
        },
    )

    def __init__(self, change=False, **kwargs):
        super().__init__(**kwargs)
        self.change = change

    def validate_storage_name(self, field, *args, **kwargs):
        """
        Validate storage_name.
        """
        # Check if the storage name exists.
        storages = StorageHandler.get_all_storage()
        groups_dn = storages.get(field.data, None)
        if not groups_dn:
            raise ValidationError(message=_('Invalid storage name: %s') % self.storage_name.data)
        # Send notification to managers
        self.recipient = User._query_managers(*groups_dn)
        if not self.recipient:
            # Fallback to ITS with an error message.
            self.note = _(
                'This request cannot be routed to the storage manager because no manager is defined for this storage.\nPlease process the request and ensure a manager is associated to the storage.'
            )
        permission_label = dict(self.permission.choices).get(self.permission.data)
        self.subject = _('Request %s access to storage: %s') % (permission_label, self.storage_name.data)


class RequestStorageAccessChangeForm(RequestStorageAccessForm):
    """
    This implementation customizes the form for access changes.
    """

    def __init__(self, change=False, **kwargs):
        """Override permissions."""
        super().__init__(change, **kwargs)
        # Add option to remove access
        self.permission.choices.append(('remove', _('remove access')))
        # Rename label
        self.reason.label.text = _('Reason for access change')

    def validate_storage_name(self, field, *args, **kwargs):
        """Override the email's subject."""
        super().validate_storage_name(field, *args, **kwargs)
        # Rename the email subject.
        self.subject = _('Request access change to storage: %s') % self.storage_name.data


class StorageHandler:
    """
    Handler for storage card.
    """

    @cherrypy.expose()
    @cherrypy.tools.allow(methods=['POST'])
    @cherrypy.tools.ratelimit(method=['POST'])
    def create_storage(self, **kwargs):
        """
        Handle storage creation.
        """
        cfg = cherrypy.tree.apps[''].cfg
        # Guest user's are not allowed to create storage directly.
        # A request will be sent to ITS.
        currentuser = cherrypy.serving.request.currentuser
        if currentuser.role == currentuser.ROLE_GUEST:
            self._send_request(CreateStorageForm())
        else:
            self._exec_request(cfg.new_storage_cmd, CreateStorageForm())

    @cherrypy.expose()
    @cherrypy.tools.allow(methods=['POST'])
    @cherrypy.tools.ratelimit(method=['POST'])
    def request_storage_access(self, **kwargs):
        """
        Handle storage creation.
        """
        self._send_request(RequestStorageAccessForm())

    @cherrypy.expose()
    @cherrypy.tools.json_out()
    @cherrypy.tools.allow(methods=['GET'])
    def storage_json(self, u=None, **kwargs):
        """
        Return list of storage resources for Datatable.
        """
        currentuser = cherrypy.serving.request.currentuser

        # Query user's attributes
        user = self._query__user(u)
        storages = []

        # A user has a user share, if their user has a non-empty bfhCephDirectory in LDAP
        ceph_dir = user.ceph_directory
        if ceph_dir:
            storages.append(
                new_storage(
                    ceph_dir=ceph_dir,
                    usage="user",
                    permission='write',
                    owners=[user.as_owner_dict()],
                    group=None,
                    manage_url=None,
                )
            )

        #  Read groups name to find storage:
        # cn=IDM.perm.storage.lni-team.write,ou=Security,ou=Groups,dc=bfh
        # cn=IDM.perm.storage.<name>.<permissions>,ou=Security,ou=Groups,dc=bfh
        for group_cn, name, permission in user.deduplicate_groups(
            "cn=IDM.perm.storage.<NAME>.<PERMISSION>,ou=Security,ou=Groups,dc=bfh",
            permission_priority=['write', 'read'],
        ):

            # Get Ceph directory
            groups_dn = [
                f"cn=IDM.perm.storage.{name}.write,ou=Security,ou=Groups,dc=bfh",
                f"cn=IDM.perm.storage.{name}.read,ou=Security,ou=Groups,dc=bfh",
            ]
            groups_details = User._query_ldap_attributes(*groups_dn, attributes=['bfhCephDirectory', 'bfhGroupManager'])
            ceph_dir = None
            for g in groups_details:
                ceph_dir = first_attribute(g, 'bfhCephDirectory', ceph_dir)

            # Get list of managers for this storage (read and write groups)
            managers = User._query_managers(*groups_dn, groups_details=groups_details)
            owners = [manager.as_owner_dict() for manager in managers]

            # Provide IDM group name to display "manage" or "Request access change" link.
            is_owner = any(True for manager in managers if currentuser.username == manager.username)
            if not is_owner and u != currentuser.username:
                group_cn = None
            manage_url = f"https://idm.bfh.ch/group/{group_cn}" if is_owner else None

            storages.append(
                new_storage(
                    ceph_dir=ceph_dir,
                    usage="group",
                    permission=permission,
                    owners=owners,
                    group=group_cn,
                    manage_url=manage_url,
                )
            )

        # Sort the storage by default. user storage, then group storage.
        sorted_storages = sorted(storages, key=lambda x: (x.usage != 'user', x.name))
        return {'data': [storage._asdict() for storage in sorted_storages]}

    @cherrypy.expose()
    @cherrypy.tools.json_out()
    @cherrypy.tools.allow(methods=['GET'])
    def all_storage_json(self, **kwargs):
        """
        Return a list of all storages available.
        """

        # Query user's storage
        return {'data': [storage for storage in self.get_all_storage()]}

    @classmethod
    def get_all_storage(cls):
        """
        Return a list of all storage available.
        """
        # Search for storage groups.
        search_filter = "(&(objectClass=*)(|(cn=IDM.perm.storage.*.write)(cn=IDM.perm.storage.*.read)))"
        try:
            response = cherrypy.ldap.search(search_filter, attributes=['bfhCephDirectory'])
        except LDAPOperationResult:
            logger.warning('failed to query all storage group', exc_info=1)
            return []
        if not response:
            return []
        # Return list of Storage name
        values = {}
        for r in response:
            group_dn = r['dn']
            ceph_dir = first_attribute(r['attributes'], 'bfhCephDirectory')
            if ceph_dir:
                name = f'LFE/{ceph_dir}'
                values.setdefault(name, []).append(group_dn)
        return values
