# This file is part of Indico.
# Copyright (C) 2002 - 2026 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.

import posixpath

from flask import g
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.declarative import declared_attr

from indico.core.config import config
from indico.core.db import db
from indico.core.db.sqlalchemy import PyIntEnum, UTCDateTime
from indico.core.db.sqlalchemy.links import LinkType
from indico.core.db.sqlalchemy.protection import ProtectionMixin
from indico.core.db.sqlalchemy.searchable import SearchableTitleMixin
from indico.core.db.sqlalchemy.util.models import auto_table_args
from indico.core.db.sqlalchemy.util.session import no_autoflush
from indico.core.storage import StoredFileMixin, VersionedResourceMixin
from indico.modules.attachments.models.principals import AttachmentPrincipal
from indico.modules.attachments.preview import get_file_previewer
from indico.modules.attachments.util import can_manage_attachments
from indico.util.date_time import now_utc
from indico.util.enum import RichIntEnum
from indico.util.fs import secure_filename
from indico.util.i18n import pgettext
from indico.util.string import strict_str
from indico.web.flask.util import url_for


class AttachmentType(RichIntEnum):
    __titles__ = [None, pgettext('Attachment type (noun)', 'File'), pgettext('Attachment type (noun)', 'Link')]
    file = 1
    link = 2


class AttachmentFile(StoredFileMixin, db.Model):
    __tablename__ = 'files'
    __table_args__ = {'schema': 'attachments'}

    version_of = 'attachment'

    #: The ID of the file
    id = db.Column(
        db.Integer,
        primary_key=True
    )
    #: The ID of the associated attachment
    attachment_id = db.Column(
        db.Integer,
        db.ForeignKey('attachments.attachments.id'),
        nullable=False,
        index=True
    )
    #: The user who uploaded the file
    user_id = db.Column(
        db.Integer,
        db.ForeignKey('users.users.id'),
        nullable=False,
        index=True
    )

    #: The user who uploaded the file
    user = db.relationship(
        'User',
        lazy=True,
        backref=db.backref(
            'attachment_files',
            lazy='dynamic'
        )
    )

    # relationship backrefs:
    # - attachment (Attachment.all_files)

    @property
    def is_previewable(self):
        return get_file_previewer(self) is not None

    @property
    def is_image(self):
        return self.content_type.startswith('image/')

    @no_autoflush
    def _build_storage_path(self):
        folder = self.attachment.folder
        assert folder.object is not None
        if folder.link_type == LinkType.category:
            # category/<id>/...
            path_segments = ['category', strict_str(folder.category.id)]
        else:
            # event/<id>/event/...
            path_segments = ['event', strict_str(folder.event.id), folder.link_type.name]
            if folder.link_type == LinkType.session:
                # event/<id>/session/<session_id>/...
                path_segments.append(strict_str(folder.session.id))
            elif folder.link_type == LinkType.contribution:
                # event/<id>/contribution/<contribution_id>/...
                path_segments.append(strict_str(folder.contribution.id))
            elif folder.link_type == LinkType.subcontribution:
                # event/<id>/subcontribution/<subcontribution_id>/...
                path_segments.append(strict_str(folder.subcontribution.id))
        self.attachment.assign_id()
        self.assign_id()
        filename = '{}-{}-{}'.format(self.attachment.id, self.id, secure_filename(self.filename, 'file'))
        path = posixpath.join(*path_segments, filename)
        return config.ATTACHMENT_STORAGE, path

    def __repr__(self):
        return f'<AttachmentFile({self.id}, {self.attachment_id}, {self.filename}, {self.content_type})>'


