"""Git Smart HTTP transport routes (clone/fetch/push over HTTP)."""

from __future__ import annotations

import gzip
import os
import subprocess
from pathlib import Path
from typing import Annotated, Literal

import pygit2
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status
from fastapi.responses import Response

from pygitweb.auth import access_token_from_request, ensure_read_project, ensure_write_project
from pygitweb.config import settings
from pygitweb.dependencies import project_visible_in_list, require_valid_project
from pygitweb.validation import is_valid_project

SmartHttpService = Literal["git-upload-pack", "git-receive-pack"]

http_router = APIRouter(prefix="/http", tags=["smart_http"])

_SERVICE_CONTENT_TYPES: dict[SmartHttpService, tuple[str, str]] = {
	"git-upload-pack": (
		"application/x-git-upload-pack-advertisement",
		"application/x-git-upload-pack-result",
	),
	"git-receive-pack": (
		"application/x-git-receive-pack-advertisement",
		"application/x-git-receive-pack-result",
	),
}


def _decode_request_body(body: bytes, content_encoding: str | None) -> bytes:
	if not content_encoding or not content_encoding.strip():
		return body
	encoding = content_encoding.strip().lower()
	if encoding in ("identity", "none"):
		return body
	if encoding == "gzip":
		try:
			return gzip.decompress(body)
		except OSError as exc:
			raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid gzip body") from exc
	raise HTTPException(
		status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
		detail=f"Unsupported Content-Encoding: {content_encoding}",
	)


def _pkt_line(data: bytes) -> bytes:
	length = len(data) + 4
	return f"{length:04x}".encode("ascii") + data


def _pkt_flush() -> bytes:
	return b"0000"


def _is_valid_http_project(project: str) -> bool:
	return is_valid_project(
		project,
		settings.PROJECTROOT,
		settings.EXPORT_OK,
		settings.STRICT_EXPORT,
		project_visible_in_list,
	)


def require_http_project(project: str) -> str:
	candidate = project.removesuffix(".git")
	if candidate != project and _is_valid_http_project(candidate):
		return candidate
	return require_valid_project(project)


def require_readable_http_project(
	project: str,
	token: Annotated[str | None, Depends(access_token_from_request)],
) -> str:
	validated = require_http_project(project)
	ensure_read_project(validated, token)
	return validated


def require_writable_http_project(
	project: str,
	token: Annotated[str | None, Depends(access_token_from_request)],
) -> str:
	validated = require_http_project(project)
	ensure_write_project(validated, token)
	return validated


ValidatedHttpProject = Annotated[str, Depends(require_http_project)]
ValidatedReadableHttpProject = Annotated[str, Depends(require_readable_http_project)]
ValidatedWritableHttpProject = Annotated[str, Depends(require_writable_http_project)]


def _repo_cwd(project: str) -> Path:
	full = os.path.join(settings.PROJECTROOT, project)
	try:
		git_dir = pygit2.discover_repository(full)
	except (KeyError, pygit2.GitError, OSError) as exc:
		raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No such project") from exc
	if not git_dir:
		raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No such project")
	repo = pygit2.Repository(git_dir)
	if repo.is_bare:
		return Path(git_dir)
	worktree = repo.workdir
	if worktree is None:
		raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Repository has no worktree")
	return Path(worktree)


def _run_git_service(
	service: SmartHttpService,
	project: str,
	*,
	advertise: bool,
	body: bytes | None = None,
	git_protocol: str | None = None,
) -> bytes:
	cwd = _repo_cwd(project)
	args = [settings.GIT, service.removeprefix("git-"), "--stateless-rpc" if not advertise else "--advertise-refs", "."]
	env = os.environ.copy()
	if git_protocol:
		env["GIT_PROTOCOL"] = git_protocol
	try:
		completed = subprocess.run(
			args,
			cwd=str(cwd),
			input=body,
			capture_output=True,
			check=False,
			env=env,
		)
	except OSError as exc:
		raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc
	if completed.returncode != 0:
		detail = completed.stderr.decode("utf-8", errors="replace").strip() or f"git {service} failed"
		raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=detail)
	return completed.stdout


def _service_advertisement(service: SmartHttpService, project: str) -> bytes:
	refs = _run_git_service(service, project, advertise=True)
	announce = _pkt_line(f"# service={service}\n".encode("ascii"))
	return announce + _pkt_flush() + refs


def _smart_http_cache_headers() -> dict[str, str]:
	return {
		"Cache-Control": "no-cache",
		"Pragma": "no-cache",
		"Expires": "Fri, 01 Jan 1980 00:00:00 GMT",
	}


@http_router.get("/{project:path}/info/refs")
def smart_http_info_refs(
	project: ValidatedHttpProject,
	service: Annotated[SmartHttpService, Query(description="Git Smart HTTP service name")],
	token: Annotated[str | None, Depends(access_token_from_request)],
) -> Response:
	if service == "git-receive-pack":
		ensure_write_project(project, token)
	else:
		ensure_read_project(project, token)
	body = _service_advertisement(service, project)
	advertise_type, _ = _SERVICE_CONTENT_TYPES[service]
	return Response(
		content=body,
		media_type=advertise_type,
		headers=_smart_http_cache_headers(),
	)


@http_router.post("/{project:path}/git-upload-pack")
async def smart_http_upload_pack(
	project: ValidatedReadableHttpProject,
	request: Request,
	content_encoding: Annotated[str | None, Header(alias="Content-Encoding")] = None,
	git_protocol: Annotated[str | None, Header(alias="Git-Protocol")] = None,
) -> Response:
	raw_body = await request.body()
	body = _decode_request_body(raw_body, content_encoding)
	result = _run_git_service(
		"git-upload-pack",
		project,
		advertise=False,
		body=body,
		git_protocol=git_protocol,
	)
	_, result_type = _SERVICE_CONTENT_TYPES["git-upload-pack"]
	return Response(content=result, media_type=result_type, headers=_smart_http_cache_headers())


@http_router.post("/{project:path}/git-receive-pack")
async def smart_http_receive_pack(
	project: ValidatedWritableHttpProject,
	request: Request,
	content_encoding: Annotated[str | None, Header(alias="Content-Encoding")] = None,
	git_protocol: Annotated[str | None, Header(alias="Git-Protocol")] = None,
) -> Response:
	raw_body = await request.body()
	body = _decode_request_body(raw_body, content_encoding)
	result = _run_git_service(
		"git-receive-pack",
		project,
		advertise=False,
		body=body,
		git_protocol=git_protocol,
	)
	_, result_type = _SERVICE_CONTENT_TYPES["git-receive-pack"]
	return Response(content=result, media_type=result_type, headers=_smart_http_cache_headers())