from types import MappingProxyType

from integrations.gitlab.gitlab_view import (
    GitlabFactory,
    GitlabInlineMRComment,
    GitlabIssue,
    GitlabIssueComment,
    GitlabMRComment,
    GitlabViewType,
)
from integrations.manager import Manager
from integrations.models import Message, SourceType
from integrations.types import ResolverViewInterface
from integrations.utils import (
    CONVERSATION_URL,
    HOST_URL,
    OPENHANDS_RESOLVER_TEMPLATES_DIR,
)
from jinja2 import Environment, FileSystemLoader
from pydantic import SecretStr
from server.auth.token_manager import TokenManager
from server.utils.conversation_callback_utils import register_callback_processor

from openhands.core.logger import openhands_logger as logger
from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
from openhands.integrations.provider import ProviderToken, ProviderType
from openhands.server.types import LLMAuthenticationError, MissingSettingsError
from openhands.storage.data_models.user_secrets import UserSecrets


class GitlabManager(Manager):
    def __init__(self, token_manager: TokenManager, data_collector: None = None):
        self.token_manager = token_manager

        self.jinja_env = Environment(
            loader=FileSystemLoader(OPENHANDS_RESOLVER_TEMPLATES_DIR + 'gitlab')
        )

    def _confirm_incoming_source_type(self, message: Message):
        if message.source != SourceType.GITLAB:
            raise ValueError(f'Unexpected message source {message.source}')

    async def _user_has_write_access_to_repo(
        self, project_id: str, user_id: str
    ) -> bool:
        """
        Check if the user has write access to the repository (can pull/push changes and open merge requests).

        Args:
            project_id: The ID of the GitLab project
            username: The username of the user
            user_id: The GitLab user ID

        Returns:
            bool: True if the user has write access to the repository, False otherwise
        """

        keycloak_user_id = await self.token_manager.get_user_id_from_idp_user_id(
            user_id, ProviderType.GITLAB
        )
        if keycloak_user_id is None:
            logger.warning(f'Got invalid keyloak user id for GitLab User {user_id}')
            return False

        # Importing here prevents circular import
        from integrations.gitlab.gitlab_service import SaaSGitLabService

        gitlab_service: SaaSGitLabService = GitLabServiceImpl(
            external_auth_id=keycloak_user_id
        )

        return await gitlab_service.user_has_write_access(project_id)

    async def receive_message(self, message: Message):
        self._confirm_incoming_source_type(message)
        if await self.is_job_requested(message):
            gitlab_view = await GitlabFactory.create_gitlab_view_from_payload(
                message, self.token_manager
            )
            logger.info(
                f'[GitLab] Creating job for {gitlab_view.user_info.username} in {gitlab_view.full_repo_name}#{gitlab_view.issue_number}'
            )

            await self.start_job(gitlab_view)

    async def is_job_requested(self, message) -> bool:
        self._confirm_incoming_source_type(message)
        if not (
            GitlabFactory.is_labeled_issue(message)
            or GitlabFactory.is_issue_comment(message)
            or GitlabFactory.is_mr_comment(message)
            or GitlabFactory.is_mr_comment(message, inline=True)
        ):
            return False

        payload = message.message['payload']

        repo_obj = payload['project']
        project_id = repo_obj['id']
        selected_project = repo_obj['path_with_namespace']
        user = payload['user']
        user_id = user['id']
        username = user['username']

        logger.info(
            f'[GitLab] Checking permissions for {username} in {selected_project}'
        )

        has_write_access = await self._user_has_write_access_to_repo(
            project_id=str(project_id), user_id=user_id
        )

        logger.info(
            f'[GitLab]: {username} access in {selected_project}: {has_write_access}'
        )
        # Check if the user has write access to the repository
        return has_write_access

    async def send_message(self, message: Message, gitlab_view: ResolverViewInterface):
        """
        Send a message to GitLab based on the view type.

        Args:
            message: The message to send
            gitlab_view: The GitLab view object containing issue/PR/comment info
        """
        keycloak_user_id = gitlab_view.user_info.keycloak_user_id

        # Importing here prevents circular import
        from integrations.gitlab.gitlab_service import SaaSGitLabService

        gitlab_service: SaaSGitLabService = GitLabServiceImpl(
            external_auth_id=keycloak_user_id
        )

        outgoing_message = message.message

        if isinstance(gitlab_view, GitlabInlineMRComment) or isinstance(
            gitlab_view, GitlabMRComment
        ):
            await gitlab_service.reply_to_mr(
                gitlab_view.project_id,
                gitlab_view.issue_number,
                gitlab_view.discussion_id,
                message.message,
            )

        elif isinstance(gitlab_view, GitlabIssueComment):
            await gitlab_service.reply_to_issue(
                gitlab_view.project_id,
                gitlab_view.issue_number,
                gitlab_view.discussion_id,
                outgoing_message,
            )
        elif isinstance(gitlab_view, GitlabIssue):
            await gitlab_service.reply_to_issue(
                gitlab_view.project_id,
                gitlab_view.issue_number,
                None,  # no discussion id, issue is tagged
                outgoing_message,
            )
        else:
            logger.warning(
                f'[GitLab] Unsupported view type: {type(gitlab_view).__name__}'
            )

    async def start_job(self, gitlab_view: GitlabViewType):
        """
        Start a job for the GitLab view.

        Args:
            gitlab_view: The GitLab view object containing issue/PR/comment info
        """
        # Importing here prevents circular import
        from server.conversation_callback_processor.gitlab_callback_processor import (
            GitlabCallbackProcessor,
        )

        try:
            try:
                user_info = gitlab_view.user_info

                logger.info(
                    f'[GitLab] Starting job for {user_info.username} in {gitlab_view.full_repo_name}#{gitlab_view.issue_number}'
                )

                user_token = await self.token_manager.get_idp_token_from_idp_user_id(
                    str(user_info.user_id), ProviderType.GITLAB
                )

                if not user_token:
                    logger.warning(
                        f'[GitLab] No token found for user {user_info.username} (id={user_info.user_id})'
                    )
                    raise MissingSettingsError('Missing settings')

                logger.info(
                    f'[GitLab] Creating new conversation for user {user_info.username}'
                )

                secret_store = UserSecrets(
                    provider_tokens=MappingProxyType(
                        {
                            ProviderType.GITLAB: ProviderToken(
                                token=SecretStr(user_token),
                                user_id=str(user_info.user_id),
                            )
                        }
                    )
                )

                await gitlab_view.create_new_conversation(
                    self.jinja_env, secret_store.provider_tokens
                )

                conversation_id = gitlab_view.conversation_id

                logger.info(
                    f'[GitLab] Created conversation {conversation_id} for user {user_info.username}'
                )

                # Create a GitlabCallbackProcessor for this conversation
                processor = GitlabCallbackProcessor(
                    gitlab_view=gitlab_view,
                    send_summary_instruction=True,
                )

                # Register the callback processor
                register_callback_processor(conversation_id, processor)

                logger.info(
                    f'[GitLab] Created callback processor for conversation {conversation_id}'
                )

                conversation_link = CONVERSATION_URL.format(conversation_id)
                msg_info = f"I'm on it! {user_info.username} can [track my progress at all-hands.dev]({conversation_link})"

            except MissingSettingsError as e:
                logger.warning(
                    f'[GitLab] Missing settings error for user {user_info.username}: {str(e)}'
                )

                msg_info = f'@{user_info.username} please re-login into [OpenHands Cloud]({HOST_URL}) before starting a job.'

            except LLMAuthenticationError as e:
                logger.warning(
                    f'[GitLab] LLM authentication error for user {user_info.username}: {str(e)}'
                )

                msg_info = f'@{user_info.username} please set a valid LLM API key in [OpenHands Cloud]({HOST_URL}) before starting a job.'

            # Send the acknowledgment message
            msg = self.create_outgoing_message(msg_info)
            await self.send_message(msg, gitlab_view)

        except Exception as e:
            logger.exception(f'[GitLab] Error starting job: {str(e)}')
            msg = self.create_outgoing_message(
                msg='Uh oh! There was an unexpected error starting the job :('
            )
            await self.send_message(msg, gitlab_view)
