# 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.

from uuid import uuid4

from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.hybrid import hybrid_property
from werkzeug.exceptions import ServiceUnavailable

from indico.core.db import db
from indico.core.db.sqlalchemy import PyIntEnum, UTCDateTime
from indico.util.date_time import now_utc
from indico.util.enum import RichIntEnum
from indico.util.i18n import pgettext


class AgreementState(RichIntEnum):
    __titles__ = [pgettext('Agreement', 'Pending'), pgettext('Agreement', 'Accepted'),
                  pgettext('Agreement', 'Rejected'), pgettext('Agreement', 'Accepted on behalf'),
                  pgettext('Agreement', 'Rejected on behalf')]
    pending = 0
    accepted = 1
    rejected = 2
    #: agreement accepted on behalf of the person
    accepted_on_behalf = 3
    #: agreement rejected on behalf of the person
    rejected_on_behalf = 4


class Agreement(db.Model):
    """Agreements between a person and Indico."""

    __tablename__ = 'agreements'
    __table_args__ = (db.UniqueConstraint('event_id', 'type', 'identifier'),
                      {'schema': 'events'})

    #: Entry ID
    id = db.Column(
        db.Integer,
        primary_key=True
    )
    #: Entry universally unique ID
    uuid = db.Column(
        db.String,
        nullable=False
    )
    #: ID of the event
    event_id = db.Column(
        db.Integer,
        db.ForeignKey('events.events.id'),
        nullable=False,
        index=True
    )
    #: Type of agreement
    type = db.Column(
        db.String,
        nullable=False
    )
    #: Unique identifier within the event and type
    identifier = db.Column(
        db.String,
        nullable=False
    )
    #: Email of the person agreeing
    person_email = db.Column(
        db.String,
        nullable=True
    )
    #: Full name of the person agreeing
    person_name = db.Column(
        db.String,
        nullable=False
    )
    #: A :class:`AgreementState`
    state = db.Column(
        PyIntEnum(AgreementState),
        default=AgreementState.pending,
        nullable=False
    )
    #: The date and time the agreement was created
    timestamp = db.Column(
        UTCDateTime,
        default=now_utc,
        nullable=False
    )
    #: ID of a linked user
    user_id = db.Column(
        db.Integer,
        db.ForeignKey('users.users.id'),
        index=True,
        nullable=True
    )
    #: The date and time the agreement was signed
    signed_dt = db.Column(
        UTCDateTime
    )
    #: The IP from which the agreement was signed
    signed_from_ip = db.Column(
        db.String
    )
    #: Explanation as to why the agreement was accepted/rejected
    reason = db.Column(
        db.String
    )
    #: Attachment
    attachment = db.deferred(db.Column(
        db.LargeBinary
    ))
    #: Filename and extension of the attachment
    attachment_filename = db.Column(
        db.String
    )
    #: Definition-specific data of the agreement
    data = db.Column(
        JSONB
    )

    #: The user this agreement is linked to
    user = db.relationship(
        'User',
        lazy=False,
        backref=db.backref(
            'agreements',
            lazy='dynamic'
        )
    )
    #: The Event this agreement is associated with
    event = db.relationship(
        'Event',
        lazy=True,
        backref=db.backref(
            'agreements',
            lazy='dynamic'
        )
    )

    @hybrid_property
    def accepted(self):
        return self.state in {AgreementState.accepted, AgreementState.accepted_on_behalf}

    @accepted.expression
    def accepted(cls):
        return cls.state.in_((AgreementState.accepted, AgreementState.accepted_on_behalf))

    @hybrid_property
    def pending(self):
        return self.state == AgreementState.pending

    @hybrid_property
    def rejected(self):
        return self.state in {AgreementState.rejected, AgreementState.rejected_on_behalf}

    @rejected.expression
    def rejected(cls):
        return cls.state.in_((AgreementState.rejected, AgreementState.rejected_on_behalf))

    @hybrid_property
    def signed_on_behalf(self):
        return self.state in {AgreementState.accepted_on_behalf, AgreementState.rejected_on_behalf}

    @signed_on_behalf.expression
    def signed_on_behalf(cls):
        return cls.state.in_((AgreementState.accepted_on_behalf, AgreementState.rejected_on_behalf))

    @property
    def definition(self):
        from indico.modules.events.agreements.util import get_agreement_definitions
        return get_agreement_definitions().get(self.type)

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

    def __repr__(self):
        state = self.state.name if self.state is not None else None
        return f'<Agreement({self.id}, {self.event_id}, {self.type}, {self.identifier}, {self.person_email}, {state})>'

    @staticmethod
    def create_from_data(event, type_, person):
        agreement = Agreement(event=event, type=type_, state=AgreementState.pending, uuid=str(uuid4()))
        agreement.identifier = person.identifier
        agreement.person_email = person.email
        agreement.person_name = person.name
        if person.user:
            agreement.user = person.user
        agreement.data = person.data
        return agreement

    def accept(self, from_ip, reason=None, on_behalf=False):
        self.state = AgreementState.accepted if not on_behalf else AgreementState.accepted_on_behalf
        self.signed_from_ip = from_ip
        self.reason = reason
        self.signed_dt = now_utc()
        self.definition.handle_accepted(self)

    def reject(self, from_ip, reason=None, on_behalf=False):
        self.state = AgreementState.rejected if not on_behalf else AgreementState.rejected_on_behalf
        self.signed_from_ip = from_ip
        self.reason = reason
        self.signed_dt = now_utc()
        self.definition.handle_rejected(self)

    def reset(self):
        self.definition.handle_reset(self)
        self.state = AgreementState.pending
        self.attachment = None
        self.attachment_filename = None
        self.reason = None
        self.signed_dt = None
        self.signed_from_ip = None

    def render(self, form, **kwargs):
        definition = self.definition
        if definition is None:
            raise ServiceUnavailable('This agreement type is currently not available.')
        return definition.render_form(self, form, **kwargs)

    def belongs_to(self, person):
        return self.identifier == person.identifier

    def is_orphan(self):
        definition = self.definition
        if definition is None:
            raise ServiceUnavailable('This agreement type is currently not available.')
        return definition.is_agreement_orphan(self.event, self)
