"""
GitLab CE REST API v4 client — Enhanced for massive codebases.
Smart file selection for repos up to 10M+ tokens.
"""

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

import httpx


class GitLabError(Exception):
    def __init__(self, status_code: int, detail: str):
        self.status_code = status_code
        self.detail = detail
        super().__init__(f"GitLab {status_code}: {detail}")


def _timeout():
    return httpx.Timeout(connect=15.0, read=120.0, write=30.0, pool=30.0)


async def _request(method: str, url: str, token: str, **kwargs) -> dict | list:
    async with httpx.AsyncClient(timeout=_timeout()) as client:
        headers = {"Private-Token": token}
        resp = await client.request(method, url, headers=headers, **kwargs)
        if resp.status_code >= 400:
            detail = resp.text[:500]
            raise GitLabError(resp.status_code, detail)
        if resp.status_code == 204:
            return {}
        return resp.json()


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


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

async def test_connection(gitlab_url: str, token: str) -> dict:
    url = _api(gitlab_url, "/user")
    data = await _request("GET", url, token)
    return {"ok": True, "username": data.get("username", ""), "name": data.get("name", "")}


# ═══════════════════════════════════════════════════
#  Projects
# ═══════════════════════════════════════════════════

async def list_projects(
    gitlab_url: str, token: str,
    search: Optional[str] = None,
    page: int = 1, per_page: int = 50,
    owned: bool = False,
) -> list[dict]:
    params = {"page": page, "per_page": per_page, "order_by": "updated_at"}
    if search:
        params["search"] = search
    if owned:
        params["owned"] = "true"
    url = _api(gitlab_url, "/projects")
    data = await _request("GET", url, token, params=params)
    return [
        {
            "id": p["id"],
            "name": p["name"],
            "path_with_namespace": p["path_with_namespace"],
            "default_branch": p.get("default_branch", "main"),
            "web_url": p.get("web_url", ""),
            "description": p.get("description") or "",
            "last_activity_at": p.get("last_activity_at", ""),
        }
        for p in data
    ]


async def create_project(
    gitlab_url: str, token: str,
    name: str, description: str = "",
    visibility: str = "private",
    initialize_with_readme: bool = True,
) -> dict:
    url = _api(gitlab_url, "/projects")
    body = {
        "name": name, "description": description,
        "visibility": visibility, "initialize_with_readme": initialize_with_readme,
    }
    data = await _request("POST", url, token, json=body)
    return {
        "id": data["id"], "name": data["name"],
        "path_with_namespace": data["path_with_namespace"],
        "default_branch": data.get("default_branch", "main"),
        "web_url": data.get("web_url", ""), "description": data.get("description") or "",
    }


async def get_project(gitlab_url: str, token: str, project_id: int) -> dict:
    url = _api(gitlab_url, f"/projects/{project_id}")
    data = await _request("GET", url, token)
    return {
        "id": data["id"], "name": data["name"],
        "path_with_namespace": data["path_with_namespace"],
        "default_branch": data.get("default_branch", "main"),
        "web_url": data.get("web_url", ""), "description": data.get("description") or "",
        "last_activity_at": data.get("last_activity_at", ""),
        "forks_count": data.get("forks_count", 0), "star_count": data.get("star_count", 0),
    }


# ═══════════════════════════════════════════════════
#  Repository Tree — loads ALL paths
# ═══════════════════════════════════════════════════

async def get_tree(
    gitlab_url: str, token: str,
    project_id: int, path: str = "",
    ref: str = "main", recursive: bool = True,
) -> list[dict]:
    all_items = []
    page = 1
    while True:
        params = {
            "ref": ref, "per_page": 100, "page": page,
            "recursive": str(recursive).lower(),
        }
        if path:
            params["path"] = path
        url = _api(gitlab_url, f"/projects/{project_id}/repository/tree")
        try:
            data = await _request("GET", url, token, params=params)
        except GitLabError:
            break
        if not data:
            break
        all_items.extend(data)
        if len(data) < 100:
            break
        page += 1
        if page > 500:  # Up to 50K files
            break
    return [
        {"name": i["name"], "path": i["path"], "type": i["type"], "mode": i.get("mode", "")}
        for i in all_items
    ]


