from __future__ import annotations

import gitlab
import gitlab.exceptions

from ..core.models import MR, Job, Pipeline, Vulnerability


class AuthenticationError(Exception):
    """Raised when the GitLab API returns 401 Unauthorized."""

    pass


class GitLabClient:
    def __init__(self, instance: str, token: str):
        url = f"https://{instance}"
        self._gl = gitlab.Gitlab(url, private_token=token)
        self._gq = gitlab.GraphQL(url, token=token)
        self._instance = instance
        self._username: str | None = None

    def get_username(self) -> str | None:
        """Get the authenticated user's username. Cached after first call."""
        if self._username is None:
            try:
                self._gl.auth()
                self._username = self._gl.user.username
            except Exception:
                pass
        return self._username

    def _project(self, project_path: str):
        return self._gl.projects.get(project_path)

    def list_merge_requests(self, project_path: str, state: str = "opened") -> list[MR]:
        import logging

        logger = logging.getLogger(__name__)

        try:
            project = self._project(project_path)
            raw = project.mergerequests.list(state=state, get_all=True)
            mrs = [
                MR(
                    id=mr.id,
                    iid=mr.iid,
                    title=mr.title,
                    author=mr.author["username"],
                    state=mr.state,
                    source_branch=mr.source_branch,
                    target_branch=mr.target_branch,
                    web_url=mr.web_url,
                    user_notes_count=mr.user_notes_count,
                    approved=mr.approved if hasattr(mr, "approved") else False,
                    created_at=mr.created_at,
                    updated_at=mr.updated_at,
                    repo_name="",
                )
                for mr in raw
            ]
            logger.debug(f"list_merge_requests({project_path}): {len(mrs)} MRs")
            return mrs
        except gitlab.exceptions.GitlabAuthenticationError:
            raise AuthenticationError("GitLab token expired or invalid")
        except Exception as e:
            logger.error(f"list_merge_requests({project_path}) failed: {e}")
            return []

    def list_pipelines(
        self, project_path: str, ref: str | None = None
    ) -> list[Pipeline]:
        import logging

        logger = logging.getLogger(__name__)

        try:
            project = self._project(project_path)
            kwargs: dict = {"get_all": False, "per_page": 20}
            if ref is not None:
                kwargs["ref"] = ref
            raw = project.pipelines.list(**kwargs)
            pipelines = [
                Pipeline(
                    id=pip.id,
                    status=pip.status,
                    ref=pip.ref,
                    web_url=pip.web_url,
                    created_at=pip.created_at,
                    repo_name="",
                )
                for pip in raw
            ]
            logger.debug(f"list_pipelines({project_path}): {len(pipelines)} pipelines")
            return pipelines
        except gitlab.exceptions.GitlabAuthenticationError:
            raise AuthenticationError("GitLab token expired or invalid")
        except Exception as e:
            logger.error(f"list_pipelines({project_path}) failed: {e}")
            return []

    def get_pipeline_jobs(self, project_path: str, pipeline_id: int) -> list[Job]:
        try:
            project = self._project(project_path)
            pipeline = project.pipelines.get(pipeline_id)
            raw = pipeline.jobs.list(get_all=True)
            return [
                Job(
                    id=job.id,
                    name=job.name,
                    status=job.status,
                    stage=job.stage,
                    web_url=job.web_url,
                )
                for job in raw
            ]
        except gitlab.exceptions.GitlabAuthenticationError:
            raise AuthenticationError("GitLab token expired or invalid")
        except Exception:
            return []

    def get_job_log(self, project_path: str, job_id: int) -> str:
        try:
            project = self._project(project_path)
            return (
                project.jobs.get(job_id, lazy=True)
                .trace()
                .decode("utf-8", errors="replace")
            )
        except gitlab.exceptions.GitlabAuthenticationError:
            raise AuthenticationError("GitLab token expired or invalid")
        except Exception:
            return ""

    _VULN_QUERY = """
    query ProjectVulnerabilities(
      $fullPath: ID!
      $severity: [VulnerabilitySeverity!]
    ) {
      project(fullPath: $fullPath) {
        vulnerabilities(
          first: 100
          severity: $severity
          sort: severity_desc
        ) {
          nodes {
            id
            title
            severity
            state
            reportType
            detectedAt
            webUrl
            primaryIdentifier {
              name
              externalType
              externalId
            }
            scanner {
              name
            }
            dismissalReason
            stateComment
            falsePositive
            solution
            location {
              ... on VulnerabilityLocationSast {
                file
                startLine
              }
              ... on VulnerabilityLocationDependencyScanning {
                file
                dependency { package { name } version }
              }
              ... on VulnerabilityLocationContainerScanning {
                image
                dependency { package { name } version }
              }
              ... on VulnerabilityLocationSecretDetection {
                file
                startLine
              }
            }
          }
        }
      }
    }
    """

    _VALID_SEVERITIES = {"LOW", "MEDIUM", "HIGH", "CRITICAL", "INFO", "UNKNOWN"}

    @staticmethod
    def _parse_vuln_location(loc: dict) -> str | None:
        if loc.get("file"):
            line = loc.get("startLine")
            return f"{loc['file']}:{line}" if line else loc["file"]
        if loc.get("image"):
            dep = loc.get("dependency") or {}
            pkg = (dep.get("package") or {}).get("name", "")
            ver = dep.get("version", "")
            return f"{loc['image']} ({pkg} {ver})".strip()
        return None

    @staticmethod
    def _parse_vuln_node(v: dict) -> Vulnerability:
        gid = v.get("id", "")
        identifier = v.get("primaryIdentifier") or {}
        return Vulnerability(
            id=int(gid.split("/")[-1]) if "/" in gid else 0,
            name=identifier.get("name") or v.get("title", ""),
            severity=(v.get("severity") or "").lower(),
            state=(v.get("state") or "").lower(),
            source=(v.get("reportType") or "").lower(),
            web_url=v.get("webUrl", ""),
            repo_name="",
            mitigation_state=(v.get("dismissalReason") or "").lower() or None,
            mitigation_details=v.get("stateComment") or None,
            location=GitLabClient._parse_vuln_location(v.get("location") or {}),
            solution=v.get("solution") or None,
        )

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

        logger = logging.getLogger(__name__)

        try:
            severity_filter = None
            if severity:
                upper = severity.upper()
                if upper in self._VALID_SEVERITIES:
                    severity_filter = [upper]

            variables: dict = {"fullPath": project_path}
            if severity_filter:
                variables["severity"] = severity_filter

            response = self._gq.execute(self._VULN_QUERY, variable_values=variables)

            project_data = (response or {}).get("project")
            if not project_data:
                return []
            vuln_nodes = (
                project_data.get("vulnerabilities", {}).get("nodes") or []
            )

            results = [self._parse_vuln_node(v) for v in vuln_nodes]
            logger.debug(f"{project_path}: {len(results)} vulns")
            return results
        except gitlab.exceptions.GitlabAuthenticationError:
            raise AuthenticationError("GitLab token expired or invalid")
        except Exception as e:
            logger.error(
                f"get_vulnerabilities({project_path}) failed: {e}", exc_info=True
            )
            return []

    def get_project_info(self, project_path: str) -> dict:
        try:
            project = self._project(project_path)
            return {
                "name": project.name,
                "web_url": project.web_url,
                "default_branch": project.default_branch,
            }
        except gitlab.exceptions.GitlabAuthenticationError:
            raise AuthenticationError("GitLab token expired or invalid")
        except Exception:
            return {}
