"""
GitLab CE API Service — v4.0
Handles all communication with a self-hosted GitLab CE instance.
Superadmin-only. Every operation is auditable.
"""

import base64
import json
from typing import Optional
from urllib.parse import quote

import httpx

_client: Optional[httpx.AsyncClient] = None


def _get_client() -> httpx.AsyncClient:
    global _client
    if _client is None or _client.is_closed:
        _client = httpx.AsyncClient(
            timeout=httpx.Timeout(connect=15.0, read=60.0, write=30.0, pool=30.0),
        )
    return _client


async def close_gitlab_client():
    global _client
    if _client and not _client.is_closed:
        await _client.aclose()
        _client = None


def _headers(token: str) -> dict:
    return {"PRIVATE-TOKEN": token, "Content-Type": "application/json"}


def _api_url(gitlab_url: str, path: str) -> str:
    base = gitlab_url.rstrip("/")
    return f"{base}/api/v4{path}"


def encode_token(plain: str) -> str:
    return base64.b64encode(plain.encode()).decode()


def decode_token(encoded: str) -> str:
    return base64.b64decode(encoded.encode()).decode()


def mask_token(encoded: str) -> str:
    try:
        plain = decode_token(encoded)
        if len(plain) <= 8:
            return "****"
        return plain[:4] + "…" + plain[-4:]
    except Exception:
        return "****"


# ══════════════════════════════════════════════════════
#  Connection
# ══════════════════════════════════════════════════════

async def test_connection(gitlab_url: str, token: str) -> dict:
    client = _get_client()
    try:
        resp = await client.get(
            _api_url(gitlab_url, "/user"),
            headers=_headers(token),
        )
        if resp.status_code == 200:
            user = resp.json()
            return {
                "ok": True,
                "user": user.get("username"),
                "name": user.get("name"),
                "email": user.get("email"),
                "is_admin": user.get("is_admin", False),
                "gitlab_version": resp.headers.get("x-gitlab-meta", ""),
            }
        return {"ok": False, "error": f"HTTP {resp.status_code}: {resp.text[:200]}"}
    except Exception as e:
        return {"ok": False, "error": str(e)}


# ══════════════════════════════════════════════════════
#  Projects / Repositories
# ══════════════════════════════════════════════════════

async def list_projects(
    gitlab_url: str, token: str,
    search: str = "", page: int = 1, per_page: int = 20,
    owned: bool = True, order_by: str = "updated_at",
) -> dict:
    client = _get_client()
    params = {
        "page": page, "per_page": per_page,
        "order_by": order_by, "sort": "desc",
        "owned": str(owned).lower(),
    }
    if search:
        params["search"] = search
    resp = await client.get(
        _api_url(gitlab_url, "/projects"),
        headers=_headers(token), params=params,
    )
    resp.raise_for_status()
    total = int(resp.headers.get("x-total", "0"))
    total_pages = int(resp.headers.get("x-total-pages", "1"))
    return {
        "projects": resp.json(),
        "total": total,
        "page": page,
        "total_pages": total_pages,
    }


async def get_project(gitlab_url: str, token: str, project_id: int) -> dict:
    client = _get_client()
    resp = await client.get(
        _api_url(gitlab_url, f"/projects/{project_id}"),
        headers=_headers(token),
    )
    resp.raise_for_status()
    return resp.json()


async def create_project(
    gitlab_url: str, token: str,
    name: str, description: str = "",
    visibility: str = "private",
    namespace_id: Optional[int] = None,
    initialize_with_readme: bool = True,
) -> dict:
    client = _get_client()
    body = {
        "name": name,
        "description": description,
        "visibility": visibility,
        "initialize_with_readme": initialize_with_readme,
    }
    if namespace_id:
        body["namespace_id"] = namespace_id
    resp = await client.post(
        _api_url(gitlab_url, "/projects"),
        headers=_headers(token), json=body,
    )
    resp.raise_for_status()
    return resp.json()