def format_tree_for_prompt(items: list[dict], repo_name: str, branch: str) -> str:
    if not items:
        return f"[Repository: {repo_name} ({branch}) — empty or inaccessible]"

    files = []
    dirs = set()
    for item in sorted(items, key=lambda x: x["path"]):
        if item["type"] == "tree":
            dirs.add(item["path"])
        else:
            files.append(item["path"])

    lines = [
        f"Repository: {repo_name} (branch: {branch})",
        f"Total files: {len(files)}",
        f"Total directories: {len(dirs)}",
        "",
    ]
    for f in files:
        lines.append(f"  {f}")
    return "\n".join(lines)


# ═══════════════════════════════════════════════════
#  File Operations
# ═══════════════════════════════════════════════════

async def get_file_content(
    gitlab_url: str, token: str,
    project_id: int, file_path: str,
    ref: str = "main",
) -> dict:
    encoded_path = quote(file_path, safe="")
    url = _api(gitlab_url, f"/projects/{project_id}/repository/files/{encoded_path}")
    params = {"ref": ref}
    data = await _request("GET", url, token, params=params)
    content_raw = data.get("content", "")
    encoding = data.get("encoding", "base64")
    if encoding == "base64" and content_raw:
        try:
            content = base64.b64decode(content_raw).decode("utf-8", errors="replace")
        except Exception:
            content = "[Binary file — cannot decode]"
    else:
        content = content_raw
    return {
        "file_path": data.get("file_path", file_path),
        "file_name": data.get("file_name", ""),
        "size": data.get("size", 0),
        "content": content,
        "ref": data.get("ref", ref),
        "last_commit_id": data.get("last_commit_id", ""),
    }


async def get_file_raw(
    gitlab_url: str, token: str,
    project_id: int, file_path: str,
    ref: str = "main",
) -> str:
    encoded_path = quote(file_path, safe="")
    url = _api(gitlab_url, f"/projects/{project_id}/repository/files/{encoded_path}/raw")
    params = {"ref": ref}
    async with httpx.AsyncClient(timeout=_timeout()) as client:
        resp = await client.get(url, headers={"Private-Token": token}, params=params)
        if resp.status_code >= 400:
            raise GitLabError(resp.status_code, resp.text[:300])
        return resp.text


# ═══════════════════════════════════════════════════
#  Commits
# ═══════════════════════════════════════════════════

async def commit_files(
    gitlab_url: str, token: str,
    project_id: int, branch: str,
    commit_message: str, actions: list[dict],
) -> dict:
    url = _api(gitlab_url, f"/projects/{project_id}/repository/commits")
    body = {"branch": branch, "commit_message": commit_message, "actions": actions}
    data = await _request("POST", url, token, json=body)
    return {
        "id": data.get("id", ""), "short_id": data.get("short_id", ""),
        "message": data.get("message", ""), "web_url": data.get("web_url", ""),
    }


async def commit_single_file(
    gitlab_url: str, token: str,
    project_id: int, branch: str,
    file_path: str, content: str,
    commit_message: str, action: str = "update",
) -> dict:
    actions = [{"action": action, "file_path": file_path, "content": content}]
    return await commit_files(gitlab_url, token, project_id, branch, commit_message, actions)


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

async def list_branches(gitlab_url: str, token: str, project_id: int) -> list[dict]:
    url = _api(gitlab_url, f"/projects/{project_id}/repository/branches")
    params = {"per_page": 100}
    data = await _request("GET", url, token, params=params)
    return [
        {
            "name": b["name"], "default": b.get("default", False),
            "web_url": b.get("web_url", ""),
            "commit_short_id": b.get("commit", {}).get("short_id", ""),
            "commit_message": (b.get("commit", {}).get("message") or "")[:100],
        }
        for b in data
    ]


async def create_branch(
    gitlab_url: str, token: str,
    project_id: int, branch_name: str, ref: str = "main",
) -> dict:
    url = _api(gitlab_url, f"/projects/{project_id}/repository/branches")
    body = {"branch": branch_name, "ref": ref}
    data = await _request("POST", url, token, json=body)
    return {"name": data["name"], "web_url": data.get("web_url", "")}


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

