import logging
import os
import shutil
import subprocess
import uuid
from datetime import datetime

from fastapi import APIRouter, Depends, HTTPException, Response, status
from pydantic import BaseModel, ConfigDict
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from src.auth.dependencies import _get_owned_project, _get_user, get_current_user_id, get_db_session
from src.config import Settings
from src.models.git_repository import GitRepository
from src.models.ssh_key import SSHKey
from src.utils.git_files import (
    commit_file,
    get_file_content,
    list_branches,
    list_tree,
)
from src.utils.git_control import (
    checkout_branch,
    commit_changes,
    create_branch,
    delete_branch,
    fetch,
    get_status,
    merge,
    pull,
    push,
)
from src.utils.git_history import get_commit_detail, get_commit_history
from src.utils.git_url_parser import parse_git_url
from src.services.ssh_keys import _get_fernet

router = APIRouter(prefix="/projects", tags=["git-repositories"])

logger = logging.getLogger(__name__)


def _get_repo_path(user_id: uuid.UUID, project_id: uuid.UUID, name: str) -> str:
    """Generate the filesystem path for a repository.

    Args:
        user_id: UUID of the repository owner.
        project_id: UUID of the project.
        name: Repository name.

    Returns:
        Absolute path to the repository directory.
    """
    base = Settings().repo_base_path or "/data/repos"
    return os.path.join(base, str(user_id), str(project_id), f"{name}.git")


def _build_provider_clone_url(owner: str, repo: str) -> str:
    """Build the SSH clone URL for the fixed git provider."""
    return f"git@git.commumedia.org:{owner}/{repo}.git"


def _prepare_ssh_env(ssh_key: SSHKey | None) -> dict | None:
    """Prepare environment variables for git commands with SSH authentication.
    
    Returns a dict of extra env vars, or None if no SSH key provided.
    The caller is responsible for cleaning up the temporary key file.
    """
    if ssh_key is None:
        return None
    
    import tempfile
    
    # Decrypt private key
    fernet = _get_fernet()
    private_key = fernet.decrypt(ssh_key.private_key_encrypted.encode()).decode()
    
    # Write to temp file with restricted permissions
    fd, key_path = tempfile.mkstemp(prefix="ssh_key_")
    try:
        os.write(fd, private_key.encode())
    finally:
        os.close(fd)
    os.chmod(key_path, 0o600)
    
    # Return env vars and the key path for cleanup
    env = {
        "GIT_SSH_COMMAND": f"ssh -i {key_path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
    }
    return env, key_path


def _preflight_remote_repository(remote_url: str, ssh_key: SSHKey | None = None) -> None:
    """Verify a remote repository is reachable before cloning."""
    env = None
    key_path = None
    
    if ssh_key is not None:
        ssh_result = _prepare_ssh_env(ssh_key)
        if ssh_result:
            env, key_path = ssh_result
    
    try:
        result = subprocess.run(
            ["git", "ls-remote", remote_url],
            capture_output=True,
            text=True,
            timeout=60,
            env={**os.environ, **env} if env else None,
        )
    except subprocess.TimeoutExpired:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="remote repository check timed out")
    except FileNotFoundError:
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="git command not found")
    finally:
        if key_path and os.path.exists(key_path):
            os.unlink(key_path)

    if result.returncode != 0:
        logger.error("Preflight check failed for %s: stderr=%s", remote_url, result.stderr)
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"repository not found or inaccessible: {result.stderr}",
        )


def _clone_working_repository(remote_url: str, repo_path: str, ssh_key: SSHKey | None = None) -> None:
    env = None
    key_path = None
    
    if ssh_key is not None:
        ssh_result = _prepare_ssh_env(ssh_key)
        if ssh_result:
            env, key_path = ssh_result
    
    try:
        result = subprocess.run(
            ["git", "clone", remote_url, repo_path],
            capture_output=True,
            text=True,
            timeout=300,
            env={**os.environ, **env} if env else None,
        )
    except subprocess.TimeoutExpired:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="clone operation timed out")
    except FileNotFoundError:
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="git command not found")
    finally:
        if key_path and os.path.exists(key_path):
            os.unlink(key_path)

    if result.returncode != 0:
        logger.error("Clone failed for %s: stderr=%s", remote_url, result.stderr)
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"failed to clone repository: {result.stderr}",
        )