async def delete_project(gitlab_url: str, token: str, project_id: int) -> bool:
    client = _get_client()
    resp = await client.delete(
        _api_url(gitlab_url, f"/projects/{project_id}"),
        headers=_headers(token),
    )
    return resp.status_code in (200, 202, 204)


async def fork_project(
    gitlab_url: str, token: str, project_id: int,
    name: Optional[str] = None, namespace_id: Optional[int] = None,
) -> dict:
    client = _get_client()
    body = {}
    if name:
        body["name"] = name
    if namespace_id:
        body["namespace_id"] = namespace_id
    resp = await client.post(
        _api_url(gitlab_url, f"/projects/{project_id}/fork"),
        headers=_headers(token), json=body,
    )
    resp.raise_for_status()
    return resp.json()


# ══════════════════════════════════════════════════════
#  Namespaces / Groups
# ══════════════════════════════════════════════════════

async def list_namespaces(gitlab_url: str, token: str) -> list:
    client = _get_client()
    resp = await client.get(
        _api_url(gitlab_url, "/namespaces"),
        headers=_headers(token), params={"per_page": 100},
    )
    resp.raise_for_status()
    return resp.json()


# ══════════════════════════════════════════════════════
#  Branches
# ══════════════════════════════════════════════════════

async def list_branches(gitlab_url: str, token: str, project_id: int) -> list:
    client = _get_client()
    resp = await client.get(
        _api_url(gitlab_url, f"/projects/{project_id}/repository/branches"),
        headers=_headers(token), params={"per_page": 100},
    )
    resp.raise_for_status()
    return resp.json()


async def create_branch(
    gitlab_url: str, token: str, project_id: int,
    branch_name: str, ref: str = "main",
) -> dict:
    client = _get_client()
    resp = await client.post(
        _api_url(gitlab_url, f"/projects/{project_id}/repository/branches"),
        headers=_headers(token),
        json={"branch": branch_name, "ref": ref},
    )
    resp.raise_for_status()
    return resp.json()


async def delete_branch(
    gitlab_url: str, token: str, project_id: int, branch_name: str,
) -> bool:
    client = _get_client()
    encoded = quote(branch_name, safe="")
    resp = await client.delete(
        _api_url(gitlab_url, f"/projects/{project_id}/repository/branches/{encoded}"),
        headers=_headers(token),
    )
    return resp.status_code in (200, 204)


# ══════════════════════════════════════════════════════
#  File Tree / Repository Browser
# ══════════════════════════════════════════════════════

async def get_tree(
    gitlab_url: str, token: str, project_id: int,
    path: str = "", ref: str = "main", recursive: bool = False,
) -> list:
    client = _get_client()
    params = {"ref": ref, "per_page": 100, "recursive": str(recursive).lower()}
    if path:
        params["path"] = path
    resp = await client.get(
        _api_url(gitlab_url, f"/projects/{project_id}/repository/tree"),
        headers=_headers(token), params=params,
    )
    if resp.status_code == 404:
        return []
    resp.raise_for_status()
    return resp.json()


async def get_file(
    gitlab_url: str, token: str, project_id: int,
    file_path: str, ref: str = "main",
) -> dict:
    client = _get_client()
    encoded_path = quote(file_path, safe="")
    resp = await client.get(
        _api_url(gitlab_url, f"/projects/{project_id}/repository/files/{encoded_path}"),
        headers=_headers(token), params={"ref": ref},
    )
    resp.raise_for_status()
    data = resp.json()
    if data.get("encoding") == "base64" and data.get("content"):
        try:
            data["decoded_content"] = base64.b64decode(data["content"]).decode("utf-8")
        except Exception:
            data["decoded_content"] = "[Binary file — cannot decode]"
    return data


async def create_or_update_file(
    gitlab_url: str, token: str, project_id: int,
    file_path: str, content: str,
    commit_message: str, branch: str = "main",
    create: bool = False,
) -> dict:
    client = _get_client()
    encoded_path = quote(file_path, safe="")
    url = _api_url(gitlab_url, f"/projects/{project_id}/repository/files/{encoded_path}")
    body = {
        "branch": branch,
        "content": content,
        "commit_message": commit_message,
    }
    if create:
        resp = await client.post(url, headers=_headers(token), json=body)
    else:
        resp = await client.put(url, headers=_headers(token), json=body)
    resp.raise_for_status()
    return resp.json()


