import re

import jwt
from integrations.manager import Manager
from integrations.models import Message, SourceType
from integrations.slack.slack_types import SlackViewInterface, StartingConvoException
from integrations.slack.slack_view import (
    SlackFactory,
    SlackNewConversationFromRepoFormView,
    SlackNewConversationView,
    SlackUnkownUserView,
    SlackUpdateExistingConversationView,
)
from integrations.utils import (
    HOST_URL,
    OPENHANDS_RESOLVER_TEMPLATES_DIR,
)
from jinja2 import Environment, FileSystemLoader
from pydantic import SecretStr
from server.auth.saas_user_auth import SaasUserAuth
from server.constants import SLACK_CLIENT_ID
from server.utils.conversation_callback_utils import register_callback_processor
from slack_sdk.oauth import AuthorizeUrlGenerator
from slack_sdk.web.async_client import AsyncWebClient
from storage.database import session_maker
from storage.slack_user import SlackUser

from openhands.core.logger import openhands_logger as logger
from openhands.integrations.provider import ProviderHandler
from openhands.integrations.service_types import Repository
from openhands.server.shared import config, server_config
from openhands.server.types import LLMAuthenticationError, MissingSettingsError
from openhands.server.user_auth.user_auth import UserAuth

authorize_url_generator = AuthorizeUrlGenerator(
    client_id=SLACK_CLIENT_ID,
    scopes=['app_mentions:read', 'chat:write'],
    user_scopes=['search:read'],
)