def _init_working_repository(repo_path: str) -> None:
    try:
        result = subprocess.run(
            ["git", "init", "-b", "main", repo_path],
            capture_output=True,
            text=True,
        )
    except FileNotFoundError:
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="git command not found")

    if result.returncode == 0:
        return

    fallback = subprocess.run(
        ["git", "init", repo_path],
        capture_output=True,
        text=True,
    )
    if fallback.returncode != 0:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"failed to initialize repository: {fallback.stderr}",
        )

    ref_result = subprocess.run(
        ["git", "-C", repo_path, "symbolic-ref", "HEAD", "refs/heads/main"],
        capture_output=True,
        text=True,
    )
    if ref_result.returncode != 0:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"failed to set initial branch: {ref_result.stderr}",
        )


class GitRepositoryCreate(BaseModel):
    name: str
    remote_url: str | None = None
    force_original_url: bool = False
    ssh_key_id: str | None = None


class URLParseRequest(BaseModel):
    url: str


class URLParseResponse(BaseModel):
    original_url: str
    base_url: str | None
    is_valid_clone_url: bool
    needs_parsing: bool
    host: str | None
    message: str
    error_code: str | None


class GitRepositoryResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: uuid.UUID
    name: str
    path: str
    project_id: uuid.UUID | None
    owner_id: uuid.UUID
    is_mirror: bool
    remote_url: str | None
    last_push: datetime | None
    ssh_key_id: uuid.UUID | None
    created_at: datetime
    updated_at: datetime


@router.get(
    "/repositories",
    response_model=list[GitRepositoryResponse],
    summary="List all user repositories",
    description="List all git repositories owned by the user, including external repositories not tied to any project.",
)
async def list_user_repositories(
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> list[GitRepository]:
    """List all repositories owned by the user.

    Args:
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        List of all repositories owned by the user.
    """
    result = await session.execute(
        select(GitRepository).where(GitRepository.owner_id == user_id)
    )
    return list(result.scalars().all())


@router.post(
    "/repositories/parse-url",
    response_model=URLParseResponse,
    summary="Parse a git URL",
    description="Parse a git URL and detect if it's a browser URL that needs correction.",
)
async def parse_repository_url(data: URLParseRequest) -> URLParseResponse:
    """Parse a git URL and detect if it's a browser URL that needs correction.

    Args:
        data: Request containing the URL to parse.

    Returns:
        Parsed URL information including whether it needs parsing and suggested corrections.
    """
    result = parse_git_url(data.url)
    return URLParseResponse(**result)


@router.post(
    "/repositories",
    response_model=GitRepositoryResponse,
    status_code=status.HTTP_201_CREATED,
    summary="Create an external repository",
    description="Create a new external git repository (not tied to any project). Can clone from remote URL.",
)
async def create_external_repository(
    data: GitRepositoryCreate,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> GitRepository:
    """Create a new external git repository.

    External repositories are not tied to any project and can be used
    across all projects for config profile git mounts.

    Args:
        data: Repository creation data including name and optional remote URL.
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        The newly created external repository.
    """
    _user = await _get_user(session, user_id)

    # Check for duplicate name (external repos only)
    existing = await session.execute(
        select(GitRepository).where(
            GitRepository.project_id.is_(None),
            GitRepository.owner_id == user_id,
            GitRepository.name == data.name,
        )
    )
    if existing.scalar_one_or_none():
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="repository name already exists")

    # Validate and potentially correct the URL
    remote_url = data.remote_url
    if remote_url and not data.force_original_url:
        parse_result = parse_git_url(remote_url)
        if parse_result["needs_parsing"] and parse_result["base_url"]:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail={
                    "message": "The provided URL appears to be a browser URL, not a git clone URL",
                    "suggested_url": parse_result["base_url"],
                    "original_url": remote_url,
                    "error_code": "URL_NEEDS_PARSING",
                },
            )
        if parse_result["base_url"]:
            remote_url = parse_result["base_url"]

    # Validate SSH key if provided
    ssh_key_id = None
    ssh_key = None
    if data.ssh_key_id:
        try:
            ssh_key_id = uuid.UUID(data.ssh_key_id)
        except ValueError:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid ssh_key_id format")
        
        ssh_key = await session.get(SSHKey, ssh_key_id)
        if ssh_key is None:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ssh key not found")
        if ssh_key.user_id != user_id:
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ssh key does not belong to user")

    if remote_url:
        _preflight_remote_repository(remote_url, ssh_key)

    # Create external repo with no project
    repo = GitRepository(
        name=data.name,
        path="",  # Will be set after clone
        project_id=None,
        owner_id=user_id,
        remote_url=remote_url,
        ssh_key_id=ssh_key_id,
    )
    session.add(repo)
    await session.flush()

    # Set path and optionally clone
    repo_path = f"/data/repos/external/{user_id}/{repo.id}"
    repo.path = repo_path

    if remote_url:
        try:
            _clone_working_repository(remote_url, repo_path, ssh_key)
            repo.is_mirror = False
        except Exception as exc:
            await session.rollback()
            raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to clone repository: {exc}")
    else:
        # Initialize empty repo
        os.makedirs(repo_path, exist_ok=True)
        subprocess.run(["git", "init", repo_path], check=True, capture_output=True)
        repo.is_mirror = False

    await session.commit()
    return repo