async def create_merge_request(
    gitlab_url: str, token: str,
    project_id: int, source_branch: str, target_branch: str,
    title: str, description: str = "",
) -> dict:
    url = _api(gitlab_url, f"/projects/{project_id}/merge_requests")
    body = {
        "source_branch": source_branch, "target_branch": target_branch,
        "title": title, "description": description,
    }
    data = await _request("POST", url, token, json=body)
    return {
        "iid": data.get("iid"), "title": data.get("title", ""),
        "web_url": data.get("web_url", ""), "state": data.get("state", ""),
    }


# ═══════════════════════════════════════════════════
#  SMART FILE SELECTION — the core of large repo support
# ═══════════════════════════════════════════════════

TEXT_EXTENSIONS = {
    ".py", ".js", ".ts", ".jsx", ".tsx", ".cs", ".java", ".cpp", ".c",
    ".h", ".hpp", ".go", ".rs", ".rb", ".php", ".swift", ".kt", ".lua",
    ".gd", ".html", ".css", ".scss", ".json", ".yaml", ".yml", ".xml",
    ".toml", ".ini", ".cfg", ".sh", ".bash", ".sql", ".md", ".txt",
    ".env", ".dockerfile", ".vue", ".svelte", ".dart", ".r", ".csv",
    ".graphql", ".proto", ".tf", ".hcl", ".gradle", ".cmake",
    ".makefile", ".rake", ".gemspec", ".lock", ".mod", ".sum",
    ".csproj", ".sln", ".props", ".targets", ".fsproj",
}

# Files that are ALWAYS loaded if they exist (project understanding)
PRIORITY_PATTERNS = [
    # Package manifests & configs
    r"^package\.json$", r"^package-lock\.json$", r"^yarn\.lock$",
    r"^requirements\.txt$", r"^setup\.py$", r"^setup\.cfg$", r"^pyproject\.toml$",
    r"^Pipfile$", r"^Cargo\.toml$", r"^go\.mod$", r"^go\.sum$",
    r"^Gemfile$", r"^pom\.xml$", r"^build\.gradle$",
    r"^\.csproj$", r".*\.sln$",
    # Docker & CI
    r"^Dockerfile", r"^docker-compose", r"^\.dockerignore$",
    r"^\.gitlab-ci\.yml$", r"^\.github/", r"^Jenkinsfile$",
    # Config files
    r"^tsconfig.*\.json$", r"^vite\.config", r"^webpack\.config",
    r"^tailwind\.config", r"^postcss\.config", r"^babel\.config",
    r"^\.eslintrc", r"^\.prettierrc", r"^\.editorconfig$",
    r"^next\.config", r"^nuxt\.config", r"^angular\.json$",
    r"^\.env\.example$", r"^\.env\.sample$",
    # Docs
    r"^README", r"^CHANGELOG", r"^CONTRIBUTING",
    r"^docs/.*\.md$",
    # Entry points
    r"^main\.", r"^index\.", r"^app\.", r"^server\.",
    r"^Program\.", r"^Startup\.",
    r"^src/main\.", r"^src/index\.", r"^src/app\.", r"^src/App\.",
    r"^backend/main\.", r"^frontend/src/main\.", r"^frontend/src/App\.",
    r"^cmd/", r"^internal/",
    # Database
    r".*migrations/.*", r".*models\.", r".*schema\.",
]

# Maximum characters for file contents in the prompt
MAX_SMART_CHARS = 600_000       # ~150K tokens
MAX_PRIORITY_CHARS = 150_000    # Reserve for priority files
MAX_QUERY_CHARS = 450_000       # For query-relevant files
MAX_FILES_TO_LOAD = 300         # Hard cap on number of files
MAX_SINGLE_FILE = 50_000        # Skip files larger than this


def _is_text_file(path: str) -> bool:
    """Check if a file path looks like a text file we can load."""
    lower = path.lower()
    name = lower.rsplit("/", 1)[-1] if "/" in lower else lower

    # Check known text filenames
    if name in {
        "dockerfile", "makefile", "gemfile", "rakefile", "procfile",
        "vagrantfile", "jenkinsfile", "brewfile", ".gitignore",
        ".dockerignore", ".env.example", ".env.sample",
    }:
        return True

    # Check extension
    if "." in name:
        ext = "." + name.rsplit(".", 1)[-1]
        return ext in TEXT_EXTENSIONS

    return False


