# retoor <retoor@molodetz.nl>
from __future__ import annotations

import asyncio
import logging
import re
from dataclasses import dataclass
from pathlib import Path
from typing import AsyncIterator, Optional
from urllib.parse import urlparse

import httpx

from devplacepy.stealth import stealth_async_client

from devplacepy.services.jobs.isslop.acquisition.workspace import directory_size_bytes
from devplacepy.services.jobs.isslop.config import (
    GIT_CLONE_TIMEOUT_SECONDS,
    GIT_SIZE_LIMIT_BYTES,
    GIT_SIZE_POLL_SECONDS,
    WEBSITE_REQUEST_TIMEOUT_SECONDS,
)

logger = logging.getLogger(__name__)


class RepositoryTooLargeError(Exception):
    pass


class CloneFailedError(Exception):
    pass


@dataclass(frozen=True)
class SizePreflight:
    known: bool
    size_bytes: int
    origin: str


def _owner_repo(url: str) -> Optional[tuple[str, str, str]]:
    match = re.match(r"^git@([\w.-]+):(.+)$", url)
    if match:
        host, path = match.group(1), match.group(2)
    else:
        parsed = urlparse(url)
        host, path = parsed.hostname or "", parsed.path
    parts = [part for part in path.strip("/").removesuffix(".git").split("/") if part]
    if len(parts) < 2 or not host:
        return None
    return host, parts[0], parts[1]


async def preflight_size(url: str) -> SizePreflight:
    located = _owner_repo(url)
    if located is None:
        return SizePreflight(known=False, size_bytes=0, origin="unparseable")
    host, owner, repo = located
    candidates: list[tuple[str, str]] = []
    if host == "github.com":
        candidates.append((f"https://api.github.com/repos/{owner}/{repo}", "github"))
    else:
        candidates.append((f"https://{host}/api/v1/repos/{owner}/{repo}", "gitea"))
    async with stealth_async_client(timeout=WEBSITE_REQUEST_TIMEOUT_SECONDS, follow_redirects=True) as client:
        for api_url, origin in candidates:
            try:
                response = await client.get(api_url, headers={"accept": "application/json"})
            except httpx.HTTPError as error:
                logger.info("Size preflight unavailable via %s: %s", origin, error)
                continue
            if response.status_code != 200:
                logger.info("Size preflight %s returned HTTP %d", origin, response.status_code)
                continue
            try:
                body = response.json()
            except ValueError:
                continue
            size_kb = body.get("size")
            if isinstance(size_kb, (int, float)) and size_kb > 0:
                size_bytes = int(size_kb) * 1024
                logger.info("Preflight size via %s: %d bytes", origin, size_bytes)
                return SizePreflight(known=True, size_bytes=size_bytes, origin=origin)
    return SizePreflight(known=False, size_bytes=0, origin="unknown")


async def clone_repository(url: str, workspace: Path, size_limit: int = GIT_SIZE_LIMIT_BYTES) -> AsyncIterator[str]:
    preflight = await preflight_size(url)
    if preflight.known:
        yield f"Repository size reported by {preflight.origin}: {preflight.size_bytes / (1024 * 1024):.1f} MB"
        if preflight.size_bytes > size_limit:
            raise RepositoryTooLargeError(
                f"Repository is {preflight.size_bytes / (1024 ** 3):.2f} GB which exceeds the 3 GB limit"
            )
    else:
        yield "Repository size not determinable up front, monitoring during clone"

    argv = [
        "git",
        "clone",
        "--depth",
        "1",
        "--single-branch",
        "--no-tags",
        url,
        str(workspace),
    ]
    yield f"Cloning with depth 1: {url}"
    proc = await asyncio.create_subprocess_exec(
        *argv,
        stdin=asyncio.subprocess.DEVNULL,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.STDOUT,
        env={"GIT_TERMINAL_PROMPT": "0", "GIT_ASKPASS": "true", "PATH": "/usr/bin:/bin:/usr/local/bin"},
    )

    aborted_reason: list[str] = []

    async def monitor() -> None:
        while proc.returncode is None:
            await asyncio.sleep(GIT_SIZE_POLL_SECONDS)
            if not workspace.exists():
                continue
            size = await asyncio.to_thread(directory_size_bytes, workspace)
            logger.debug("Clone size check: %d bytes", size)
            if size > size_limit:
                aborted_reason.append(f"Clone exceeded 3 GB limit at {size / (1024 ** 3):.2f} GB, cancelled")
                proc.kill()
                return

    monitor_task = asyncio.create_task(monitor())
    output_lines: list[str] = []
    try:
        assert proc.stdout is not None
        while True:
            try:
                line = await asyncio.wait_for(proc.stdout.readline(), timeout=GIT_CLONE_TIMEOUT_SECONDS)
            except asyncio.TimeoutError:
                proc.kill()
                raise CloneFailedError("Clone timed out")
            if not line:
                break
            text = line.decode("utf-8", errors="replace").strip()
            if text:
                output_lines.append(text)
                yield f"git: {text}"
        await proc.wait()
    finally:
        monitor_task.cancel()
        try:
            await monitor_task
        except asyncio.CancelledError:
            logger.debug("Clone size monitor stopped")

    if aborted_reason:
        raise RepositoryTooLargeError(aborted_reason[0])
    if proc.returncode != 0:
        tail = " | ".join(output_lines[-4:])
        raise CloneFailedError(f"git clone failed with code {proc.returncode}: {tail}")
    final_size = await asyncio.to_thread(directory_size_bytes, workspace)
    if final_size > size_limit:
        raise RepositoryTooLargeError(f"Repository is {final_size / (1024 ** 3):.2f} GB which exceeds the 3 GB limit")
    yield f"Clone complete: {final_size / (1024 * 1024):.1f} MB on disk"
