"""GitLab integration via python-gitlab library."""

from __future__ import annotations

import os
from dataclasses import dataclass
from functools import lru_cache

import gitlab
from gitlab.exceptions import GitlabAuthenticationError, GitlabGetError


@dataclass
class GitLabConfig:
    """GitLab connection configuration."""

    url: str
    private_token: str | None = None
    job_token: str | None = None
    oauth_token: str | None = None

    @classmethod
    def from_environment(cls, host: str | None = None) -> "GitLabConfig":
        """Create config from environment variables.

        Checks (in order):
            1. CI_JOB_TOKEN (for CI pipelines)
            2. GITLAB_PRIVATE_TOKEN
            3. GITLAB_TOKEN (alias)
            4. GITLAB_OAUTH_TOKEN

        Args:
            host: GitLab host (e.g., "gitlab.com"). If None, uses GITLAB_URL env
                  or defaults to "gitlab.com".

        Returns:
            GitLabConfig instance.
        """
        url = os.environ.get("GITLAB_URL")
        if not url:
            url = f"https://{host}" if host else "https://gitlab.com"

        return cls(
            url=url,
            job_token=os.environ.get("CI_JOB_TOKEN"),
            private_token=os.environ.get("GITLAB_PRIVATE_TOKEN")
            or os.environ.get("GITLAB_TOKEN"),
            oauth_token=os.environ.get("GITLAB_OAUTH_TOKEN"),
        )

    @property
    def is_authenticated(self) -> bool:
        """Check if any authentication token is configured."""
        return bool(self.private_token or self.job_token or self.oauth_token)


@lru_cache(maxsize=1)
def _get_gitlab_client(url: str, token: str | None, job_token: str | None) -> gitlab.Gitlab:
    """Get a cached GitLab client instance."""
    if job_token:
        return gitlab.Gitlab(url, job_token=job_token)
    elif token:
        return gitlab.Gitlab(url, private_token=token)
    else:
        return gitlab.Gitlab(url)


def get_gitlab_client(config: GitLabConfig | None = None) -> gitlab.Gitlab | None:
    """Get a GitLab client instance.

    Args:
        config: GitLab configuration. If None, reads from environment.

    Returns:
        Authenticated GitLab client, or None if not configured.
    """
    if config is None:
        config = GitLabConfig.from_environment()

    if not config.is_authenticated:
        return None

    try:
        gl = _get_gitlab_client(
            config.url,
            config.private_token or config.oauth_token,
            config.job_token,
        )
        gl.auth()
        return gl
    except GitlabAuthenticationError:
        return None


def gitlab_available(host: str | None = None) -> bool:
    """Check if GitLab authentication is available.

    Args:
        host: GitLab host to check against.

    Returns:
        True if authenticated successfully.
    """
    config = GitLabConfig.from_environment(host)
    if not config.is_authenticated:
        return False

    client = get_gitlab_client(config)
    return client is not None


@dataclass
class GitLabIssue:
    """Represents a GitLab issue."""

    number: str
    url: str
    title: str
    state: str
    assignees: list[str]
    labels: list[str]

    @classmethod
    def from_api_response(cls, issue) -> "GitLabIssue":
        """Create from python-gitlab issue object."""
        return cls(
            number=str(issue.iid),
            url=issue.web_url,
            title=issue.title,
            state=issue.state,
            assignees=[a["username"] for a in issue.assignees] if issue.assignees else [],
            labels=issue.labels or [],
        )


@dataclass
class GitLabMember:
    """Represents a GitLab project member."""

    username: str
    name: str
    access_level: int

    @property
    def access_level_name(self) -> str:
        """Human-readable access level."""
        levels = {
            50: "Owner",
            40: "Maintainer",
            30: "Developer",
            20: "Reporter",
            10: "Guest",
        }
        return levels.get(self.access_level, f"Level {self.access_level}")

    @classmethod
    def from_api_response(cls, member) -> "GitLabMember":
        """Create from python-gitlab member object."""
        return cls(
            username=member.username,
            name=member.name,
            access_level=member.access_level,
        )


def _get_project(gl: gitlab.Gitlab, project_path: str):
    """Get a project by path.

    Args:
        gl: GitLab client.
        project_path: Project path (e.g., "user/project").

    Returns:
        Project object or None.
    """
    try:
        return gl.projects.get(project_path)
    except GitlabGetError:
        return None


