"""External provider API clients for GitHub, GitLab, and Jira.

One-way integrations (Proxym -> External) without webhooks.
Supports project creation, issue management, and sync via API keys.
"""
from __future__ import annotations

import os
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any

import httpx


@dataclass
class ExternalProject:
    """Unified project representation across providers."""
    id: str
    name: str
    description: str | None
    url: str
    provider: str  # github, gitlab, jira
    external_id: str | None = None
    namespace: str | None = None  # owner/org


@dataclass
class ExternalIssue:
    """Unified issue/ticket representation."""
    id: str
    number: int
    title: str
    description: str | None
    state: str  # open, closed, etc.
    url: str
    labels: list[str]
    external_id: str | None = None


class BaseExternalProvider(ABC):
    """Base class for external provider integrations."""

    def __init__(self, api_key: str, base_url: str | None = None):
        self.api_key = api_key
        self.base_url = base_url
        self._client: httpx.AsyncClient | None = None

    async def __aenter__(self):
        self._client = httpx.AsyncClient(
            headers=self._get_headers(),
            timeout=30.0,
        )
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        if self._client:
            await self._client.aclose()
            self._client = None

    @abstractmethod
    def _get_headers(self) -> dict[str, str]:
        """Return HTTP headers for API requests."""
        pass

    @abstractmethod
    def get_name(self) -> str:
        """Return provider name."""
        pass

    @abstractmethod
    async def validate_credentials(self) -> bool:
        """Validate that the API key works."""
        pass

    @abstractmethod
    async def list_projects(self) -> list[ExternalProject]:
        """List accessible projects/repos."""
        pass

    @abstractmethod
    async def create_project(
        self, name: str, description: str | None = None, namespace: str | None = None
    ) -> ExternalProject:
        """Create a new project/repo."""
        pass

    @abstractmethod
    async def create_issue(
        self, project_id: str, title: str, description: str | None = None, labels: list[str] | None = None
    ) -> ExternalIssue:
        """Create an issue/ticket in the project."""
        pass

    @abstractmethod
    async def update_issue_status(self, project_id: str, issue_id: str, status: str) -> bool:
        """Update issue status (open/closed)."""
        pass

    @abstractmethod
    async def list_issues(self, project_id: str, state: str = "all") -> list[ExternalIssue]:
        """List issues in a project."""
        pass


