from typing import Any
from urllib.parse import quote

import requests

from openhands.core.logger import openhands_logger as logger
from openhands.resolver.interfaces.issue import (
    Issue,
    IssueHandlerInterface,
    ReviewThread,
)
from openhands.resolver.utils import extract_issue_references


class GitlabIssueHandler(IssueHandlerInterface):
    def __init__(self, owner: str, repo: str, token: str, username: str | None = None):
        self.owner = owner
        self.repo = repo
        self.token = token
        self.username = username
        self.base_url = self.get_base_url()
        self.download_url = self.get_download_url()
        self.clone_url = self.get_clone_url()
        self.headers = self.get_headers()

    def set_owner(self, owner: str) -> None:
        self.owner = owner

    def get_headers(self) -> dict[str, str]:
        return {
            'Authorization': f'Bearer {self.token}',
            'Accept': 'application/json',
        }

    def get_base_url(self) -> str:
        project_path = quote(f'{self.owner}/{self.repo}', safe='')
        return f'https://gitlab.com/api/v4/projects/{project_path}'

    def get_authorize_url(self) -> str:
        return f'https://{self.username}:{self.token}@gitlab.com/'

    def get_branch_url(self, branch_name: str) -> str:
        return self.get_base_url() + f'/repository/branches/{branch_name}'

    def get_download_url(self) -> str:
        return f'{self.base_url}/issues'

    def get_clone_url(self) -> str:
        username_and_token = self.token
        if self.username:
            username_and_token = f'{self.username}:{self.token}'
        return f'https://{username_and_token}@gitlab.com/{self.owner}/{self.repo}.git'

    def get_graphql_url(self) -> str:
        return 'https://gitlab.com/api/graphql'

    def get_compare_url(self, branch_name: str) -> str:
        return f'https://gitlab.com/{self.owner}/{self.repo}/-/compare/{self.get_default_branch_name()}...{branch_name}'

    def get_converted_issues(
        self, issue_numbers: list[int] | None = None, comment_id: int | None = None
    ) -> list[Issue]:
        """Download issues from Gitlab.

        Args:
            issue_numbers: The numbers of the issues to download
            comment_id: The ID of a single comment, if provided, otherwise all comments

        Returns:
            List of Gitlab issues.
        """

        if not issue_numbers:
            raise ValueError('Unspecified issue number')

        all_issues = self.download_issues()
        logger.info(f'Limiting resolving to issues {issue_numbers}.')
        all_issues = [
            issue
            for issue in all_issues
            # if issue['iid'] in issue_numbers and issue['merge_requests_count'] == 0
            if issue['iid'] in issue_numbers  # TODO for testing
        ]

        if len(issue_numbers) == 1 and not all_issues:
            raise ValueError(f'Issue {issue_numbers[0]} not found')

        converted_issues = []
        for issue in all_issues:
            if any([issue.get(key) is None for key in ['iid', 'title']]):
                logger.warning(f'Skipping issue {issue} as it is missing iid or title.')
                continue

            # Handle empty body by using empty string
            if issue.get('description') is None:
                issue['description'] = ''

            # Get issue thread comments
            thread_comments = self.get_issue_comments(
                issue['iid'], comment_id=comment_id
            )
            # Convert empty lists to None for optional fields
            issue_details = Issue(
                owner=self.owner,
                repo=self.repo,
                number=issue['iid'],
                title=issue['title'],
                body=issue['description'],
                thread_comments=thread_comments,
                review_comments=None,  # Initialize review comments as None for regular issues
            )

            converted_issues.append(issue_details)

        return converted_issues

    def download_issues(self) -> list[Any]:
        params: dict[str, int | str] = {
            'state': 'opened',
            'scope': 'all',
            'per_page': 100,
            'page': 1,
        }
        all_issues = []

        while True:
            response = requests.get(
                self.download_url, headers=self.headers, params=params
            )
            response.raise_for_status()
            issues = response.json()

            if not issues:
                break

            if not isinstance(issues, list) or any(
                [not isinstance(issue, dict) for issue in issues]
            ):
                raise ValueError(
                    'Expected list of dictionaries from Service Gitlab API.'
                )

            all_issues.extend(issues)
            assert isinstance(params['page'], int)
            params['page'] += 1

        return all_issues

    def get_issue_comments(
        self, issue_number: int, comment_id: int | None = None
    ) -> list[str] | None:
        """Download comments for a specific issue from Gitlab."""
        url = f'{self.download_url}/{issue_number}/notes'
        params = {'per_page': 100, 'page': 1}
        all_comments = []

        while True:
            response = requests.get(url, headers=self.headers, params=params)
            response.raise_for_status()
            comments = response.json()

            if not comments:
                break

            if comment_id:
                matching_comment = next(
                    (
                        comment['body']
                        for comment in comments
                        if comment['id'] == comment_id
                    ),
                    None,
                )
                if matching_comment:
                    return [matching_comment]
            else:
                all_comments.extend([comment['body'] for comment in comments])

            params['page'] += 1

        return all_comments if all_comments else None

    def branch_exists(self, branch_name: str) -> bool:
        print(f'Checking if branch {branch_name} exists...')
        response = requests.get(
            f'{self.base_url}/repository/branches/{branch_name}', headers=self.headers
        )
        exists = response.status_code == 200
        print(f'Branch {branch_name} exists: {exists}')
        return exists

    def get_branch_name(self, base_branch_name: str) -> str:
        branch_name = base_branch_name
        attempt = 1
        while self.branch_exists(branch_name):
            attempt += 1
            branch_name = f'{base_branch_name}-try{attempt}'
        return branch_name

    def reply_to_comment(self, pr_number: int, comment_id: str, reply: str) -> None:
        response = requests.get(
            f'{self.base_url}/merge_requests/{pr_number}/discussions/{comment_id.split('/')[-1]}',
            headers=self.headers,
        )
        response.raise_for_status()
        discussions = response.json()
        if len(discussions.get('notes', [])) > 0:
            data = {
                'body': f'Openhands fix success summary\n\n\n{reply}',
                'note_id': discussions.get('notes', [])[-1]['id'],
            }
            response = requests.post(
                f'{self.base_url}/merge_requests/{pr_number}/discussions/{comment_id.split('/')[-1]}/notes',
                headers=self.headers,
                json=data,
            )
            response.raise_for_status()

    def get_pull_url(self, pr_number: int) -> str:
        return (
            f'https://gitlab.com/{self.owner}/{self.repo}/-/merge_requests/{pr_number}'
        )

    def get_default_branch_name(self) -> str:
        response = requests.get(f'{self.base_url}', headers=self.headers)
        response.raise_for_status()
        data = response.json()
        return str(data['default_branch'])

    def create_pull_request(self, data: dict[str, Any] | None = None) -> dict[str, Any]:
        if data is None:
            data = {}
        response = requests.post(
            f'{self.base_url}/merge_requests', headers=self.headers, json=data
        )
        if response.status_code == 403:
            raise RuntimeError(
                'Failed to create pull request due to missing permissions. '
                'Make sure that the provided token has push permissions for the repository.'
            )
        response.raise_for_status()
        pr_data = response.json()
        if 'web_url' in pr_data:
            pr_data['html_url'] = pr_data['web_url']

        if 'iid' in pr_data:
            pr_data['number'] = pr_data['iid']

        return dict(pr_data)

    def request_reviewers(self, reviewer: str, pr_number: int) -> None:
        response = requests.get(
            f'https://gitlab.com/api/v4/users?username={reviewer}',
            headers=self.headers,
        )
        response.raise_for_status()
        user_data = response.json()
        if len(user_data) > 0:
            review_data = {'reviewer_ids': [user_data[0]['id']]}
            review_response = requests.put(
                f'{self.base_url}/merge_requests/{pr_number}',
                headers=self.headers,
                json=review_data,
            )
            if review_response.status_code != 200:
                print(
                    f'Warning: Failed to request review from {reviewer}: {review_response.text}'
                )

    def send_comment_msg(self, issue_number: int, msg: str) -> None:
        """Send a comment message to a GitHub issue or pull request.

        Args:
            issue_number: The issue or pull request number
            msg: The message content to post as a comment
        """
        # Post a comment on the PR
        comment_url = f'{self.base_url}/issues/{issue_number}/notes'
        comment_data = {'body': msg}
        comment_response = requests.post(
            comment_url, headers=self.headers, json=comment_data
        )
        if comment_response.status_code != 201:
            print(
                f'Failed to post comment: {comment_response.status_code} {comment_response.text}'
            )
        else:
            print(f'Comment added to the PR: {msg}')

    def get_context_from_external_issues_references(
        self,
        closing_issues: list[str],
        closing_issue_numbers: list[int],
        issue_body: str,
        review_comments: list[str] | None,
        review_threads: list[ReviewThread],
        thread_comments: list[str] | None,
    ) -> list[str]:
        return []