def create_issue(
    title: str,
    description: str,
    project_path: str,
    host: str | None = None,
    labels: list[str] | None = None,
    assignee: str | None = None,
) -> GitLabIssue | None:
    """Create a GitLab issue.

    Args:
        title: Issue title.
        description: Issue body (markdown).
        project_path: Project path (e.g., "user/project").
        host: GitLab host.
        labels: Labels to apply.
        assignee: Username to assign (without @).

    Returns:
        Created issue, or None on failure.
    """
    config = GitLabConfig.from_environment(host)
    gl = get_gitlab_client(config)

    if not gl:
        return None

    project = _get_project(gl, project_path)
    if not project:
        return None

    issue_data = {
        "title": title,
        "description": description,
    }

    if labels:
        issue_data["labels"] = labels

    if assignee:
        # Look up user ID from username
        username = assignee.lstrip("@")
        try:
            users = gl.users.list(username=username)
            if users:
                issue_data["assignee_ids"] = [users[0].id]
        except Exception:
            pass  # Continue without assignee if lookup fails

    try:
        issue = project.issues.create(issue_data)
        return GitLabIssue.from_api_response(issue)
    except Exception:
        return None


def get_issue(
    issue_number: str,
    project_path: str,
    host: str | None = None,
) -> GitLabIssue | None:
    """Get details for an issue.

    Args:
        issue_number: Issue number (without #).
        project_path: Project path (e.g., "user/project").
        host: GitLab host.

    Returns:
        Issue details, or None if not found.
    """
    config = GitLabConfig.from_environment(host)
    gl = get_gitlab_client(config)

    if not gl:
        return None

    project = _get_project(gl, project_path)
    if not project:
        return None

    try:
        issue = project.issues.get(int(issue_number))
        return GitLabIssue.from_api_response(issue)
    except Exception:
        return None


def get_project_members(
    project_path: str,
    host: str | None = None,
) -> list[GitLabMember]:
    """Get all project members.

    Args:
        project_path: Project path (e.g., "user/project").
        host: GitLab host.

    Returns:
        List of members, empty list on failure.
    """
    config = GitLabConfig.from_environment(host)
    gl = get_gitlab_client(config)

    if not gl:
        return []

    project = _get_project(gl, project_path)
    if not project:
        return []

    try:
        # Get all members including inherited
        members = project.members_all.list(all=True)
        return [GitLabMember.from_api_response(m) for m in members]
    except Exception:
        return []


def close_issue(
    issue_number: str,
    project_path: str,
    host: str | None = None,
    comment: str | None = None,
) -> bool:
    """Close a GitLab issue.

    Args:
        issue_number: Issue number (without #).
        project_path: Project path (e.g., "user/project").
        host: GitLab host.
        comment: Optional comment to add before closing.

    Returns:
        True if closed successfully, False otherwise.
    """
    config = GitLabConfig.from_environment(host)
    gl = get_gitlab_client(config)

    if not gl:
        return False

    project = _get_project(gl, project_path)
    if not project:
        return False

    try:
        issue = project.issues.get(int(issue_number))

        # Add comment if provided
        if comment:
            issue.notes.create({"body": comment})

        # Close the issue
        issue.state_event = "close"
        issue.save()
        return True
    except Exception:
        return False


def add_issue_comment(
    issue_number: str,
    project_path: str,
    comment: str,
    host: str | None = None,
) -> bool:
    """Add a comment to a GitLab issue.

    Args:
        issue_number: Issue number (without #).
        project_path: Project path (e.g., "user/project").
        comment: Comment text.
        host: GitLab host.

    Returns:
        True if comment added successfully, False otherwise.
    """
    config = GitLabConfig.from_environment(host)
    gl = get_gitlab_client(config)

    if not gl:
        return False

    project = _get_project(gl, project_path)
    if not project:
        return False

    try:
        issue = project.issues.get(int(issue_number))
        issue.notes.create({"body": comment})
        return True
    except Exception:
        return False


def get_compare_url(
    project: str,
    host: str,
    from_commit: str,
    to_commit: str,
) -> str:
    """Generate a GitLab compare URL.

    Args:
        project: Project path (e.g., "user/project").
        host: GitLab host (e.g., "gitlab.com").
        from_commit: Base commit SHA.
        to_commit: Head commit SHA.

    Returns:
        Compare URL.
    """
    return (
        f"https://{host}/{project}/-/compare/"
        f"{from_commit}...{to_commit}?from_project_id=&straight=false"
    )