async def delete_file(
    gitlab_url: str, token: str, project_id: int,
    file_path: str, commit_message: str, branch: str = "main",
) -> dict:
    client = _get_client()
    encoded_path = quote(file_path, safe="")
    resp = await client.delete(
        _api_url(gitlab_url, f"/projects/{project_id}/repository/files/{encoded_path}"),
        headers=_headers(token),
        json={"branch": branch, "commit_message": commit_message},
    )
    resp.raise_for_status()
    return resp.json()


# ══════════════════════════════════════════════════════
#  Batch Commits (THE SURGICAL WEAPON)
# ══════════════════════════════════════════════════════

async def batch_commit(
    gitlab_url: str, token: str, project_id: int,
    branch: str, commit_message: str,
    actions: list[dict],
) -> dict:
    client = _get_client()
    body = {
        "branch": branch,
        "commit_message": commit_message,
        "actions": actions,
    }
    resp = await client.post(
        _api_url(gitlab_url, f"/projects/{project_id}/repository/commits"),
        headers=_headers(token), json=body,
    )
    resp.raise_for_status()
    return resp.json()


async def smart_batch_commit(
    gitlab_url: str, token: str, project_id: int,
    branch: str, commit_message: str,
    files: list[dict],
) -> dict:
    """Intelligently create or update files. Checks if each file exists first."""
    actions = []
    for f in files:
        exists = False
        try:
            await get_file(gitlab_url, token, project_id, f["file_path"], ref=branch)
            exists = True
        except Exception:
            pass
        actions.append({
            "action": "update" if exists else "create",
            "file_path": f["file_path"],
            "content": f["content"],
        })
    return await batch_commit(gitlab_url, token, project_id, branch, commit_message, actions)


# ══════════════════════════════════════════════════════
#  Merge Requests
# ══════════════════════════════════════════════════════

async def list_merge_requests(
    gitlab_url: str, token: str, project_id: int,
    state: str = "opened",
) -> list:
    client = _get_client()
    resp = await client.get(
        _api_url(gitlab_url, f"/projects/{project_id}/merge_requests"),
        headers=_headers(token),
        params={"state": state, "per_page": 50, "order_by": "updated_at", "sort": "desc"},
    )
    resp.raise_for_status()
    return resp.json()


async def create_merge_request(
    gitlab_url: str, token: str, project_id: int,
    source_branch: str, target_branch: str,
    title: str, description: str = "",
) -> dict:
    client = _get_client()
    body = {
        "source_branch": source_branch,
        "target_branch": target_branch,
        "title": title,
        "description": description,
        "remove_source_branch": True,
    }
    resp = await client.post(
        _api_url(gitlab_url, f"/projects/{project_id}/merge_requests"),
        headers=_headers(token), json=body,
    )
    resp.raise_for_status()
    return resp.json()


async def get_merge_request_changes(
    gitlab_url: str, token: str, project_id: int, mr_iid: int,
) -> dict:
    client = _get_client()
    resp = await client.get(
        _api_url(gitlab_url, f"/projects/{project_id}/merge_requests/{mr_iid}/changes"),
        headers=_headers(token),
    )
    resp.raise_for_status()
    return resp.json()


# ══════════════════════════════════════════════════════
#  Pipelines
# ══════════════════════════════════════════════════════

async def list_pipelines(gitlab_url: str, token: str, project_id: int) -> list:
    client = _get_client()
    resp = await client.get(
        _api_url(gitlab_url, f"/projects/{project_id}/pipelines"),
        headers=_headers(token), params={"per_page": 30},
    )
    resp.raise_for_status()
    return resp.json()


async def get_pipeline(gitlab_url: str, token: str, project_id: int, pipeline_id: int) -> dict:
    client = _get_client()
    resp = await client.get(
        _api_url(gitlab_url, f"/projects/{project_id}/pipelines/{pipeline_id}"),
        headers=_headers(token),
    )
    resp.raise_for_status()
    return resp.json()


