from __future__ import annotations
import bcrypt

from vantage6.server.model.base import DatabaseSessionManager
from sqlalchemy.orm import relationship, validates
from sqlalchemy import Column, Integer, String, ForeignKey

from vantage6.server.model.authenticatable import Authenticatable


class Node(Authenticatable):
    """
    Table that contains all registered nodes.

    Attributes
    ----------
    id : int
        Primary key
    name : str
        Name of the node
    api_key : str
        API key of the node
    collaboration : :class:`~.model.collaboration.Collaboration`
        Collaboration that the node belongs to
    organization : :class:`~.model.organization.Organization`
        Organization that the node belongs to
    """
    _hidden_attributes = ['api_key']

    id = Column(Integer, ForeignKey('authenticatable.id'), primary_key=True)

    # fields
    name = Column(String, unique=True)
    api_key = Column(String)
    collaboration_id = Column(Integer, ForeignKey("collaboration.id"))
    organization_id = Column(Integer, ForeignKey("organization.id"))

    # relationships
    collaboration = relationship("Collaboration", back_populates='nodes')
    organization = relationship("Organization", back_populates='nodes')
    config = relationship("NodeConfig", back_populates="node")

    # the type specification in Authenticatable
    __mapper_args__ = {
        'polymorphic_identity': 'node',
    }

    @validates("api_key")
    def _validate_api_key(self, key: str, api_key: str) -> str:
        """
        Hashes the api_key before storing it in the database.

        Parameters
        ----------
        key : str
            The name of the attribute that is being validated
        api_key : str
            The value of the attribute that is being validated

        Returns
        -------
        str
            The hashed api_key
        """
        return self.hash(api_key)

    def check_key(self, key: str) -> bool:
        """
        Checks if the provided key matches the stored key.

        Parameters
        ----------
        key : str
            The key to check

        Returns
        -------
        bool
            True if the provided key matches the stored key, False otherwise
        """
        if self.api_key is not None:
            expected_hash = self.api_key.encode('utf8')
            return bcrypt.checkpw(key.encode('utf8'), expected_hash)
        return False

    @classmethod
    def get_by_api_key(cls, api_key: str) -> Node | None:
        """
        Returns Node based on the provided API key.

        Parameters
        ----------
        api_key : str
            The API key of the node to search for

        Returns
        -------
        Node | None
            Returns the node if a node is associated with api_key, None if no
            node is associated with api_key.
        """
        session = DatabaseSessionManager.get_session()

        nodes = session.query(cls).all()
        session.commit()
        for node in nodes:
            is_correct_key = node.check_key(api_key)
            if is_correct_key:
                return node
        # no node found with matching API key
        return None

    @classmethod
    def get_online_nodes(cls) -> list[Node]:
        """
        Return nodes that currently have status 'online'

        Returns
        -------
        list[Node]
            List of node models that are currently online
        """
        session = DatabaseSessionManager.get_session()

        result = session.query(cls).filter_by(status='online').all()
        session.commit()
        return result

    @classmethod
    def exists_by_id(cls, organization_id: int, collaboration_id: int) -> bool:
        """
        Check if a node exists for the given organization and collaboration.

        Parameters
        ----------
        organization_id : int
            The id of the organization
        collaboration_id : int
            The id of the collaboration

        Returns
        -------
        bool
            True if a node exists for the given organization and collaboration,
            False otherwise.
        """
        session = DatabaseSessionManager.get_session()
        result = session.query(cls).filter_by(
            organization_id=organization_id,
            collaboration_id=collaboration_id
        ).scalar()
        session.commit()
        return result

    def __repr__(self) -> str:
        """
        String representation of the Node model.

        Returns
        -------
        str
            String representation of the Node model
        """
        return (
            "<Node "
            f"{self.id}: '{self.name}', "
            f"organization: {self.organization.name}, "
            f"collaboration: {self.collaboration.name}, "
            f"api_key: {self.api_key} "
            ">"
        )