class Attachment(SearchableTitleMixin, ProtectionMixin, VersionedResourceMixin, db.Model):
    __tablename__ = 'attachments'
    __auto_table_args = (
        # links: url but no file
        db.CheckConstraint(f'type != {AttachmentType.link.value} OR (link_url IS NOT NULL AND file_id IS NULL)',
                           'valid_link'),
        # we can't require the file_id to be NOT NULL for files because of the circular relationship...
        # but we can ensure that we never have both a file_id AND a link_url...for
        db.CheckConstraint('link_url IS NULL OR file_id IS NULL', 'link_or_file'),
        {'schema': 'attachments'}
    )

    stored_file_table = 'attachments.files'
    stored_file_class = AttachmentFile
    stored_file_fkey = 'attachment_id'
    title_required = False

    @declared_attr
    def __table_args__(cls):
        return auto_table_args(cls)

    #: The ID of the attachment
    id = db.Column(
        db.Integer,
        primary_key=True

    )
    #: The ID of the folder the attachment belongs to
    folder_id = db.Column(
        db.Integer,
        db.ForeignKey('attachments.folders.id'),
        nullable=False,
        index=True
    )
    #: The ID of the user who created the attachment
    user_id = db.Column(
        db.Integer,
        db.ForeignKey('users.users.id'),
        index=True,
        nullable=False
    )
    #: If the attachment has been deleted
    is_deleted = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: The description of the attachment
    description = db.Column(
        db.Text,
        nullable=False,
        default=''
    )
    #: The date/time when the attachment was created/modified
    modified_dt = db.Column(
        UTCDateTime,
        nullable=False,
        default=now_utc,
        onupdate=now_utc
    )
    #: The type of the attachment (file or link)
    type = db.Column(
        PyIntEnum(AttachmentType),
        nullable=False
    )
    #: The target URL for a link attachment
    link_url = db.Column(
        db.String,
        nullable=True
    )
    #: The ID of the attachment that was converted from
    converted_from_id = db.Column(
        db.Integer,
        db.ForeignKey('attachments.attachments.id'),
        nullable=True,
        index=True
    )
    #: Attachment annotations (metadata)
    annotations = db.Column(
        JSONB,
        nullable=False,
        default={}
    )
    #: The user who created the attachment
    user = db.relationship(
        'User',
        lazy=True,
        backref=db.backref(
            'attachments',
            lazy='dynamic'
        )
    )
    #: The folder containing the attachment
    folder = db.relationship(
        'AttachmentFolder',
        lazy=True,
        backref=db.backref(
            'all_attachments',
            lazy=True
        )
    )
    acl_entries = db.relationship(
        'AttachmentPrincipal',
        backref='attachment',
        cascade='all, delete-orphan',
        collection_class=set
    )
    #: The ACL of the folder (used for ProtectionMode.protected)
    acl = association_proxy('acl_entries', 'principal', creator=lambda v: AttachmentPrincipal(principal=v))

    converted_from = db.relationship(
        'Attachment',
        lazy=True,
        remote_side=id,
        backref=db.backref(
            'converted_into',
            primaryjoin='(Attachment.id == Attachment.converted_from_id) & ~Attachment.is_deleted',
            lazy=True
        )
    )

    # relationship backrefs:
    # - converted_into (Attachment.converted_from)
    # - legacy_mapping (LegacyAttachmentMapping.attachment)

    @property
    def protection_parent(self):
        return self.folder

    @property
    def locator(self):
        return dict(self.folder.locator, attachment_id=self.id)

    def get_download_url(self, absolute=False):
        """Return the download url for the attachment.

        During static site generation this returns a local URL for the
        file or the target URL for the link.

        :param absolute: If the returned URL should be absolute.
        """
        if g.get('static_site'):
            return _offline_download_url(self)
        else:
            filename = self.file.filename if self.type == AttachmentType.file else 'go'
            return url_for('attachments.download', self, filename=filename, _external=absolute)

    @property
    def download_url(self):
        """The download url for the attachment."""
        return self.get_download_url()

    @property
    def absolute_download_url(self):
        """The absolute download url for the attachment."""
        return self.get_download_url(absolute=True)

    def can_access(self, user, *args, **kwargs):
        """Check if the user is allowed to access the attachment.

        This is the case if the user has access to see the attachment
        or if the user can manage attachments for the linked object.
        """
        return (super().can_access(user, *args, **kwargs) or
                can_manage_attachments(self.folder.object, user, allow_admin=kwargs.get('allow_admin', True)))

    def __repr__(self):
        return '<Attachment({}, {}, {}{}, {}, {})>'.format(
            self.id,
            self.title,
            self.file if self.type == AttachmentType.file else self.link_url,
            ', is_deleted=True' if self.is_deleted else '',
            self.protection_repr,
            self.folder_id
        )


# Register all SQLAlchemy-related events
Attachment.register_versioned_resource_events()
Attachment.register_protection_events()


def _offline_download_url(attachment):
    # Legacy offline download link generation
    if attachment.type == AttachmentType.file:
        if isinstance(attachment.folder.object, db.m.Event):
            path = ''
        elif isinstance(attachment.folder.object, db.m.Session):
            path = f'{attachment.folder.session.friendly_id}-session'
        elif isinstance(attachment.folder.object, db.m.Contribution):
            path = f'{attachment.folder.contribution.friendly_id}-contribution'
        elif isinstance(attachment.folder.object, db.m.SubContribution):
            path = f'{attachment.folder.subcontribution.friendly_id}-subcontribution'
        else:
            return ''
        return posixpath.join('material', path, str(attachment.id) + '-' + attachment.file.filename)
    else:
        return attachment.link_url