@router.get(
    "/{project_id}/repositories",
    response_model=list[GitRepositoryResponse],
    summary="List repositories",
    description="List all git repositories in a project.",
)
async def list_repositories(
    project_id: uuid.UUID,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> list[GitRepository]:
    """List all repositories in a project.

    Args:
        project_id: UUID of the project.
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        List of repositories in the project.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    result = await session.execute(
        select(GitRepository).where(GitRepository.project_id == project_id)
    )
    return list(result.scalars().all())


@router.delete(
    "/{project_id}/repositories/{repo_id}",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Delete a repository",
    description="Delete a git repository from the project and remove it from disk.",
)
async def delete_repository(
    project_id: uuid.UUID,
    repo_id: uuid.UUID,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> Response:
    """Delete a repository.

    Args:
        project_id: UUID of the project.
        repo_id: UUID of the repository to delete.
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        Empty response with 204 status code.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    repo = await session.get(GitRepository, repo_id)
    if repo is None or repo.project_id != project_id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")

    # Remove from disk
    if os.path.exists(repo.path):
        shutil.rmtree(repo.path)

    await session.delete(repo)
    await session.commit()
    return Response(status_code=status.HTTP_204_NO_CONTENT)


@router.post(
    "/{project_id}/repositories",
    response_model=GitRepositoryResponse,
    status_code=status.HTTP_201_CREATED,
    summary="Create a repository",
    description="Create a new git repository in a project. Can clone from remote or initialize a working repository.",
)
async def create_repository(
    project_id: uuid.UUID,
    data: GitRepositoryCreate,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> GitRepository:
    """Create a new git repository.

    Args:
        project_id: UUID of the project.
        data: Repository creation data including name and optional remote URL.
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        The newly created repository.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    # Check for duplicate name
    existing = await session.execute(
        select(GitRepository).where(
            GitRepository.project_id == project_id,
            GitRepository.name == data.name,
        )
    )
    if existing.scalar_one_or_none():
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="repository name already exists")

    # Validate and potentially correct the URL
    remote_url = data.remote_url
    if remote_url and not data.force_original_url:
        parse_result = parse_git_url(remote_url)
        if parse_result["needs_parsing"] and parse_result["base_url"]:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail={
                    "message": "The provided URL appears to be a browser URL, not a git clone URL",
                    "suggested_url": parse_result["base_url"],
                    "original_url": remote_url,
                    "error_code": "URL_NEEDS_PARSING",
                },
            )
        # Use base_url if it was extracted (for URLs without .git suffix)
        if parse_result["base_url"]:
            remote_url = parse_result["base_url"]

    # Validate SSH key if provided
    ssh_key_id = None
    ssh_key = None
    if data.ssh_key_id:
        try:
            ssh_key_id = uuid.UUID(data.ssh_key_id)
        except ValueError:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid ssh_key_id format")
        
        ssh_key = await session.get(SSHKey, ssh_key_id)
        if ssh_key is None:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ssh key not found")
        if ssh_key.user_id != user_id and ssh_key.project_id != project_id:
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ssh key does not belong to user or project")

    if remote_url:
        _preflight_remote_repository(remote_url, ssh_key)

    repo_path = _get_repo_path(user_id, project_id, data.name)

    # Ensure parent directory exists
    os.makedirs(os.path.dirname(repo_path), exist_ok=True)

    if remote_url:
        _clone_working_repository(remote_url, repo_path, ssh_key)
    else:
        _init_working_repository(repo_path)

    repo = GitRepository(
        name=data.name,
        path=repo_path,
        project_id=project_id,
        owner_id=user_id,
        is_mirror=False,
        remote_url=remote_url,
        ssh_key_id=ssh_key_id,
    )
    session.add(repo)
    await session.commit()
    await session.refresh(repo)
    return repo


class UpdateSSHKeyRequest(BaseModel):
    ssh_key_id: str | None = None


@router.patch(
    "/{project_id}/repositories/{repo_id}/ssh-key",
    response_model=GitRepositoryResponse,
    summary="Update repository SSH key",
    description="Update the SSH key associated with a repository.",
)
async def update_repository_ssh_key(
    project_id: uuid.UUID,
    repo_id: uuid.UUID,
    data: UpdateSSHKeyRequest,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> GitRepository:
    """Update the SSH key for a repository.

    Args:
        project_id: UUID of the project.
        repo_id: UUID of the repository.
        data: Update data containing the new SSH key ID.
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        The updated repository.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    repo = await session.get(GitRepository, repo_id)
    if repo is None or repo.project_id != project_id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")

    # Validate SSH key if provided
    if data.ssh_key_id:
        try:
            ssh_key_id = uuid.UUID(data.ssh_key_id)
        except ValueError:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid ssh_key_id format")

        ssh_key = await session.get(SSHKey, ssh_key_id)
        if ssh_key is None:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ssh key not found")
        if ssh_key.user_id != user_id and ssh_key.project_id != project_id:
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ssh key does not belong to user or project")

        repo.ssh_key_id = ssh_key_id
    else:
        repo.ssh_key_id = None

    await session.commit()
    await session.refresh(repo)
    return repo


@router.get(
    "/{project_id}/repositories/{repo_id}/history",
    summary="Get repository history",
    description="Get commit history for a repository with optional branch filtering.",
)
async def get_repository_history(
    project_id: uuid.UUID,
    repo_id: uuid.UUID,
    view: str = "graph",
    branch: str | None = None,
    limit: int = 100,
    offset: int = 0,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> dict:
    """Get commit history for a repository.

    Args:
        project_id: UUID of the project.
        repo_id: UUID of the repository.
        view: View type for history display (default: graph).
        branch: Optional branch name to filter commits.
        limit: Maximum number of commits to return (default: 100).
        offset: Number of commits to skip (default: 0).
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        Dictionary containing commit history data.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    repo = await session.get(GitRepository, repo_id)
    if repo is None or repo.project_id != project_id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")

    if not os.path.exists(repo.path):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")

    try:
        history = get_commit_history(repo.path, branch=branch, limit=limit, offset=offset)
        return history
    except RuntimeError as e:
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))


@router.get(
    "/{project_id}/repositories/{repo_id}/commits/{commit_hash}",
    summary="Get commit details",
    description="Get detailed information about a specific commit.",
)
async def get_repository_commit(
    project_id: uuid.UUID,
    repo_id: uuid.UUID,
    commit_hash: str,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> dict:
    """Get detailed information about a specific commit.

    Args:
        project_id: UUID of the project.
        repo_id: UUID of the repository.
        commit_hash: Hash of the commit to retrieve.
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        Dictionary containing commit details.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    repo = await session.get(GitRepository, repo_id)
    if repo is None or repo.project_id != project_id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")

    if not os.path.exists(repo.path):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")

    try:
        detail = get_commit_detail(repo.path, commit_hash)
        return detail
    except (RuntimeError, ValueError) as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))


# File browsing endpoints


class FileListResponse(BaseModel):
    path: str
    branch: str
    entries: list[dict]


class FileContentResponse(BaseModel):
    path: str
    branch: str
    content: str
    size: int
    encoding: str
    language: str | None
    is_binary: bool
    last_commit: dict | None


class BranchesResponse(BaseModel):
    branches: list[dict]
    default_branch: str


class FileUpdateRequest(BaseModel):
    path: str
    branch: str
    content: str
    commit_message: str


class FileUpdateResponse(BaseModel):
    commit_hash: str
    message: str
    branch: str


@router.get(
    "/{project_id}/repositories/{repo_id}/files",
    response_model=FileListResponse,
    summary="List repository files",
    description="List files and directories in a repository path.",
)
async def list_repository_files(
    project_id: uuid.UUID,
    repo_id: uuid.UUID,
    branch: str = "main",
    path: str = "",
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> FileListResponse:
    """List files and directories in a repository path.

    Args:
        project_id: UUID of the project.
        repo_id: UUID of the repository.
        branch: Branch name to browse (default: main).
        path: Directory path within the repository (default: root).
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        List of files and directories in the specified path.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    repo = await session.get(GitRepository, repo_id)
    if repo is None or repo.project_id != project_id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")

    if not os.path.exists(repo.path):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")

    try:
        entries = list_tree(repo.path, branch=branch, path=path)
        return FileListResponse(
            path=path,
            branch=branch,
            entries=[
                {
                    "name": e.name,
                    "type": e.type,
                    "path": e.path,
                    "size": e.size,
                    "mode": e.mode,
                    "last_commit": e.last_commit,
                }
                for e in entries
            ],
        )
    except RuntimeError as e:
        logger.error(
            "Failed to list files for repo %s (path=%s, branch=%s): %s",
            repo_id,
            path,
            branch,
            str(e),
            exc_info=True,
        )
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))


@router.get(
    "/{project_id}/repositories/{repo_id}/files/content",
    response_model=FileContentResponse,
    summary="Get file content",
    description="Get the content of a file in a repository.",
)
async def get_repository_file_content(
    project_id: uuid.UUID,
    repo_id: uuid.UUID,
    branch: str,
    path: str,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> FileContentResponse:
    """Get the content of a file.

    Args:
        project_id: UUID of the project.
        repo_id: UUID of the repository.
        branch: Branch name where the file is located.
        path: File path within the repository.
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        File content and metadata.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    repo = await session.get(GitRepository, repo_id)
    if repo is None or repo.project_id != project_id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")

    if not os.path.exists(repo.path):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")

    try:
        file_content = get_file_content(repo.path, branch=branch, path=path)
        return FileContentResponse(
            path=file_content.path,
            branch=file_content.branch,
            content=file_content.content,
            size=file_content.size,
            encoding=file_content.encoding,
            language=file_content.language,
            is_binary=file_content.is_binary,
            last_commit=file_content.last_commit,
        )
    except FileNotFoundError:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="file not found")
    except RuntimeError as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))


