"""Bitbucket Cloud provider using the atlassian-python-api library."""

from __future__ import annotations

import logging

from atlassian.bitbucket.cloud import Cloud
from requests import HTTPError

from ..core.models import MR, Issue, Job, Pipeline, Vulnerability
from .provider import AuthenticationError, ForgeProvider

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# State mappings
# ---------------------------------------------------------------------------

_PR_STATE_MAP = {
    "OPEN": "opened",
    "MERGED": "merged",
    "DECLINED": "closed",
    "SUPERSEDED": "closed",
}

_PIPELINE_STATE_MAP = {
    "PENDING": "pending",
    "RUNNING": "running",
    "STOPPED": "canceled",
}

_ISSUE_STATE_MAP = {
    "new": "opened",
    "on hold": "opened",
    "resolved": "closed",
    "wontfix": "closed",
    "duplicate": "closed",
    "invalid": "closed",
}


def _map_pr_state(state: str) -> str:
    return _PR_STATE_MAP.get(state, "closed")


def _map_pipeline_status(state_name: str, result_name: str | None) -> str:
    """Map Bitbucket pipeline (state, result) pair to a unified status string."""
    if state_name == "COMPLETED":
        result_map = {
            "SUCCESSFUL": "success",
            "FAILED": "failed",
            "ERROR": "failed",
        }
        return result_map.get(result_name or "", "failed")
    return _PIPELINE_STATE_MAP.get(state_name, "pending")


def _map_issue_state(state: str) -> str:
    return _ISSUE_STATE_MAP.get(state, "opened")


def _is_auth_error(exc: HTTPError) -> bool:
    """Return True when the HTTP error is a 401 Unauthorized."""
    return (
        exc.response is not None
        and exc.response.status_code == 401
    )


# ---------------------------------------------------------------------------
# Provider
# ---------------------------------------------------------------------------

