"""
V5 Worktree API routes.

Worktrees provide filesystem-level isolation for agents: each agent gets
its own working directory and git index while sharing the same repository
object store. V1 intentionally stays narrow and does not link worktrees to
claims or enrich the state brief.

Endpoints:
  POST   /api/v5/worktrees                     - create a git worktree
  GET    /api/v5/worktrees?space_id=...        - list worktrees for a space
  GET    /api/v5/worktrees/{worktree_id}       - show one worktree row
  DELETE /api/v5/worktrees/{worktree_id}       - close/remove a worktree
"""
from __future__ import annotations

import re
import shutil
import subprocess
import uuid
from datetime import UTC, datetime
from pathlib import Path

from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from sqlalchemy import String, cast, func, select
from sqlalchemy.orm import Session

from app.db.database import get_db
from app.db.models import CommitModel, RepoModel, WorkTree
from app.services.worktree_probe import _probe_worktree

router = APIRouter(prefix="/worktrees", tags=["worktrees-v5"])

DEMO_USER_ID = uuid.UUID("00000000-0000-0000-0000-00000000000a")
VALID_STATUSES = {"active", "closed"}


def _utcnow() -> datetime:
    return datetime.now(UTC)


def _slugify(value: str, fallback: str = "item") -> str:
    """Lowercase a user/project label into a filesystem/branch-safe slug."""
    slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
    return slug or fallback


def _default_branch_name(agent: str, suffix: str) -> str:
    return f"smriti/{_slugify(agent, fallback='agent')}/{suffix}"


def _default_worktree_path(space_name: str, agent: str, suffix: str) -> str:
    space_slug = _slugify(space_name, fallback="space")
    agent_slug = _slugify(agent, fallback="agent")
    return str(
        Path.home()
        / ".smriti"
        / "worktrees"
        / space_slug
        / f"{agent_slug}-{suffix}"
    )


def _run_git(
    args: list[str],
    *,
    cwd: str | Path | None = None,
    timeout: float = 30.0,
) -> subprocess.CompletedProcess[str]:
    """Run git with captured output and consistent infrastructure errors."""
    try:
        return subprocess.run(
            ["git", *args],
            cwd=str(cwd) if cwd is not None else None,
            capture_output=True,
            text=True,
            check=False,
            timeout=timeout,
        )
    except FileNotFoundError:
        raise HTTPException(status_code=500, detail="git executable not found")
    except subprocess.TimeoutExpired:
        raise HTTPException(status_code=500, detail="git command timed out")


def _get_repo(space_id: uuid.UUID, db: Session) -> RepoModel:
    repo = db.get(RepoModel, space_id)
    if not repo or repo.user_id != DEMO_USER_ID:
        raise HTTPException(status_code=404, detail="Space not found")
    return repo


def _resolve_worktree_id(id_or_prefix: str, db: Session) -> WorkTree:
    """Resolve a worktree by full UUID or unique short prefix."""
    value = id_or_prefix.strip()
    if len(value) < 4:
        raise HTTPException(
            status_code=400,
            detail="Worktree id must be at least 4 characters",
        )

    try:
        worktree_id = uuid.UUID(value)
    except ValueError:
        worktree_id = None

    if worktree_id is not None:
        worktree = db.get(WorkTree, worktree_id)
        if not worktree:
            raise HTTPException(status_code=404, detail="Worktree not found")
        return worktree

    prefix = value.lower()
    stmt = select(WorkTree).where(
        func.lower(cast(WorkTree.id, String)).like(f"{prefix}%")
    )
    matches = list(db.scalars(stmt).all())
    if not matches:
        raise HTTPException(
            status_code=404,
            detail=f"No worktree matches prefix '{value}'",
        )
    if len(matches) > 1:
        ids = ", ".join(f"{str(match.id)[:8]}..." for match in matches[:3])
        raise HTTPException(
            status_code=422,
            detail=f"Worktree prefix '{value}' is ambiguous (matches {ids})",
        )
    return matches[0]