@router.get(
    "/{project_id}/repositories/{repo_id}/branches",
    response_model=BranchesResponse,
    summary="List branches",
    description="List all branches in the repository.",
)
async def get_repository_branches(
    project_id: uuid.UUID,
    repo_id: uuid.UUID,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> BranchesResponse:
    """List all branches in the repository.

    Args:
        project_id: UUID of the project.
        repo_id: UUID of the repository.
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        List of branches and the default branch name.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    repo = await session.get(GitRepository, repo_id)
    if repo is None or repo.project_id != project_id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")

    if not os.path.exists(repo.path):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")

    try:
        branches, default_branch = list_branches(repo.path)
        return BranchesResponse(
            branches=[
                {
                    "name": b.name,
                    "is_default": b.is_default,
                    "last_commit": b.last_commit,
                }
                for b in branches
            ],
            default_branch=default_branch,
        )
    except RuntimeError as e:
        logger.error(
            "Failed to list branches for repo %s: %s",
            repo_id,
            str(e),
            exc_info=True,
        )
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))


@router.post(
    "/{project_id}/repositories/{repo_id}/files/content",
    response_model=FileUpdateResponse,
    summary="Update file content",
    description="Update a file and create a commit.",
)
async def update_repository_file(
    project_id: uuid.UUID,
    repo_id: uuid.UUID,
    data: FileUpdateRequest,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> FileUpdateResponse:
    """Update a file and create a commit.

    Args:
        project_id: UUID of the project.
        repo_id: UUID of the repository.
        data: File update data including path, branch, content, and commit message.
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        Commit information for the file update.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    repo = await session.get(GitRepository, repo_id)
    if repo is None or repo.project_id != project_id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")

    if not os.path.exists(repo.path):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")

    # Get user info for commit
    user = await _get_user(session, user_id)
    author_name = user.name or "Unknown"
    author_email = user.email or "unknown@example.com"

    try:
        commit_hash = commit_file(
            repo_path=repo.path,
            branch=data.branch,
            path=data.path,
            content=data.content,
            commit_message=data.commit_message,
            author_name=author_name,
            author_email=author_email,
        )
        return FileUpdateResponse(
            commit_hash=commit_hash,
            message=data.commit_message,
            branch=data.branch,
        )
    except RuntimeError as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))