def _is_priority_file(path: str) -> bool:
    """Check if file matches a priority pattern (should always be loaded)."""
    for pattern in PRIORITY_PATTERNS:
        if re.search(pattern, path, re.IGNORECASE):
            return True
    return False


def _score_file_for_query(path: str, query: str, previous_files: set[str]) -> float:
    """Score how relevant a file is to the user's message."""
    if not query:
        return 0.0

    score = 0.0
    lower_path = path.lower()
    lower_query = query.lower()
    path_parts = set(lower_path.replace("/", " ").replace(".", " ").replace("_", " ").replace("-", " ").split())
    name = lower_path.rsplit("/", 1)[-1] if "/" in lower_path else lower_path
    name_no_ext = name.rsplit(".", 1)[0] if "." in name else name
    dir_parts = lower_path.rsplit("/", 1)[0].split("/") if "/" in lower_path else []

    # Exact path mentioned in query
    if lower_path in lower_query or path in query:
        score += 200

    # Filename mentioned
    if name in lower_query or name_no_ext in lower_query:
        score += 100

    # Directory mentioned
    for d in dir_parts:
        if len(d) > 2 and d in lower_query:
            score += 40

    # Individual path component words in query
    query_words = set(re.findall(r'[a-z]{3,}', lower_query))
    for part in path_parts:
        if len(part) > 2 and part in query_words:
            score += 15

    # Technology/framework keywords → matching extensions
    tech_ext_map = {
        "react": {".jsx", ".tsx", ".js"},
        "vue": {".vue"},
        "svelte": {".svelte"},
        "python": {".py"},
        "django": {".py"},
        "flask": {".py"},
        "fastapi": {".py"},
        "typescript": {".ts", ".tsx"},
        "javascript": {".js", ".jsx"},
        "rust": {".rs"},
        "go": {".go"},
        "java": {".java"},
        "csharp": {".cs"},
        "c#": {".cs"},
        "unity": {".cs"},
        "ruby": {".rb"},
        "php": {".php"},
        "swift": {".swift"},
        "kotlin": {".kt"},
        "dart": {".dart"},
        "sql": {".sql"},
        "docker": {"dockerfile", ".yml", ".yaml"},
        "css": {".css", ".scss"},
        "html": {".html"},
        "api": {".py", ".js", ".ts", ".go", ".rs"},
        "route": {".py", ".js", ".ts", ".go"},
        "model": {".py", ".cs", ".java", ".ts"},
        "test": {".py", ".js", ".ts", ".java", ".cs"},
        "config": {".json", ".yaml", ".yml", ".toml", ".ini"},
        "database": {".sql", ".py"},
        "migration": {".sql", ".py"},
        "component": {".jsx", ".tsx", ".vue", ".svelte"},
        "style": {".css", ".scss"},
    }

    ext = ""
    if "." in name:
        ext = "." + name.rsplit(".", 1)[-1]

    for keyword, extensions in tech_ext_map.items():
        if keyword in lower_query:
            if ext in extensions or name in extensions:
                score += 20

    # Bonus for files discussed previously in this chat
    if path in previous_files:
        score += 30

    # Bonus for files in common important directories
    important_dirs = {"src", "lib", "app", "api", "routes", "models", "services", "components", "pages", "views", "controllers", "utils", "helpers", "config", "tests", "test"}
    for d in dir_parts:
        if d in important_dirs:
            score += 5

    return score