class GitlabPRHandler(GitlabIssueHandler):
    def __init__(self, owner: str, repo: str, token: str, username: str | None = None):
        super().__init__(owner, repo, token, username)
        self.download_url = f'{self.base_url}/merge_requests'

    def download_pr_metadata(
        self, pull_number: int, comment_id: int | None = None
    ) -> tuple[list[str], list[int], list[str] | None, list[ReviewThread], list[str]]:
        """Run a GraphQL query against the Gitlab API for information.

        Retrieves information about:
            1. unresolved review comments
            2. referenced issues the pull request would close

        Args:
            pull_number: The number of the pull request to query.
            comment_id: Optional ID of a specific comment to focus on.
            query: The GraphQL query as a string.
            variables: A dictionary of variables for the query.
            token: Your Gitlab personal access token.

        Returns:
            The JSON response from the Gitlab API.
        """
        # Using graphql as REST API doesn't indicate resolved status for review comments
        # TODO: grabbing the first 10 issues, 100 review threads, and 100 coments; add pagination to retrieve all
        response = requests.get(
            f'{self.base_url}/merge_requests/{pull_number}/related_issues',
            headers=self.headers,
        )
        response.raise_for_status()
        closing_issues = response.json()
        closing_issues_bodies = [issue['description'] for issue in closing_issues]
        closing_issue_numbers = [
            issue['iid'] for issue in closing_issues
        ]  # Extract issue numbers

        query = """
                query($projectPath: ID!, $pr: String!) {
                    project(fullPath: $projectPath) {
                        mergeRequest(iid: $pr) {
                            webUrl
                            discussions(first: 100) {
                                edges {
                                    node {
                                        id
                                        resolved
                                        resolvable
                                        notes(first: 100) {
                                            nodes {
                                                body
                                                id
                                                position {
                                                    filePath
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            """

        project_path = f'{self.owner}/{self.repo}'
        variables = {'projectPath': project_path, 'pr': str(pull_number)}

        response = requests.post(
            self.get_graphql_url(),
            json={'query': query, 'variables': variables},
            headers=self.headers,
        )
        response.raise_for_status()
        response_json = response.json()

        # Parse the response to get closing issue references and unresolved review comments
        pr_data = (
            response_json.get('data', {}).get('project', {}).get('mergeRequest', {})
        )

        # Get review comments
        review_bodies = None

        # Get unresolved review threads
        review_threads = []
        thread_ids = []  # Store thread IDs; agent replies to the thread
        raw_review_threads = pr_data.get('discussions', {}).get('edges', [])

        for thread in raw_review_threads:
            node = thread.get('node', {})
            if not node.get('resolved', True) and node.get(
                'resolvable', True
            ):  # Check if the review thread is unresolved
                id = node.get('id')
                thread_contains_comment_id = False
                my_review_threads = node.get('notes', {}).get('nodes', [])
                message = ''
                files = []
                for i, review_thread in enumerate(my_review_threads):
                    if (
                        comment_id is not None
                        and int(review_thread['id'].split('/')[-1]) == comment_id
                    ):
                        thread_contains_comment_id = True

                    if (
                        i == len(my_review_threads) - 1
                    ):  # Check if it's the last thread in the thread
                        if len(my_review_threads) > 1:
                            message += '---\n'  # Add "---" before the last message if there's more than one thread
                        message += 'latest feedback:\n' + review_thread['body'] + '\n'
                    else:
                        message += (
                            review_thread['body'] + '\n'
                        )  # Add each thread in a new line

                    file = review_thread.get('position', {})
                    file = file.get('filePath') if file is not None else None
                    if file and file not in files:
                        files.append(file)

                if comment_id is None or thread_contains_comment_id:
                    unresolved_thread = ReviewThread(comment=message, files=files)
                    review_threads.append(unresolved_thread)
                    thread_ids.append(id)

        return (
            closing_issues_bodies,
            closing_issue_numbers,
            review_bodies,
            review_threads,
            thread_ids,
        )

    # Override processing of downloaded issues
    def get_pr_comments(
        self, pr_number: int, comment_id: int | None = None
    ) -> list[str] | None:
        """Download comments for a specific pull request from Gitlab."""
        url = f'{self.base_url}/merge_requests/{pr_number}/notes'
        params = {'per_page': 100, 'page': 1}
        all_comments = []

        while True:
            response = requests.get(url, headers=self.headers, params=params)
            response.raise_for_status()
            comments = response.json()
            comments = [
                comment
                for comment in comments
                if comment.get('resolvable', True) and not comment.get('system', True)
            ]

            if not comments:
                break

            if comment_id is not None:
                matching_comment = next(
                    (
                        comment['body']
                        for comment in comments
                        if comment['id'] == comment_id
                    ),
                    None,
                )
                if matching_comment:
                    return [matching_comment]
            else:
                all_comments.extend([comment['body'] for comment in comments])

            params['page'] += 1

        return all_comments if all_comments else None

    def get_context_from_external_issues_references(
        self,
        closing_issues: list[str],
        closing_issue_numbers: list[int],
        issue_body: str,
        review_comments: list[str] | None,
        review_threads: list[ReviewThread],
        thread_comments: list[str] | None,
    ) -> list[str]:
        new_issue_references = []

        if issue_body:
            new_issue_references.extend(extract_issue_references(issue_body))

        if review_comments:
            for comment in review_comments:
                new_issue_references.extend(extract_issue_references(comment))

        if review_threads:
            for review_thread in review_threads:
                new_issue_references.extend(
                    extract_issue_references(review_thread.comment)
                )

        if thread_comments:
            for thread_comment in thread_comments:
                new_issue_references.extend(extract_issue_references(thread_comment))

        non_duplicate_references = set(new_issue_references)
        unique_issue_references = non_duplicate_references.difference(
            closing_issue_numbers
        )

        for issue_number in unique_issue_references:
            try:
                url = f'{self.base_url}/issues/{issue_number}'
                response = requests.get(url, headers=self.headers)
                response.raise_for_status()
                issue_data = response.json()
                issue_body = issue_data.get('description', '')
                if issue_body:
                    closing_issues.append(issue_body)
            except requests.exceptions.RequestException as e:
                logger.warning(f'Failed to fetch issue {issue_number}: {str(e)}')

        return closing_issues

    def get_converted_issues(
        self, issue_numbers: list[int] | None = None, comment_id: int | None = None
    ) -> list[Issue]:
        if not issue_numbers:
            raise ValueError('Unspecified issue numbers')

        all_issues = self.download_issues()
        logger.info(f'Limiting resolving to issues {issue_numbers}.')
        all_issues = [issue for issue in all_issues if issue['iid'] in issue_numbers]

        converted_issues = []
        for issue in all_issues:
            # For PRs, body can be None
            if any([issue.get(key) is None for key in ['iid', 'title']]):
                logger.warning(f'Skipping #{issue} as it is missing iid or title.')
                continue

            # Handle None body for PRs
            body = (
                issue.get('description') if issue.get('description') is not None else ''
            )
            (
                closing_issues,
                closing_issues_numbers,
                review_comments,
                review_threads,
                thread_ids,
            ) = self.download_pr_metadata(issue['iid'], comment_id=comment_id)
            head_branch = issue['source_branch']

            # Get PR thread comments
            thread_comments = self.get_pr_comments(issue['iid'], comment_id=comment_id)

            closing_issues = self.get_context_from_external_issues_references(
                closing_issues,
                closing_issues_numbers,
                body,
                review_comments,
                review_threads,
                thread_comments,
            )

            issue_details = Issue(
                owner=self.owner,
                repo=self.repo,
                number=issue['iid'],
                title=issue['title'],
                body=body,
                closing_issues=closing_issues,
                review_comments=review_comments,
                review_threads=review_threads,
                thread_ids=thread_ids,
                head_branch=head_branch,
                thread_comments=thread_comments,
            )

            converted_issues.append(issue_details)

        return converted_issues