class BitbucketProvider(ForgeProvider):
    """Synchronous Bitbucket Cloud client wrapping atlassian-python-api."""

    supported_features = frozenset({"mrs", "issues", "pipelines"})

    def __init__(self, instance: str, token: str) -> None:
        super().__init__(instance, token)
        self._bb = Cloud(token=token)
        self._username: str | None = None
        self._auth_failed: bool = False

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _split_path(project_path: str) -> tuple[str, str]:
        """Split 'workspace/repo-slug' into (workspace, slug)."""
        parts = project_path.split("/", 1)
        if len(parts) != 2:
            raise ValueError(f"Invalid Bitbucket project path: {project_path!r}")
        return parts[0], parts[1]

    def _get_repo(self, project_path: str):
        ws, slug = self._split_path(project_path)
        return self._bb.repositories.get(workspace=ws, repo_slug=slug)

    def _raise_if_auth(self, exc: HTTPError) -> None:
        if _is_auth_error(exc):
            raise AuthenticationError("Bitbucket token expired or invalid") from exc

    # ------------------------------------------------------------------
    # Username
    # ------------------------------------------------------------------

    def get_username(self) -> str | None:
        if self._username is None and not self._auth_failed:
            try:
                data = self._bb.get("user", absolute=True)
                self._username = data.get("username") or data.get("account_id")
            except HTTPError as exc:
                if _is_auth_error(exc):
                    self._auth_failed = True
                else:
                    logger.error("get_username failed: %s", exc)
            except Exception as exc:  # pylint: disable=broad-exception-caught
                logger.error("get_username unexpected error: %s", exc)
        return self._username

    # ------------------------------------------------------------------
    # Merge requests (pull requests)
    # ------------------------------------------------------------------

    def _parse_pr(self, pr) -> MR:
        data = pr.data
        participants = data.get("participants", [])
        reviewers = [
            p["user"]["nickname"]
            for p in participants
            if p.get("role") == "REVIEWER"
        ]
        approved = any(
            p.get("approved") and p.get("role") == "REVIEWER"
            for p in participants
        )
        web_url = data.get("links", {}).get("html", {}).get("href", "")

        return MR(
            id=pr.id,
            iid=pr.id,
            title=pr.title,
            author=pr.author.nickname,
            state=_map_pr_state(pr.state),
            source_branch=pr.source_branch,
            target_branch=pr.destination_branch,
            web_url=web_url,
            user_notes_count=pr.comment_count,
            approved=approved,
            created_at=pr.created_on,
            updated_at=pr.updated_on,
            repo_name="",
            sha=pr.source_commit,
            assignees=None,
            reviewers=reviewers or None,
        )

    def list_merge_requests(self, project_path: str, state: str = "opened") -> list[MR]:
        # Map unified state → Bitbucket PR state(s)
        bb_state_filter: str | None
        if state == "opened":
            bb_state_filter = "OPEN"
        elif state == "merged":
            bb_state_filter = "MERGED"
        elif state in ("closed",):
            bb_state_filter = None  # DECLINED + SUPERSEDED — need post-filter
        else:
            bb_state_filter = None

        try:
            repo = self._get_repo(project_path)
            q = f'state="{bb_state_filter}"' if bb_state_filter else None
            kwargs = {"q": q} if q else {}
            mrs = []
            for pr in repo.pullrequests.each(**kwargs):
                mr = self._parse_pr(pr)
                # When asking for "closed" we only got results via no-filter, so post-filter
                if state == "closed" and mr.state not in ("closed",):
                    continue
                mrs.append(mr)
            logger.debug("list_merge_requests(%s): %d MRs", project_path, len(mrs))
            return mrs
        except HTTPError as exc:
            self._raise_if_auth(exc)
            logger.error("list_merge_requests(%s) failed: %s", project_path, exc)
            return []

    def get_mrs_by_iids(self, project_path: str, iids: list[int]) -> list[MR]:
        if not iids:
            return []
        try:
            repo = self._get_repo(project_path)
            mrs = []
            for iid in iids:
                try:
                    pr = repo.pullrequests.get(iid)
                    mrs.append(self._parse_pr(pr))
                except HTTPError as inner_exc:
                    if _is_auth_error(inner_exc):
                        raise
                    logger.debug(
                        "get_pr(%s, %d) failed (HTTP %s) — skipping",
                        project_path, iid,
                        inner_exc.response.status_code if inner_exc.response else "?",
                    )
            return mrs
        except HTTPError as exc:
            self._raise_if_auth(exc)
            logger.error("get_mrs_by_iids(%s) failed: %s", project_path, exc)
            return []

    # ------------------------------------------------------------------
    # Issues
    # ------------------------------------------------------------------

    def _parse_issue(self, issue) -> Issue:
        data = issue.data
        web_url = data.get("links", {}).get("html", {}).get("href", "")
        author = (
            data.get("reporter", {}).get("nickname")
            or data.get("reporter", {}).get("display_name", "")
        )
        return Issue(
            id=int(issue.id),
            iid=int(issue.id),
            title=issue.title,
            author=author,
            state=_map_issue_state(issue.state),
            web_url=web_url,
            user_notes_count=0,
            created_at=data.get("created_on", ""),
            updated_at=data.get("updated_on", ""),
            repo_name="",
            labels=None,
            assignees=None,
            participants=None,
            confidential=False,
        )

    def list_issues(self, project_path: str, state: str = "opened") -> list[Issue]:
        try:
            repo = self._get_repo(project_path)
            # Build a state filter query for Bitbucket's issue tracker
            if state == "opened":
                q = 'state="new" OR state="on hold"'
            elif state == "closed":
                q = 'state="resolved" OR state="wontfix" OR state="duplicate" OR state="invalid"'
            else:
                q = None

            kwargs = {"q": q} if q else {}
            issues = [self._parse_issue(iss) for iss in repo.issues.each(**kwargs)]
            logger.debug("list_issues(%s): %d issues", project_path, len(issues))
            return issues
        except HTTPError as exc:
            if exc.response is not None and exc.response.status_code == 404:
                # Issue tracker disabled for this repository
                return []
            self._raise_if_auth(exc)
            logger.error("list_issues(%s) failed: %s", project_path, exc)
            return []

    def get_issues_by_iids(self, project_path: str, iids: list[int]) -> list[Issue]:
        if not iids:
            return []
        try:
            repo = self._get_repo(project_path)
            issues = []
            for iid in iids:
                try:
                    iss = repo.issues.get(iid)
                    issues.append(self._parse_issue(iss))
                except HTTPError as inner_exc:
                    if _is_auth_error(inner_exc):
                        raise
                    logger.debug(
                        "get_issue(%s, %d) failed (HTTP %s) — skipping",
                        project_path, iid,
                        inner_exc.response.status_code if inner_exc.response else "?",
                    )
            return issues
        except HTTPError as exc:
            self._raise_if_auth(exc)
            logger.error("get_issues_by_iids(%s) failed: %s", project_path, exc)
            return []

    # ------------------------------------------------------------------
    # Pipelines
    # ------------------------------------------------------------------

    def _parse_pipeline(self, pipeline) -> Pipeline:
        data = pipeline.data
        state_data = data.get("state", {})
        state_name = state_data.get("name", "PENDING")
        result_name = state_data.get("result", {}).get("name")
        status = _map_pipeline_status(state_name, result_name)
        ref = data.get("target", {}).get("ref_name", "")
        web_url = data.get("links", {}).get("html", {}).get("href", "")
        return Pipeline(
            id=pipeline.uuid,
            status=status,
            ref=ref,
            web_url=web_url,
            created_at=pipeline.created_on,
            repo_name="",
        )

    def list_pipelines(
        self, project_path: str, ref: str | None = None, per_page: int = 20
    ) -> list[Pipeline]:
        try:
            repo = self._get_repo(project_path)
            kwargs: dict = {"sort": "-created_on"}
            pipelines = []
            for pipeline in repo.pipelines.each(**kwargs):
                parsed = self._parse_pipeline(pipeline)
                if ref is not None and parsed.ref != ref:
                    continue
                pipelines.append(parsed)
                if len(pipelines) >= per_page:
                    break
            logger.debug("list_pipelines(%s): %d pipelines", project_path, len(pipelines))
            return pipelines
        except HTTPError as exc:
            self._raise_if_auth(exc)
            logger.error("list_pipelines(%s) failed: %s", project_path, exc)
            return []

    # ------------------------------------------------------------------
    # Pipeline jobs (steps)
    # ------------------------------------------------------------------

    def _parse_step(self, step) -> Job:
        data = step.data
        state_data = data.get("state", {})
        state_name = state_data.get("name", "PENDING")
        result_name = state_data.get("result", {}).get("name")
        status = _map_pipeline_status(state_name, result_name)
        web_url = data.get("links", {}).get("html", {}).get("href", "")
        return Job(
            id=step.uuid,
            name=data.get("name", ""),
            status=status,
            stage="",
            web_url=web_url,
        )

    def get_pipeline_jobs(self, project_path: str, pipeline_id: int | str) -> list[Job]:
        try:
            repo = self._get_repo(project_path)
            pipeline = repo.pipelines.get(uuid=str(pipeline_id))
            return [self._parse_step(step) for step in pipeline.steps()]
        except HTTPError as exc:
            self._raise_if_auth(exc)
            logger.error("get_pipeline_jobs(%s, %s) failed: %s", project_path, pipeline_id, exc)
            return []

    # ------------------------------------------------------------------
    # Job log (step log)
    # ------------------------------------------------------------------

    def get_job_log(self, project_path: str, job_id: int | str) -> str:
        # job_id is expected as "pipeline_uuid/step_uuid" for Bitbucket
        # The caller may pass just a step UUID — we do a best-effort lookup.
        try:
            repo = self._get_repo(project_path)
            # job_id format: "<pipeline_uuid>/<step_uuid>" or just "<step_uuid>"
            parts = str(job_id).split("/", 1)
            if len(parts) == 2:
                pipeline_uuid, step_uuid = parts
            else:
                logger.warning("get_job_log: cannot resolve step without pipeline UUID")
                return ""
            pipeline = repo.pipelines.get(uuid=pipeline_uuid)
            step = pipeline.step(uuid=step_uuid)
            log_bytes = step.log()
            if log_bytes is None:
                return ""
            return log_bytes.decode("utf-8", errors="replace")
        except HTTPError as exc:
            self._raise_if_auth(exc)
            logger.error("get_job_log(%s, %s) failed: %s", project_path, job_id, exc)
            return ""
        except Exception as exc:  # pylint: disable=broad-exception-caught
            logger.error("get_job_log(%s, %s) unexpected error: %s", project_path, job_id, exc)
            return ""

    # ------------------------------------------------------------------
    # Vulnerabilities — not supported on Bitbucket Cloud
    # ------------------------------------------------------------------

    def get_vulnerabilities(
        self, project_path: str, severity: str | None = None
    ) -> list[Vulnerability]:
        return []

    def get_pipeline_findings(
        self,
        project_path: str,
        pipeline_id: int | str | None,
        ref: str | None = None,
    ) -> set[str]:
        return set()

    # ------------------------------------------------------------------
    # Project info
    # ------------------------------------------------------------------

    def get_default_branch(self, project_path: str) -> str | None:
        try:
            repo = self._get_repo(project_path)
            mb = repo.get_data("mainbranch")
            if mb:
                return mb.get("name")
            return None
        except HTTPError as exc:
            self._raise_if_auth(exc)
            logger.error("get_default_branch(%s) failed: %s", project_path, exc)
            return None
        except Exception:  # pylint: disable=broad-exception-caught
            return None

    def get_project_info(self, project_path: str) -> dict:
        try:
            repo = self._get_repo(project_path)
            mb = repo.get_data("mainbranch")
            default_branch = mb.get("name") if mb else None
            web_url = repo.data.get("links", {}).get("html", {}).get("href", "")
            return {
                "name": repo.name,
                "description": repo.description or "",
                "web_url": web_url,
                "default_branch": default_branch,
            }
        except HTTPError as exc:
            self._raise_if_auth(exc)
            logger.error("get_project_info(%s) failed: %s", project_path, exc)
            return {}