class GitHubProvider(BaseExternalProvider):
    """GitHub API integration using Personal Access Tokens."""

    DEFAULT_BASE_URL = "https://api.github.com"

    def __init__(self, api_key: str, base_url: str | None = None):
        super().__init__(api_key, base_url or self.DEFAULT_BASE_URL)

    def _get_headers(self) -> dict[str, str]:
        return {
            "Authorization": f"Bearer {self.api_key}",
            "Accept": "application/vnd.github+json",
            "X-GitHub-Api-Version": "2022-11-28",
        }

    def get_name(self) -> str:
        return "github"

    async def validate_credentials(self) -> bool:
        if not self._client:
            raise RuntimeError("Provider not in async context")
        try:
            response = await self._client.get(f"{self.base_url}/user")
            return response.status_code == 200
        except httpx.HTTPError:
            return False

    async def list_projects(self) -> list[ExternalProject]:
        if not self._client:
            raise RuntimeError("Provider not in async context")

        repos: list[ExternalProject] = []
        page = 1

        while True:
            response = await self._client.get(
                f"{self.base_url}/user/repos",
                params={"page": page, "per_page": 100, "sort": "updated"},
            )
            response.raise_for_status()
            data = response.json()

            if not data:
                break

            for repo in data:
                repos.append(ExternalProject(
                    id=str(repo["id"]),
                    external_id=repo["full_name"],
                    name=repo["name"],
                    namespace=repo["owner"]["login"],
                    description=repo.get("description"),
                    url=repo["html_url"],
                    provider="github",
                ))

            page += 1
            if len(data) < 100:
                break

        return repos

    async def create_project(
        self, name: str, description: str | None = None, namespace: str | None = None
    ) -> ExternalProject:
        if not self._client:
            raise RuntimeError("Provider not in async context")

        payload: dict[str, Any] = {"name": name}
        if description:
            payload["description"] = description
        payload["private"] = True

        # If namespace (org) provided, create under org
        if namespace:
            endpoint = f"{self.base_url}/orgs/{namespace}/repos"
        else:
            endpoint = f"{self.base_url}/user/repos"

        response = await self._client.post(endpoint, json=payload)
        response.raise_for_status()
        repo = response.json()

        return ExternalProject(
            id=str(repo["id"]),
            external_id=repo["full_name"],
            name=repo["name"],
            namespace=repo["owner"]["login"],
            description=repo.get("description"),
            url=repo["html_url"],
            provider="github",
        )

    async def create_issue(
        self, project_id: str, title: str, description: str | None = None, labels: list[str] | None = None
    ) -> ExternalIssue:
        if not self._client:
            raise RuntimeError("Provider not in async context")

        # project_id is full_name (owner/repo)
        payload: dict[str, Any] = {"title": title}
        if description:
            payload["body"] = description
        if labels:
            payload["labels"] = labels

        response = await self._client.post(
            f"{self.base_url}/repos/{project_id}/issues",
            json=payload,
        )
        response.raise_for_status()
        issue = response.json()

        return ExternalIssue(
            id=str(issue["id"]),
            number=issue["number"],
            title=issue["title"],
            description=issue.get("body"),
            state=issue["state"],
            url=issue["html_url"],
            labels=[label["name"] for label in issue.get("labels", [])],
            external_id=project_id,
        )

    async def update_issue_status(self, project_id: str, issue_id: str, status: str) -> bool:
        if not self._client:
            raise RuntimeError("Provider not in async context")

        # issue_id is issue number
        payload: dict[str, Any] = {"state": status}

        response = await self._client.patch(
            f"{self.base_url}/repos/{project_id}/issues/{issue_id}",
            json=payload,
        )
        return response.status_code == 200

    async def list_issues(self, project_id: str, state: str = "all") -> list[ExternalIssue]:
        if not self._client:
            raise RuntimeError("Provider not in async context")

        params: dict[str, Any] = {"state": state}
        response = await self._client.get(
            f"{self.base_url}/repos/{project_id}/issues",
            params=params,
        )
        response.raise_for_status()
        data = response.json()

        return [
            ExternalIssue(
                id=str(issue["id"]),
                number=issue["number"],
                title=issue["title"],
                description=issue.get("body"),
                state=issue["state"],
                url=issue["html_url"],
                labels=[label["name"] for label in issue.get("labels", [])],
                external_id=project_id,
            )
            for issue in data
        ]