def _validate_project_root(project_root: str) -> Path:
    resolved = Path(project_root).expanduser().resolve()
    if not resolved.is_dir():
        raise HTTPException(
            status_code=400,
            detail=f"project_root does not exist on disk: {resolved}",
        )
    return resolved


def _resolve_project_root(repo: RepoModel, db: Session) -> Path:
    """Resolve the worktree anchor path.

    Prefer the space-level canonical root. For existing spaces without one,
    fall back to the latest checkpoint root and lazily backfill the canonical
    field so future worktree opens are no longer agent-activity-dependent.
    """
    if repo.project_root:
        return _validate_project_root(repo.project_root)

    stmt = (
        select(CommitModel.project_root)
        .where(
            CommitModel.repo_id == repo.id,
            CommitModel.project_root.is_not(None),
            CommitModel.project_root != "",
        )
        .order_by(CommitModel.created_at.desc())
        .limit(1)
    )
    project_root = db.scalars(stmt).first()
    if not project_root:
        raise HTTPException(
            status_code=400,
            detail=(
                "No project_root configured for this space. Set it via "
                "`smriti space set-project-root <space> <path>` or create a "
                "checkpoint with project_root set."
            ),
        )

    resolved = _validate_project_root(project_root)
    repo.project_root = str(resolved)
    db.commit()
    db.refresh(repo)
    return resolved


def _current_head(project_root: Path) -> str:
    result = _run_git(["rev-parse", "HEAD"], cwd=project_root)
    if result.returncode != 0:
        detail = result.stderr.strip() or result.stdout.strip() or "could not resolve HEAD"
        raise HTTPException(status_code=500, detail=detail)
    return result.stdout.strip()


def _branch_exists(project_root: Path, branch_name: str) -> bool:
    result = _run_git(
        ["show-ref", "--verify", "--quiet", f"refs/heads/{branch_name}"],
        cwd=project_root,
    )
    if result.returncode == 0:
        return True
    if result.returncode == 1:
        return False
    detail = result.stderr.strip() or "could not check branch existence"
    raise HTTPException(status_code=500, detail=detail)


def _resolve_target_path(path_value: str) -> Path:
    target = Path(path_value).expanduser()
    if not target.is_absolute():
        raise HTTPException(status_code=400, detail="base_path must be absolute")
    return target.resolve()


def _git_error(result: subprocess.CompletedProcess[str], fallback: str) -> str:
    return result.stderr.strip() or result.stdout.strip() or fallback


# -- Request / Response schemas ----------------------------------------------


class CreateWorkTreeRequest(BaseModel):
    space_id: str
    agent: str
    branch_name: str | None = None
    base_commit_sha: str | None = None
    base_path: str | None = Field(
        default=None,
        description="Absolute target path for the new worktree.",
    )


class WorkTreeResponse(BaseModel):
    id: uuid.UUID
    repo_id: uuid.UUID
    agent: str
    path: str
    branch_name: str
    base_commit_sha: str | None = None
    status: str
    created_at: datetime
    closed_at: datetime | None = None

    model_config = {"from_attributes": True}


class WorkTreeProbe(BaseModel):
    dirty_files: int
    dirty_paths: list[str] = Field(default_factory=list)
    ahead: int
    behind: int
    last_commit_sha: str | None = None
    last_commit_relative: str | None = None


class WorkTreeListEntry(WorkTreeResponse):
    probe: WorkTreeProbe | None = None


# -- Endpoints ----------------------------------------------------------------