class SlackManager(Manager):
    def __init__(self, token_manager):
        self.token_manager = token_manager
        self.login_link = (
            'User has not yet authenticated: [Click here to Login to OpenHands]({}).'
        )

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

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

    async def _get_user_auth(self, keycloak_user_id: str) -> UserAuth:
        offline_token = await self.token_manager.load_offline_token(keycloak_user_id)
        if offline_token is None:
            logger.info('no_offline_token_found')

        user_auth = SaasUserAuth(
            user_id=keycloak_user_id,
            refresh_token=SecretStr(offline_token),
        )
        return user_auth

    async def authenticate_user(
        self, slack_user_id: str
    ) -> tuple[SlackUser | None, UserAuth | None]:
        # We get the user and correlate them back to a user in OpenHands - if we can
        slack_user = None
        with session_maker() as session:
            slack_user = (
                session.query(SlackUser)
                .filter(SlackUser.slack_user_id == slack_user_id)
                .first()
            )

            # slack_view.slack_to_openhands_user = slack_user # attach user auth info to view

        saas_user_auth = None
        if slack_user:
            saas_user_auth = await self._get_user_auth(slack_user.keycloak_user_id)
            # slack_view.saas_user_auth = await self._get_user_auth(slack_view.slack_to_openhands_user.keycloak_user_id)

        return slack_user, saas_user_auth

    def _infer_repo_from_message(self, user_msg: str) -> str | None:
        # Regular expression to match patterns like "All-Hands-AI/OpenHands" or "deploy repo"
        pattern = r'([a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+)|([a-zA-Z0-9_-]+)(?=\s+repo)'
        match = re.search(pattern, user_msg)

        if match:
            repo = match.group(1) if match.group(1) else match.group(2)
            return repo

        return None

    async def _get_repositories(self, user_auth: UserAuth) -> list[Repository]:
        provider_tokens = await user_auth.get_provider_tokens()
        if provider_tokens is None:
            return []
        access_token = await user_auth.get_access_token()
        user_id = await user_auth.get_user_id()
        client = ProviderHandler(
            provider_tokens=provider_tokens,
            external_auth_token=access_token,
            external_auth_id=user_id,
        )
        repos: list[Repository] = await client.get_repositories(
            'pushed', server_config.app_mode, None, None, None, None
        )
        return repos

    def _generate_repo_selection_form(
        self, repo_list: list[Repository], message_ts: str, thread_ts: str | None
    ):
        options = [
            {
                'text': {'type': 'plain_text', 'text': 'No Repository'},
                'value': '-',
            }
        ]
        options.extend(
            {
                'text': {
                    'type': 'plain_text',
                    'text': repo.full_name,
                },
                'value': repo.full_name,
            }
            for repo in repo_list
        )

        return [
            {
                'type': 'header',
                'text': {
                    'type': 'plain_text',
                    'text': 'Choose a repository',
                    'emoji': True,
                },
            },
            {
                'type': 'actions',
                'elements': [
                    {
                        'type': 'static_select',
                        'action_id': f'repository_select:{message_ts}:{thread_ts}',
                        'options': options,
                    }
                ],
            },
        ]

    def filter_potential_repos_by_user_msg(
        self, user_msg: str, user_repos: list[Repository]
    ) -> tuple[bool, list[Repository]]:
        inferred_repo = self._infer_repo_from_message(user_msg)
        if not inferred_repo:
            return False, user_repos[0:99]

        final_repos = []
        for repo in user_repos:
            if inferred_repo.lower() in repo.full_name.lower():
                final_repos.append(repo)

        # no repos matched, return original list
        if len(final_repos) == 0:
            return False, user_repos[0:99]

        # Found exact match
        elif len(final_repos) == 1:
            return True, final_repos

        # Found partial matches
        return False, final_repos[0:99]

    async def receive_message(self, message: Message):
        self._confirm_incoming_source_type(message)

        slack_user, saas_user_auth = await self.authenticate_user(
            slack_user_id=message.message['slack_user_id']
        )

        try:
            slack_view = SlackFactory.create_slack_view_from_payload(
                message, slack_user, saas_user_auth
            )
        except Exception as e:
            logger.error(
                f'[Slack]: Failed to create slack view: {e}',
                exc_info=True,
                stack_info=True,
            )
            return

        if isinstance(slack_view, SlackUnkownUserView):
            jwt_secret = config.jwt_secret
            if not jwt_secret:
                raise ValueError('Must configure jwt_secret')
            state = jwt.encode(
                message.message, jwt_secret.get_secret_value(), algorithm='HS256'
            )
            link = authorize_url_generator.generate(state)
            msg = self.login_link.format(link)

            logger.info('slack_not_yet_authenticated')
            await self.send_message(
                self.create_outgoing_message(msg, ephemeral=True), slack_view
            )
            return

        if not await self.is_job_requested(message, slack_view):
            return

        await self.start_job(slack_view)

    async def send_message(self, message: Message, slack_view: SlackViewInterface):
        client = AsyncWebClient(token=slack_view.bot_access_token)
        if message.ephemeral and isinstance(message.message, str):
            await client.chat_postEphemeral(
                channel=slack_view.channel_id,
                markdown_text=message.message,
                user=slack_view.slack_user_id,
                thread_ts=slack_view.thread_ts,
            )
        elif message.ephemeral and isinstance(message.message, dict):
            await client.chat_postEphemeral(
                channel=slack_view.channel_id,
                user=slack_view.slack_user_id,
                thread_ts=slack_view.thread_ts,
                text=message.message['text'],
                blocks=message.message['blocks'],
            )
        else:
            await client.chat_postMessage(
                channel=slack_view.channel_id,
                markdown_text=message.message,
                thread_ts=slack_view.message_ts,
            )

    async def is_job_requested(
        self, message: Message, slack_view: SlackViewInterface
    ) -> bool:
        """
        A job is always request we only receive webhooks for events associated with the slack bot
        This method really just checks
            1. Is the user is authenticated
            2. Do we have the necessary information to start a job (either by inferring the selected repo, otherwise asking the user)
        """

        # Infer repo from user message is not needed; user selected repo from the form or is updating existing convo
        if isinstance(slack_view, SlackUpdateExistingConversationView):
            return True
        elif isinstance(slack_view, SlackNewConversationFromRepoFormView):
            return True
        elif isinstance(slack_view, SlackNewConversationView):
            user = slack_view.slack_to_openhands_user
            user_repos: list[Repository] = await self._get_repositories(
                slack_view.saas_user_auth
            )
            match, repos = self.filter_potential_repos_by_user_msg(
                slack_view.user_msg, user_repos
            )

            # User mentioned a matching repo is their message, start job without repo selection form
            if match:
                slack_view.selected_repo = repos[0].full_name
                return True

            logger.info(
                'render_repository_selector',
                extra={
                    'slack_user_id': user,
                    'keycloak_user_id': user.keycloak_user_id,
                    'message_ts': slack_view.message_ts,
                    'thread_ts': slack_view.thread_ts,
                },
            )

            repo_selection_msg = {
                'text': 'Choose a Repository:',
                'blocks': self._generate_repo_selection_form(
                    repos, slack_view.message_ts, slack_view.thread_ts
                ),
            }
            await self.send_message(
                self.create_outgoing_message(repo_selection_msg, ephemeral=True),
                slack_view,
            )

            return False

        return True

    async def start_job(self, slack_view: SlackViewInterface):
        # Importing here prevents circular import
        from server.conversation_callback_processor.slack_callback_processor import (
            SlackCallbackProcessor,
        )

        try:
            msg_info = None
            user_info: SlackUser = slack_view.slack_to_openhands_user
            try:
                logger.info(
                    f'[Slack] Starting job for user {user_info.slack_display_name} (id={user_info.slack_user_id})',
                    extra={'keyloak_user_id': user_info.keycloak_user_id},
                )
                conversation_id = await slack_view.create_or_update_conversation(
                    self.jinja_env
                )

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

                if not isinstance(slack_view, SlackUpdateExistingConversationView):
                    # We don't re-subscribe for follow up messages from slack.
                    # Summaries are generated for every messages anyways, we only need to do
                    # this subscription once for the event which kicked off the job.
                    processor = SlackCallbackProcessor(
                        slack_user_id=slack_view.slack_user_id,
                        channel_id=slack_view.channel_id,
                        message_ts=slack_view.message_ts,
                        thread_ts=slack_view.thread_ts,
                        team_id=slack_view.team_id,
                    )

                    # Register the callback processor
                    register_callback_processor(conversation_id, processor)

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

                msg_info = slack_view.get_response_msg()

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

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

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

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

            except StartingConvoException as e:
                msg_info = str(e)

            await self.send_message(self.create_outgoing_message(msg_info), slack_view)

        except Exception:
            logger.exception('[Slack]: Error starting job')
            msg = 'Uh oh! There was an unexpected error starting the job :('
            await self.send_message(self.create_outgoing_message(msg), slack_view)