async def load_smart_files(
    gitlab_url: str,
    token: str,
    project_id: int,
    ref: str,
    tree: list[dict],
    user_query: str = "",
    previous_files: set[str] | None = None,
) -> dict:
    """
    Smart file loader for massive codebases.
    
    Returns:
        {
            "tree_summary": str,        # Full file tree (paths only)
            "priority_files": [...],     # Always-loaded config/entry files
            "query_files": [...],        # Files relevant to user's question
            "total_files_in_tree": int,
            "files_loaded": int,
            "total_characters": int,
            "skipped_large": int,
        }
    """
    if previous_files is None:
        previous_files = set()

    # Separate text files from binary
    text_files = []
    for item in tree:
        if item["type"] != "blob":
            continue
        if _is_text_file(item["path"]):
            text_files.append(item["path"])

    # Split into priority and regular files
    priority_paths = []
    regular_paths = []
    for fp in text_files:
        if _is_priority_file(fp):
            priority_paths.append(fp)
        else:
            regular_paths.append(fp)

    # Score regular files by relevance to user query
    scored = []
    for fp in regular_paths:
        s = _score_file_for_query(fp, user_query, previous_files)
        scored.append((s, fp))

    # Sort by score descending
    scored.sort(key=lambda x: -x[0])

    # Load priority files first
    priority_loaded = []
    priority_chars = 0
    skipped_large = 0

    for fp in priority_paths[:100]:  # Cap at 100 priority files
        if priority_chars >= MAX_PRIORITY_CHARS:
            break
        try:
            raw = await get_file_raw(gitlab_url, token, project_id, fp, ref=ref)
            if len(raw) > MAX_SINGLE_FILE:
                raw = raw[:MAX_SINGLE_FILE] + f"\n\n... [truncated — file is {len(raw)} chars]"
                skipped_large += 1
            priority_loaded.append({"path": fp, "content": raw})
            priority_chars += len(raw)
        except Exception:
            priority_loaded.append({"path": fp, "content": "[Could not read file]"})

    # Load query-relevant files
    query_loaded = []
    query_chars = 0
    loaded_paths = {f["path"] for f in priority_loaded}
    files_loaded_count = len(priority_loaded)

    for score, fp in scored:
        if files_loaded_count >= MAX_FILES_TO_LOAD:
            break
        if query_chars >= MAX_QUERY_CHARS:
            break
        if fp in loaded_paths:
            continue
        # Only load scored files (score > 0) OR if we have room and few files loaded
        if score <= 0 and files_loaded_count > 50:
            break

        try:
            raw = await get_file_raw(gitlab_url, token, project_id, fp, ref=ref)
            if len(raw) > MAX_SINGLE_FILE:
                raw = raw[:MAX_SINGLE_FILE] + f"\n\n... [truncated — file is {len(raw)} chars]"
                skipped_large += 1
            query_loaded.append({"path": fp, "content": raw, "score": score})
            query_chars += len(raw)
            loaded_paths.add(fp)
            files_loaded_count += 1
        except Exception:
            query_loaded.append({"path": fp, "content": "[Could not read file]", "score": score})
            files_loaded_count += 1

    return {
        "priority_files": priority_loaded,
        "query_files": query_loaded,
        "total_files_in_tree": len(text_files),
        "total_binary_files": len(tree) - len(text_files),
        "files_loaded": files_loaded_count,
        "priority_chars": priority_chars,
        "query_chars": query_chars,
        "total_characters": priority_chars + query_chars,
        "skipped_large": skipped_large,
    }


# ═══════════════════════════════════════════════════
#  Legacy bulk loader (used by /analyze endpoint)
# ═══════════════════════════════════════════════════

MAX_ANALYSIS_FILES = 200
MAX_ANALYSIS_CHARS = 600_000


async def load_project_files(
    gitlab_url: str, token: str,
    project_id: int, ref: str = "main",
    path: str = "",
) -> dict:
    tree = await get_tree(gitlab_url, token, project_id, path=path, ref=ref, recursive=True)
    text_files = []
    for item in tree:
        if item["type"] != "blob":
            continue
        if _is_text_file(item["path"]):
            text_files.append(item["path"])

    # Sort: priority files first, then alphabetical
    def sort_key(fp):
        return (0 if _is_priority_file(fp) else 1, fp)

    text_files.sort(key=sort_key)
    text_files = text_files[:MAX_ANALYSIS_FILES]

    contents = []
    total_chars = 0
    for fp in text_files:
        if total_chars >= MAX_ANALYSIS_CHARS:
            break
        try:
            raw = await get_file_raw(gitlab_url, token, project_id, fp, ref=ref)
            if len(raw) > MAX_SINGLE_FILE:
                raw = raw[:MAX_SINGLE_FILE] + "\n... [truncated]"
            if total_chars + len(raw) > MAX_ANALYSIS_CHARS:
                raw = raw[:MAX_ANALYSIS_CHARS - total_chars] + "\n... [truncated]"
            contents.append({"path": fp, "content": raw})
            total_chars += len(raw)
        except Exception:
            contents.append({"path": fp, "content": "[Could not read file]"})

    return {
        "total_files_in_tree": len(tree),
        "files_loaded": len(contents),
        "total_characters": total_chars,
        "files": contents,
    }