# Git Control Endpoints


class StatusResponse(BaseModel):
    branch: str
    modified: list[str]
    added: list[str]
    deleted: list[str]
    untracked: list[str]
    renamed: list[str]
    ahead: int
    behind: int


@router.get(
    "/{project_id}/repositories/{repo_id}/status",
    response_model=StatusResponse,
    summary="Get repository status",
    description="Get the working directory status including modified, added, and deleted files.",
)
async def get_repository_status(
    project_id: uuid.UUID,
    repo_id: uuid.UUID,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> StatusResponse:
    """Get the working directory status.

    Args:
        project_id: UUID of the project.
        repo_id: UUID of the repository.
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        Repository status including branch, modified files, and ahead/behind counts.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    repo = await session.get(GitRepository, repo_id)
    if repo is None or repo.project_id != project_id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")

    if not os.path.exists(repo.path):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")

    try:
        status_result = get_status(repo.path)
        return StatusResponse(
            branch=status_result.branch,
            modified=status_result.modified,
            added=status_result.added,
            deleted=status_result.deleted,
            untracked=status_result.untracked,
            renamed=status_result.renamed,
            ahead=status_result.ahead,
            behind=status_result.behind,
        )
    except RuntimeError as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))


class BranchCreateRequest(BaseModel):
    name: str
    base_branch: str = "HEAD"


class CheckoutRequest(BaseModel):
    branch: str


@router.post(
    "/{project_id}/repositories/{repo_id}/branches",
    summary="Create a branch",
    description="Create a new branch in the repository.",
)
async def create_repository_branch(
    project_id: uuid.UUID,
    repo_id: uuid.UUID,
    data: BranchCreateRequest,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> dict:
    """Create a new branch.

    Args:
        project_id: UUID of the project.
        repo_id: UUID of the repository.
        data: Branch creation data including name and optional base branch.
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        Dictionary with success message and branch name.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    repo = await session.get(GitRepository, repo_id)
    if repo is None or repo.project_id != project_id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")

    if not os.path.exists(repo.path):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")

    try:
        create_branch(repo.path, data.name, data.base_branch)
        return {"message": f"Branch '{data.name}' created", "branch": data.name}
    except RuntimeError as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))


