import logging
import os
import shutil
import uuid
from fastapi import APIRouter, Depends, HTTPException, Response, status
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.models import GitRepository, SSHKey
from src.schemas.project.git_repository import (
    BranchCreateRequest,
    BranchesResponse,
    CheckoutRequest,
    CommitRequest,
    CommitResponse,
    FetchResponse,
    FileContentResponse,
    FileListResponse,
    FileUpdateRequest,
    FileUpdateResponse,
    GitRepositoryCreate,
    GitRepositoryResponse,
    MergeRequest,
    MergeResponse,
    PullResponse,
    PushResponse,
    StatusResponse,
    UpdateSSHKeyRequest,
    URLParseRequest,
    URLParseResponse,
)
from src.services.git.operations import (
    clone_working_repository,
    get_repo_path,
    init_working_repository,
    list_remote_branches,
    preflight_remote_repository,
)
from src.utils.git_control import (
    checkout_branch,
    commit_changes,
    create_branch,
    delete_branch,
    fetch,
    get_status,
    merge,
    pull,
    push,
)
from src.utils.git_files import commit_file, get_file_content, list_branches, list_tree
from src.utils.git_history import get_commit_detail, get_commit_history
from src.utils.git_url_parser import parse_git_url

router = APIRouter(prefix="/projects", tags=["git-repositories"])
logger = logging.getLogger(__name__)


async def _get_repo(
    session: AsyncSession, user_id: uuid.UUID, project_id: uuid.UUID, repo_id: uuid.UUID
) -> GitRepository:
    _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")
    return repo


async def _get_repo_on_disk(
    session: AsyncSession, user_id: uuid.UUID, project_id: uuid.UUID, repo_id: uuid.UUID
) -> GitRepository:
    repo = await _get_repo(session, user_id, project_id, repo_id)
    if not os.path.exists(repo.path):
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
    return repo


def _parse_remote_url(remote_url: str | None, force_original: bool) -> str | None:
    if not remote_url or force_original:
        return remote_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",
            },
        )
    return parse_result.get("base_url") or remote_url


async def _commit_author(session: AsyncSession, user_id: uuid.UUID) -> tuple[str, str]:
    user = await _get_user(session, user_id)
    return user.name or "Unknown", user.email or "unknown@example.com"


@router.get("/repositories", response_model=list[GitRepositoryResponse])
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."""
    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)
async def parse_repository_url(data: URLParseRequest) -> URLParseResponse:
    """Parse a git URL and detect if it's a browser URL that needs correction."""
    return URLParseResponse(**parse_git_url(data.url))


@router.post("/repositories", response_model=GitRepositoryResponse, status_code=status.HTTP_201_CREATED)
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 (not tied to any project)."""
    _user = await _get_user(session, user_id)
    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")
    remote_url = _parse_remote_url(data.remote_url, data.force_original_url)
    ssh_key_id, ssh_key = None, 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)
    repo = GitRepository(name=data.name, path="", project_id=None, owner_id=user_id, remote_url=remote_url, ssh_key_id=ssh_key_id)
    session.add(repo)
    await session.flush()
    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:
        init_working_repository(repo_path)
        repo.is_mirror = False
    await session.commit()
    return repo


@router.get("/{project_id}/repositories", response_model=list[GitRepositoryResponse])
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]:
    _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)
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:
    repo = await _get_repo(session, user_id, project_id, repo_id)
    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)
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 in a project."""
    _user = await _get_user(session, user_id)
    _project = await _get_owned_project(project_id, user_id, session)
    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")
    remote_url = _parse_remote_url(data.remote_url, data.force_original_url)
    ssh_key_id, ssh_key = None, 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)
    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


@router.patch("/{project_id}/repositories/{repo_id}/ssh-key", response_model=GitRepositoryResponse)
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:
    repo = await _get_repo(session, user_id, project_id, repo_id)
    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")
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:
    repo = await _get_repo_on_disk(session, user_id, project_id, repo_id)
    try:
        return get_commit_history(repo.path, branch=branch, limit=limit, offset=offset)
    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}")
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:
    repo = await _get_repo_on_disk(session, user_id, project_id, repo_id)
    try:
        return get_commit_detail(repo.path, commit_hash)
    except (RuntimeError, ValueError) as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))