class GitLabProvider(BaseExternalProvider):
    """GitLab API integration using Personal/Project Access Tokens."""

    DEFAULT_BASE_URL = "https://gitlab.com/api/v4"

    def __init__(self, api_key: str, base_url: str | None = None):
        # Support self-hosted GitLab
        super().__init__(api_key, base_url or self.DEFAULT_BASE_URL)

    def _get_headers(self) -> dict[str, str]:
        return {
            "PRIVATE-TOKEN": self.api_key,
        }

    def get_name(self) -> str:
        return "gitlab"

    async def validate_credentials(self) -> bool:
        if not self._client:
            raise RuntimeError("Provider not in async context")
        try:
            response = await self._client.get(f"{self.base_url}/user")
            return response.status_code == 200
        except httpx.HTTPError:
            return False

    async def list_projects(self) -> list[ExternalProject]:
        if not self._client:
            raise RuntimeError("Provider not in async context")

        projects: list[ExternalProject] = []
        page = 1

        while True:
            response = await self._client.get(
                f"{self.base_url}/projects",
                params={
                    "page": page,
                    "per_page": 100,
                    "membership": True,
                    "order_by": "last_activity_at",
                    "sort": "desc",
                },
            )
            response.raise_for_status()
            data = response.json()

            if not data:
                break

            for proj in data:
                projects.append(ExternalProject(
                    id=str(proj["id"]),
                    external_id=str(proj["id"]),
                    name=proj["name"],
                    namespace=proj.get("namespace", {}).get("name") if isinstance(proj.get("namespace"), dict) else None,
                    description=proj.get("description"),
                    url=proj["web_url"],
                    provider="gitlab",
                ))

            page += 1
            if len(data) < 100:
                break

        return projects

    async def create_project(
        self, name: str, description: str | None = None, namespace: str | None = None
    ) -> ExternalProject:
        if not self._client:
            raise RuntimeError("Provider not in async context")

        payload: dict[str, Any] = {
            "name": name,
            "visibility": "private",
        }
        if description:
            payload["description"] = description
        if namespace:
            # Need to resolve namespace ID
            ns_response = await self._client.get(
                f"{self.base_url}/groups/{namespace}"
            )
            if ns_response.status_code == 200:
                ns_data = ns_response.json()
                payload["namespace_id"] = ns_data["id"]

        response = await self._client.post(
            f"{self.base_url}/projects",
            json=payload,
        )
        response.raise_for_status()
        proj = response.json()

        return ExternalProject(
            id=str(proj["id"]),
            external_id=str(proj["id"]),
            name=proj["name"],
            namespace=proj.get("namespace", {}).get("name") if isinstance(proj.get("namespace"), dict) else None,
            description=proj.get("description"),
            url=proj["web_url"],
            provider="gitlab",
        )

    async def create_issue(
        self, project_id: str, title: str, description: str | None = None, labels: list[str] | None = None
    ) -> ExternalIssue:
        if not self._client:
            raise RuntimeError("Provider not in async context")

        payload: dict[str, Any] = {"title": title}
        if description:
            payload["description"] = description
        if labels:
            payload["labels"] = ",".join(labels)

        response = await self._client.post(
            f"{self.base_url}/projects/{project_id}/issues",
            json=payload,
        )
        response.raise_for_status()
        issue = response.json()

        return ExternalIssue(
            id=str(issue["id"]),
            number=issue["iid"],
            title=issue["title"],
            description=issue.get("description"),
            state=issue["state"],
            url=issue["web_url"],
            labels=issue.get("labels", []),
            external_id=project_id,
        )

    async def update_issue_status(self, project_id: str, issue_id: str, status: str) -> bool:
        if not self._client:
            raise RuntimeError("Provider not in async context")

        # GitLab uses state_event: close/reopen
        state_event = "close" if status == "closed" else "reopen"

        response = await self._client.put(
            f"{self.base_url}/projects/{project_id}/issues/{issue_id}",
            json={"state_event": state_event},
        )
        return response.status_code == 200

    async def list_issues(self, project_id: str, state: str = "all") -> list[ExternalIssue]:
        if not self._client:
            raise RuntimeError("Provider not in async context")

        params: dict[str, str] = {}
        if state != "all":
            params["state"] = state

        response = await self._client.get(
            f"{self.base_url}/projects/{project_id}/issues",
            params=params,
        )
        response.raise_for_status()
        data = response.json()

        return [
            ExternalIssue(
                id=str(issue["id"]),
                number=issue["iid"],
                title=issue["title"],
                description=issue.get("description"),
                state=issue["state"],
                url=issue["web_url"],
                labels=issue.get("labels", []),
                external_id=project_id,
            )
            for issue in data
        ]

    async def shell_command(self, project_id: str, command: str) -> dict[str, Any]:
        """Execute GitLab CLI-like command (for gitlab-shell integration)."""
        # This is a helper for CLI shell integration
        return {
            "project_id": project_id,
            "command": command,
            "note": "Use 'gitlab' CLI tool for shell operations",
            "docs": "https://docs.gitlab.com/ee/integration/gitlab_cli.html",
        }