@router.delete(
    "/{project_id}/repositories/{repo_id}/branches/{branch_name}",
    summary="Delete a branch",
    description="Delete a branch from the repository.",
)
async def delete_repository_branch(
    project_id: uuid.UUID,
    repo_id: uuid.UUID,
    branch_name: str,
    force: bool = False,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> dict:
    """Delete a branch.

    Args:
        project_id: UUID of the project.
        repo_id: UUID of the repository.
        branch_name: Name of the branch to delete.
        force: Whether to force delete the branch.
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        Dictionary with success message.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    repo = await session.get(GitRepository, repo_id)
    if repo is None or repo.project_id != project_id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")

    if not os.path.exists(repo.path):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")

    try:
        delete_branch(repo.path, branch_name, force)
        return {"message": f"Branch '{branch_name}' deleted"}
    except RuntimeError as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))


@router.post(
    "/{project_id}/repositories/{repo_id}/checkout",
    summary="Checkout a branch",
    description="Checkout a branch in the repository.",
)
async def checkout_repository_branch(
    project_id: uuid.UUID,
    repo_id: uuid.UUID,
    data: CheckoutRequest,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> dict:
    """Checkout a branch.

    Args:
        project_id: UUID of the project.
        repo_id: UUID of the repository.
        data: Checkout request containing the branch name.
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        Dictionary with success message and checked out branch name.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    repo = await session.get(GitRepository, repo_id)
    if repo is None or repo.project_id != project_id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")

    if not os.path.exists(repo.path):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")

    try:
        checkout_branch(repo.path, data.branch)
        return {"message": f"Checked out branch '{data.branch}'", "branch": data.branch}
    except RuntimeError as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))


class CommitRequest(BaseModel):
    message: str
    files: list[str] | None = None


class CommitResponse(BaseModel):
    commit_hash: str
    message: str