async def get_pipeline_jobs(gitlab_url: str, token: str, project_id: int, pipeline_id: int) -> list:
    client = _get_client()
    resp = await client.get(
        _api_url(gitlab_url, f"/projects/{project_id}/pipelines/{pipeline_id}/jobs"),
        headers=_headers(token), params={"per_page": 100},
    )
    resp.raise_for_status()
    return resp.json()


async def trigger_pipeline(gitlab_url: str, token: str, project_id: int, ref: str = "main") -> dict:
    client = _get_client()
    resp = await client.post(
        _api_url(gitlab_url, f"/projects/{project_id}/pipeline"),
        headers=_headers(token), json={"ref": ref},
    )
    resp.raise_for_status()
    return resp.json()


async def retry_pipeline(gitlab_url: str, token: str, project_id: int, pipeline_id: int) -> dict:
    client = _get_client()
    resp = await client.post(
        _api_url(gitlab_url, f"/projects/{project_id}/pipelines/{pipeline_id}/retry"),
        headers=_headers(token),
    )
    resp.raise_for_status()
    return resp.json()


# ══════════════════════════════════════════════════════
#  Compare / Diff
# ══════════════════════════════════════════════════════

async def compare(
    gitlab_url: str, token: str, project_id: int,
    from_ref: str, to_ref: str,
) -> dict:
    client = _get_client()
    resp = await client.get(
        _api_url(gitlab_url, f"/projects/{project_id}/repository/compare"),
        headers=_headers(token),
        params={"from": from_ref, "to": to_ref},
    )
    resp.raise_for_status()
    return resp.json()


# ══════════════════════════════════════════════════════
#  Project Context for AI (NEW in v4.0)
# ══════════════════════════════════════════════════════

async def get_project_context(
    gitlab_url: str, token: str, project_id: int, branch: str = "main",
) -> str:
    """Build a formatted project context string for the AI system prompt."""
    try:
        project = await get_project(gitlab_url, token, project_id)
    except Exception:
        return ""

    tree = []
    try:
        tree = await get_tree(gitlab_url, token, project_id, ref=branch, recursive=True)
    except Exception:
        pass

    lines = []
    lines.append(f"**Project:** {project.get('name_with_namespace', project.get('name', '?'))}")
    lines.append(f"**Description:** {project.get('description') or 'No description'}")
    lines.append(f"**Default Branch:** {project.get('default_branch', 'main')}")
    lines.append(f"**Active Branch:** {branch}")
    lines.append(f"**Web URL:** {project.get('web_url', '')}")

    langs = project.get("languages") or {}
    if not langs:
        try:
            client = _get_client()
            resp = await client.get(
                _api_url(gitlab_url, f"/projects/{project_id}/languages"),
                headers=_headers(token),
            )
            if resp.status_code == 200:
                langs = resp.json()
        except Exception:
            pass
    if langs:
        lang_str = ", ".join(f"{k} ({v:.1f}%)" for k, v in sorted(langs.items(), key=lambda x: -x[1])[:8])
        lines.append(f"**Languages:** {lang_str}")

    if tree:
        lines.append(f"\n**File Tree ({len(tree)} items):**")
        lines.append("```")
        dirs = sorted([t for t in tree if t.get("type") == "tree"], key=lambda x: x["path"])
        files = sorted([t for t in tree if t.get("type") == "blob"], key=lambda x: x["path"])
        for d in dirs:
            lines.append(f"  📁 {d['path']}/")
        for f in files:
            lines.append(f"  📄 {f['path']}")
        lines.append("```")

    # Try to fetch README for extra context
    for readme_name in ["README.md", "README.rst", "README.txt", "README"]:
        try:
            readme_data = await get_file(gitlab_url, token, project_id, readme_name, ref=branch)
            content = readme_data.get("decoded_content", "")
            if content and len(content) < 8000:
                lines.append(f"\n**README ({readme_name}):**")
                lines.append(content[:6000])
            break
        except Exception:
            continue

    return "\n".join(lines)