from __future__ import annotations

import asyncio
import logging
import os
import shutil
import subprocess
from pathlib import Path
from typing import Awaitable, Callable

from dotenv import load_dotenv
from prompt_auto_lab.config.picasso import PICASSO_REMOTE_URL, SPARSE_CHECKOUT_PATHS

from prompt_auto_lab.core.store_dir import get_store_dir

# Progress callback type: async function that takes (step, total_steps, message)
ProgressCallback = Callable[[int, int, str], Awaitable[None]]


def get_session_picasso_root() -> Path:
    """
    Get the root directory used for per-session Picasso clones.

    If AUTOPROMPTLAB_PICASSO_ROOT is set (e.g. by the VS Code extension setting
    "Picasso Session Root"), clones are created under that path to keep checkout
    paths short on Windows. Otherwise, defaults to the general store directory.
    """
    load_dotenv()
    value = os.getenv("AUTOPROMPTLAB_PICASSO_ROOT")
    return Path(value) if value else get_store_dir()


def _clone_step1_fetch_metadata(dest_path: Path, branch: str, remote_url: str) -> None:
    """Step 1: Clone with no checkout (fast - only fetches metadata)."""
    result = subprocess.run(
        [
            "git",
            "-c",
            "core.longpaths=true",
            "clone",
            "--depth",
            "1",
            "--branch",
            branch,
            "--no-checkout",
            "--filter=blob:none",
            remote_url,
            str(dest_path),
        ],
        timeout=120,
    )
    if result.returncode != 0:
        raise RuntimeError(
            f"Failed to clone repo (branch: {branch}), exit code: {result.returncode}"
        )


def _clone_step2_init_sparse(dest_path: Path) -> None:
    """Step 2: Enable sparse checkout."""
    result = subprocess.run(
        ["git", "sparse-checkout", "init", "--cone"],
        cwd=dest_path,
        timeout=30,
    )
    if result.returncode != 0:
        raise RuntimeError(
            f"Failed to init sparse-checkout, exit code: {result.returncode}"
        )


def _clone_step3_set_paths(dest_path: Path) -> None:
    """Step 3: Set sparse checkout paths."""
    result = subprocess.run(
        ["git", "sparse-checkout", "set"] + SPARSE_CHECKOUT_PATHS,
        cwd=dest_path,
        timeout=30,
    )
    if result.returncode != 0:
        raise RuntimeError(
            f"Failed to set sparse-checkout paths, exit code: {result.returncode}"
        )


def _clone_step4_checkout(dest_path: Path) -> None:
    """Step 4: Checkout (only fetches blobs for sparse paths)."""
    result = subprocess.run(
        ["git", "-c", "core.longpaths=true", "checkout"],
        cwd=dest_path,
        timeout=300,
    )
    if result.returncode != 0:
        raise RuntimeError(f"Failed to checkout, exit code: {result.returncode}")


async def copy_picasso_repo(
    dest_path: Path,
    branch: str = "main",
    progress_callback: ProgressCallback | None = None,
) -> None:
    """
    Shallow clone the Picasso repo to destination with sparse checkout.
    Runs git clone steps in a thread pool to avoid blocking the event loop.

    Clones from the hardcoded remote URL (PICASSO_REMOTE_URL) — no local
    Picasso repo is required.

    Args:
        dest_path: Destination path for the cloned repo
        branch: Branch to clone (from SessionConfig)
        progress_callback: Optional async callback for progress updates

    Raises:
        FileExistsError: If destination already exists
    """
    if dest_path.exists():
        raise FileExistsError(f"Destination already exists: {dest_path}")

    dest_path.parent.mkdir(parents=True, exist_ok=True)

    total_steps = 4

    if progress_callback:
        await progress_callback(1, total_steps, "Fetching repository metadata...")
    await asyncio.to_thread(_clone_step1_fetch_metadata, dest_path, branch, PICASSO_REMOTE_URL)

    if progress_callback:
        await progress_callback(2, total_steps, "Initializing sparse checkout...")
    await asyncio.to_thread(_clone_step2_init_sparse, dest_path)

    if progress_callback:
        await progress_callback(3, total_steps, "Configuring template paths...")
    await asyncio.to_thread(_clone_step3_set_paths, dest_path)

    if progress_callback:
        await progress_callback(4, total_steps, "Checking out template files...")
    await asyncio.to_thread(_clone_step4_checkout, dest_path)


logger = logging.getLogger(__name__)


def make_session_branch_name(session_id: str) -> str:
    """Derive a deterministic session branch name from a session ID.

    Uses the UUID portion (first 8 chars after the timestamp prefix) so that
    branches created within the same second don't collide.
    """
    parts = session_id.split("_", 1)
    suffix = parts[1][:8] if len(parts) > 1 else session_id[:8]
    return f"autopromptlab/session/{suffix}"


async def create_session_branch(
    repo_path: Path,
    session_id: str,
    user_branch: str,
) -> str:
    """Create an isolated session branch and push it to remote.

    Args:
        repo_path: Path to the cloned Picasso repo.
        session_id: The session identifier (used for branch naming).
        user_branch: The user's original branch (for rollback on failure).

    Returns:
        The session branch name on success.

    Raises:
        RuntimeError: If branch creation or push fails.
    """
    session_branch = make_session_branch_name(session_id)

    try:
        result = await asyncio.to_thread(
            subprocess.run,
            ["git", "checkout", "-b", session_branch],
            cwd=str(repo_path),
            capture_output=True, text=True, timeout=30,
        )
        if result.returncode != 0:
            raise RuntimeError(f"git checkout -b failed: {result.stderr}")

        result = await asyncio.to_thread(
            subprocess.run,
            ["git", "push", "-u", "origin", session_branch],
            cwd=str(repo_path),
            capture_output=True, text=True, timeout=60,
        )
        if result.returncode != 0:
            raise RuntimeError(f"git push failed: {result.stderr}")

        logger.info("Session branch created: %s from %s", session_branch, user_branch)
        return session_branch
    except Exception:
        # Attempt to restore the original branch before re-raising
        try:
            await asyncio.to_thread(
                subprocess.run,
                ["git", "checkout", user_branch],
                cwd=str(repo_path),
                capture_output=True, text=True, timeout=30,
            )
        except Exception:
            pass
        raise


def delete_picasso_repo(repo_path: Path) -> None:
    """Delete a Picasso repo copy."""
    if repo_path.exists():
        shutil.rmtree(repo_path)


def get_session_picasso_path(user_id: str, session_id: str) -> Path:
    """Get the Picasso repo path for a session."""
    return get_session_picasso_root() / "users" / user_id / "sessions" / session_id / "picasso"


def resolve_working_picasso_path(
    *,
    user_id: str | None = None,
    session_id: str,
) -> Path:
    """
    Resolve the effective Picasso working copy path for a session.
    """
    normalized_user_id = str(user_id or "sdk-user").strip() or "sdk-user"
    return get_session_picasso_path(normalized_user_id, str(session_id))