class JiraProvider(BaseExternalProvider):
    """Jira API integration using API tokens."""

    def __init__(self, api_key: str, base_url: str, email: str | None = None):
        # Jira requires base_url (instance) and uses email+token auth
        self.email = email or os.environ.get("JIRA_EMAIL", "")
        super().__init__(api_key, base_url.rstrip("/"))

    def _get_headers(self) -> dict[str, str]:
        import base64
        credentials = base64.b64encode(f"{self.email}:{self.api_key}".encode()).decode()
        return {
            "Authorization": f"Basic {credentials}",
            "Content-Type": "application/json",
        }

    def get_name(self) -> str:
        return "jira"

    async def validate_credentials(self) -> bool:
        if not self._client:
            raise RuntimeError("Provider not in async context")
        try:
            response = await self._client.get(f"{self.base_url}/rest/api/3/myself")
            return response.status_code == 200
        except httpx.HTTPError:
            return False

    async def list_projects(self) -> list[ExternalProject]:
        if not self._client:
            raise RuntimeError("Provider not in async context")

        response = await self._client.get(
            f"{self.base_url}/rest/api/3/project",
            params={"expand": "description"},
        )
        response.raise_for_status()
        data = response.json()

        return [
            ExternalProject(
                id=proj["id"],
                external_id=proj["key"],
                name=proj["name"],
                namespace=None,
                description=proj.get("description"),
                url=f"{self.base_url}/browse/{proj['key']}",
                provider="jira",
            )
            for proj in data
        ]

    async def create_project(
        self, name: str, description: str | None = None, namespace: str | None = None
    ) -> ExternalProject:
        if not self._client:
            raise RuntimeError("Provider not in async context")

        # Jira requires a project key (short uppercase identifier)
        import re
        key = re.sub(r'[^A-Z]', '', name.upper())[:10] or "PROJ"

        payload: dict[str, Any] = {
            "key": key,
            "name": name,
            "projectTypeKey": "software",
            "leadAccountId": "currentUser",  # Simplified - should be resolved
        }
        if description:
            payload["description"] = description

        response = await self._client.post(
            f"{self.base_url}/rest/api/3/project",
            json=payload,
        )
        response.raise_for_status()
        proj = response.json()

        return ExternalProject(
            id=proj["id"],
            external_id=proj["key"],
            name=proj["name"],
            namespace=None,
            description=description,
            url=f"{self.base_url}/browse/{proj['key']}",
            provider="jira",
        )

    async def create_issue(
        self, project_id: str, title: str, description: str | None = None, labels: list[str] | None = None
    ) -> ExternalIssue:
        if not self._client:
            raise RuntimeError("Provider not in async context")

        # Jira uses project key as project_id
        payload: dict[str, Any] = {
            "fields": {
                "project": {"key": project_id},
                "summary": title,
                "issuetype": {"name": "Task"},
            }
        }
        if description:
            payload["fields"]["description"] = {
                "type": "doc",
                "version": 1,
                "content": [
                    {
                        "type": "paragraph",
                        "content": [{"type": "text", "text": description}],
                    }
                ],
            }
        if labels:
            payload["fields"]["labels"] = labels

        response = await self._client.post(
            f"{self.base_url}/rest/api/3/issue",
            json=payload,
        )
        response.raise_for_status()
        issue = response.json()

        return ExternalIssue(
            id=issue["id"],
            number=int(issue["key"].split("-")[-1]),
            title=title,
            description=description,
            state="open",  # Jira uses statuses, not open/closed
            url=f"{self.base_url}/browse/{issue['key']}",
            labels=labels or [],
            external_id=project_id,
        )

    async def update_issue_status(self, project_id: str, issue_id: str, status: str) -> bool:
        if not self._client:
            raise RuntimeError("Provider not in async context")

        # Jira requires transition IDs - simplified version
        # In production, you'd look up available transitions first
        response = await self._client.post(
            f"{self.base_url}/rest/api/3/issue/{issue_id}/transitions",
            json={"transition": {"id": status}},  # status should be transition ID
        )
        return response.status_code == 200

    async def list_issues(self, project_id: str, state: str = "all") -> list[ExternalIssue]:
        if not self._client:
            raise RuntimeError("Provider not in async context")

        # JQL query
        jql = f"project = {project_id}"
        if state == "open":
            jql += " AND status != Done"
        elif state == "closed":
            jql += " AND status = Done"

        response = await self._client.get(
            f"{self.base_url}/rest/api/3/search",
            params={"jql": jql, "maxResults": 100},
        )
        response.raise_for_status()
        data = response.json()

        return [
            ExternalIssue(
                id=issue["id"],
                number=int(issue["key"].split("-")[-1]),
                title=issue["fields"]["summary"],
                description=None,  # Would need to extract from ADF format
                state="open" if issue["fields"]["status"]["name"] != "Done" else "closed",
                url=f"{self.base_url}/browse/{issue['key']}",
                labels=issue["fields"].get("labels", []),
                external_id=project_id,
            )
            for issue in data.get("issues", [])
        ]


# Provider factory
def get_provider(provider: str, api_key: str, base_url: str | None = None, email: str | None = None) -> BaseExternalProvider:
    """Factory function to get the right provider instance."""
    if provider == "github":
        return GitHubProvider(api_key, base_url)
    elif provider == "gitlab":
        return GitLabProvider(api_key, base_url)
    elif provider == "jira":
        if not base_url:
            raise ValueError("Jira requires base_url (your-instance.atlassian.net)")
        return JiraProvider(api_key, base_url, email)
    else:
        raise ValueError(f"Unknown provider: {provider}")