@router.post(
    "/{project_id}/repositories/{repo_id}/commit",
    response_model=CommitResponse,
    summary="Commit changes",
    description="Commit changes to the repository.",
)
async def commit_repository_changes(
    project_id: uuid.UUID,
    repo_id: uuid.UUID,
    data: CommitRequest,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> CommitResponse:
    """Commit changes to the repository.

    Args:
        project_id: UUID of the project.
        repo_id: UUID of the repository.
        data: Commit request containing message and optional files to commit.
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        Commit information including hash and message.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    repo = await session.get(GitRepository, repo_id)
    if repo is None or repo.project_id != project_id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")

    if not os.path.exists(repo.path):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")

    # Get user info for commit
    user = await _get_user(session, user_id)
    author_name = user.name or "Unknown"
    author_email = user.email or "unknown@example.com"

    try:
        commit_hash = commit_changes(
            repo_path=repo.path,
            message=data.message,
            author_name=author_name,
            author_email=author_email,
            files=data.files,
        )
        return CommitResponse(
            commit_hash=commit_hash,
            message=data.message,
        )
    except RuntimeError as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))


class FetchResponse(BaseModel):
    message: str


@router.post(
    "/{project_id}/repositories/{repo_id}/fetch",
    response_model=FetchResponse,
    summary="Fetch from remote",
    description="Fetch updates from the remote repository.",
)
async def fetch_repository(
    project_id: uuid.UUID,
    repo_id: uuid.UUID,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> FetchResponse:
    """Fetch from remote.

    Args:
        project_id: UUID of the project.
        repo_id: UUID of the repository.
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        Success message.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    repo = await session.get(GitRepository, repo_id)
    if repo is None or repo.project_id != project_id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")

    if not os.path.exists(repo.path):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")

    try:
        fetch(repo.path)
        return FetchResponse(message="Fetched from remote")
    except RuntimeError as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))


class PullResponse(BaseModel):
    message: str


@router.post(
    "/{project_id}/repositories/{repo_id}/pull",
    response_model=PullResponse,
    summary="Pull from remote",
    description="Pull updates from the remote repository.",
)
async def pull_repository(
    project_id: uuid.UUID,
    repo_id: uuid.UUID,
    branch: str | None = None,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> PullResponse:
    """Pull updates from remote.

    Args:
        project_id: UUID of the project.
        repo_id: UUID of the repository.
        branch: Optional branch name to pull.
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        Success message.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    repo = await session.get(GitRepository, repo_id)
    if repo is None or repo.project_id != project_id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")

    if not os.path.exists(repo.path):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")

    try:
        pull(repo.path, branch)
        return PullResponse(message="Pulled from remote")
    except RuntimeError as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))


class PushResponse(BaseModel):
    message: str


@router.post(
    "/{project_id}/repositories/{repo_id}/push",
    response_model=PushResponse,
    summary="Push to remote",
    description="Push changes to the remote repository.",
)
async def push_repository(
    project_id: uuid.UUID,
    repo_id: uuid.UUID,
    branch: str | None = None,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> PushResponse:
    """Push changes to remote.

    Args:
        project_id: UUID of the project.
        repo_id: UUID of the repository.
        branch: Optional branch name to push.
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        Success message.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    repo = await session.get(GitRepository, repo_id)
    if repo is None or repo.project_id != project_id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")

    if not os.path.exists(repo.path):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")

    try:
        push(repo.path, branch)
        return PushResponse(message="Pushed to remote")
    except RuntimeError as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))


class MergeRequest(BaseModel):
    source_branch: str
    target_branch: str | None = None
    message: str | None = None


class MergeResponse(BaseModel):
    commit_hash: str
    message: str


@router.post(
    "/{project_id}/repositories/{repo_id}/merge",
    response_model=MergeResponse,
    summary="Merge branches",
    description="Merge one branch into another.",
)
async def merge_repository_branches(
    project_id: uuid.UUID,
    repo_id: uuid.UUID,
    data: MergeRequest,
    user_id: uuid.UUID = Depends(get_current_user_id),
    session: AsyncSession = Depends(get_db_session),
) -> MergeResponse:
    """Merge branches.

    Args:
        project_id: UUID of the project.
        repo_id: UUID of the repository.
        data: Merge request containing source branch, optional target branch, and message.
        user_id: ID of the authenticated user.
        session: Database session.

    Returns:
        Merge result with commit hash and message.
    """
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)

    repo = await session.get(GitRepository, repo_id)
    if repo is None or repo.project_id != project_id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")

    if not os.path.exists(repo.path):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")

    try:
        commit_hash = merge(
            repo_path=repo.path,
            source_branch=data.source_branch,
            target_branch=data.target_branch,
            message=data.message,
        )
        return MergeResponse(
            commit_hash=commit_hash,
            message=data.message or f"Merge {data.source_branch}",
        )
    except RuntimeError as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