@router.get("/{project_id}/repositories/{repo_id}/files", response_model=FileListResponse)
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:
    repo = await _get_repo_on_disk(session, user_id, project_id, repo_id)
    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: %s", repo_id, str(e))
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))


@router.get("/{project_id}/repositories/{repo_id}/files/content", response_model=FileContentResponse)
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:
    repo = await _get_repo_on_disk(session, user_id, project_id, repo_id)
    try:
        fc = get_file_content(repo.path, branch=branch, path=path)
        return FileContentResponse(path=fc.path, branch=fc.branch, content=fc.content, size=fc.size,
            encoding=fc.encoding, language=fc.language, is_binary=fc.is_binary, last_commit=fc.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)
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:
    repo = await _get_repo(session, user_id, project_id, repo_id)
    is_valid = os.path.isdir(os.path.join(repo.path, ".git")) or os.path.isfile(os.path.join(repo.path, "HEAD"))
    if is_valid:
        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))
            raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) from e
    if repo.remote_url:
        ssh_key = await session.get(SSHKey, repo.ssh_key_id) if repo.ssh_key_id else None
        try:
            remote_branches, default_branch = list_remote_branches(repo.remote_url, ssh_key)
            if remote_branches:
                return BranchesResponse(branches=[
                    {"name": b, "is_default": b == default_branch, "last_commit": None} for b in remote_branches
                ], default_branch=default_branch)
        except RuntimeError:
            pass
    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk — re-clone or re-create the repository")


@router.post("/{project_id}/repositories/{repo_id}/files/content", response_model=FileUpdateResponse)
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:
    repo = await _get_repo_on_disk(session, user_id, project_id, repo_id)
    author_name, author_email = await _commit_author(session, user_id)
    try:
        commit_hash = commit_file(repo.path, data.branch, data.path, data.content, data.commit_message, author_name, 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))


@router.get("/{project_id}/repositories/{repo_id}/status", response_model=StatusResponse)
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:
    repo = await _get_repo_on_disk(session, user_id, project_id, repo_id)
    try:
        s = get_status(repo.path)
        return StatusResponse(branch=s.branch, modified=s.modified, added=s.added, deleted=s.deleted,
            untracked=s.untracked, renamed=s.renamed, ahead=s.ahead, behind=s.behind)
    except RuntimeError as e:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))


@router.post("/{project_id}/repositories/{repo_id}/branches")
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:
    repo = await _get_repo_on_disk(session, user_id, project_id, repo_id)
    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}")
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:
    repo = await _get_repo_on_disk(session, user_id, project_id, repo_id)
    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")
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:
    repo = await _get_repo_on_disk(session, user_id, project_id, repo_id)
    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))


@router.post("/{project_id}/repositories/{repo_id}/commit", response_model=CommitResponse)
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:
    repo = await _get_repo_on_disk(session, user_id, project_id, repo_id)
    author_name, author_email = await _commit_author(session, user_id)
    try:
        commit_hash = commit_changes(repo.path, data.message, author_name, author_email, 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))


@router.post("/{project_id}/repositories/{repo_id}/fetch", response_model=FetchResponse)
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:
    repo = await _get_repo_on_disk(session, user_id, project_id, repo_id)
    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))


@router.post("/{project_id}/repositories/{repo_id}/pull", response_model=PullResponse)
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:
    repo = await _get_repo_on_disk(session, user_id, project_id, repo_id)
    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))


@router.post("/{project_id}/repositories/{repo_id}/push", response_model=PushResponse)
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:
    repo = await _get_repo_on_disk(session, user_id, project_id, repo_id)
    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))


@router.post("/{project_id}/repositories/{repo_id}/merge", response_model=MergeResponse)
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:
    repo = await _get_repo_on_disk(session, user_id, project_id, repo_id)
    try:
        commit_hash = merge(repo.path, data.source_branch, data.target_branch, 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))