@router.post("", response_model=WorkTreeResponse, status_code=201)
def create_worktree(
    payload: CreateWorkTreeRequest,
    db: Session = Depends(get_db),
):
    """Create a git worktree and record it after git succeeds."""
    try:
        space_id = uuid.UUID(payload.space_id)
    except ValueError:
        raise HTTPException(status_code=400, detail="Invalid space_id")

    repo = _get_repo(space_id, db)
    agent = payload.agent.strip()
    if not agent:
        raise HTTPException(status_code=400, detail="agent must be non-empty")

    project_root = _resolve_project_root(repo, db)
    suffix = uuid.uuid4().hex[:8]
    branch_name = (payload.branch_name or "").strip() or _default_branch_name(agent, suffix)
    target_path = _resolve_target_path(
        payload.base_path or _default_worktree_path(repo.name, agent, suffix)
    )

    if _branch_exists(project_root, branch_name):
        raise HTTPException(
            status_code=409,
            detail=f"Branch already exists: {branch_name}",
        )

    base_commit_sha = (payload.base_commit_sha or "").strip() or _current_head(project_root)
    target_existed = target_path.exists()
    target_path.parent.mkdir(parents=True, exist_ok=True)

    result = _run_git(
        [
            "worktree",
            "add",
            "-b",
            branch_name,
            str(target_path),
            base_commit_sha,
        ],
        cwd=project_root,
    )
    if result.returncode != 0:
        if target_path.exists() and not target_existed:
            shutil.rmtree(target_path, ignore_errors=True)
        raise HTTPException(
            status_code=500,
            detail=_git_error(result, "git worktree add failed"),
        )

    worktree = WorkTree(
        repo_id=space_id,
        agent=agent,
        path=str(target_path),
        branch_name=branch_name,
        base_commit_sha=base_commit_sha,
        status="active",
    )
    db.add(worktree)
    db.commit()
    db.refresh(worktree)
    return worktree


@router.get("", response_model=list[WorkTreeListEntry])
def list_worktrees(
    space_id: uuid.UUID = Query(..., description="Space UUID"),
    include_closed: bool = Query(False, description="Include closed worktrees"),
    db: Session = Depends(get_db),
):
    """List worktrees for a space, newest first."""
    _get_repo(space_id, db)
    stmt = (
        select(WorkTree)
        .where(WorkTree.repo_id == space_id)
        .order_by(WorkTree.created_at.desc())
    )
    if not include_closed:
        stmt = stmt.where(WorkTree.status == "active")
    entries = []
    for worktree in db.scalars(stmt).all():
        probe = None
        if worktree.status == "active":
            probed = _probe_worktree(
                str(worktree.id),
                worktree.path,
                worktree.branch_name,
            )
            if probed:
                probe = WorkTreeProbe(**probed)
        entries.append(
            WorkTreeListEntry.model_validate(worktree).model_copy(
                update={"probe": probe},
            )
        )
    return entries


@router.get("/{worktree_id}", response_model=WorkTreeResponse)
def get_worktree(worktree_id: str, db: Session = Depends(get_db)):
    """Return one worktree row."""
    return _resolve_worktree_id(worktree_id, db)


@router.delete("/{worktree_id}", response_model=WorkTreeResponse)
def close_worktree(
    worktree_id: str,
    force: bool = Query(False, description="Force removal even if dirty"),
    db: Session = Depends(get_db),
):
    """Remove a git worktree and mark its row closed."""
    worktree = _resolve_worktree_id(worktree_id, db)
    if worktree.status == "closed":
        raise HTTPException(status_code=409, detail="Worktree is already closed")
    if worktree.status not in VALID_STATUSES:
        raise HTTPException(
            status_code=409,
            detail=f"Worktree has invalid status: {worktree.status}",
        )

    repo = _get_repo(worktree.repo_id, db)
    project_root = _resolve_project_root(repo, db)
    worktree_path = Path(worktree.path).expanduser().resolve()

    if not force:
        status = _run_git(["status", "--porcelain"], cwd=worktree_path)
        if status.returncode != 0:
            raise HTTPException(
                status_code=500,
                detail=_git_error(status, "git status failed"),
            )
        if status.stdout.strip():
            raise HTTPException(
                status_code=409,
                detail="Worktree has uncommitted changes; pass force=true to remove it.",
            )

    args = ["worktree", "remove"]
    if force:
        args.append("--force")
    args.append(str(worktree_path))
    result = _run_git(args, cwd=project_root)
    if result.returncode != 0:
        raise HTTPException(
            status_code=500,
            detail=_git_error(result, "git worktree remove failed"),
        )

    worktree.status = "closed"
    worktree.closed_at = _utcnow()
    db.commit()
    db.refresh(worktree)
    return worktree
