"""working_dir 的 DB-aware 文件系统变更。

网页 files API 与对话内延迟动作共用这里，保证顶层工作目录改名始终同时更新所有
关联 task，且不会与活跃 run 或 no-subtask 约束打架。
"""
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from uuid import UUID

from sqlalchemy import select, update

from .artifact_lifecycle import artifact_subtree_clause
from .paths import to_db_path
from .storage import NoSubtaskError, check_no_subtask, session_scope
from .storage.models import Artifact, Task


class WorkingDirRenameError(RuntimeError):
    """working_dir 无法安全改名。"""


class WorkingDirConflictError(WorkingDirRenameError):
    """目录状态或 task 约束冲突，可由用户调整后重试。"""


class WorkingDirBusyError(WorkingDirConflictError):
    """至少一个关联 task 仍有活跃 run。"""

    def __init__(self, task_ids: list[str]) -> None:
        self.task_ids = task_ids
        super().__init__(
            f"folder has active run(s) on task(s) {task_ids}; cancel before renaming"
        )


@dataclass(frozen=True)
class WorkingDirRenameResult:
    old_path: Path
    new_path: Path
    tasks_updated: int


def rename_working_dir(
    *,
    user_id: UUID,
    old_path: Path,
    new_path: Path,
) -> WorkingDirRenameResult:
    """原子地平移一个顶层 working_dir 的 DB 引用并重命名目录。

    调用方负责用户边界与 leaf 名校验；本函数仍校验同级目录、源/目标状态和并发
    task，作为网页路由与延迟动作共用的最后一道一致性闸。

    顺序为 DB UPDATE → FS rename → transaction commit。FS 失败会回滚 DB；仍存在
    极小的“FS 已成功但 PG commit 失败”窗口，与原 files API 的既有语义一致。
    """
    old = Path(old_path).resolve()
    new = Path(new_path).resolve()
    if old.parent != new.parent:
        raise WorkingDirRenameError("working_dir rename must stay under the same parent")
    if old == new:
        raise WorkingDirRenameError("new working_dir equals the old path")
    if not old.exists():
        raise WorkingDirRenameError(f"working_dir not found: {old}")
    if not old.is_dir():
        raise WorkingDirRenameError(f"working_dir is not a directory: {old}")
    if new.exists():
        raise WorkingDirConflictError(f"target already exists: {new}")

    old_db = to_db_path(old)
    new_db = to_db_path(new)
    with session_scope() as s:
        rows = s.execute(
            select(Task.task_id, Task.run_status)
            .where(Task.user_id == user_id, Task.working_dir == old_db)
            .with_for_update()
        ).all()
        tids = [r.task_id for r in rows]
        active = [
            str(r.task_id)[:8]
            for r in rows
            if r.run_status in ("running", "cancelling")
        ]
        if active:
            raise WorkingDirBusyError(active)
        try:
            check_no_subtask(new_db, user_id=user_id, exclude_task_ids=tids)
        except NoSubtaskError as e:
            raise WorkingDirConflictError(str(e)) from e
        if tids:
            s.execute(
                update(Task)
                .where(Task.task_id.in_(tids))
                .values(working_dir=new_db)
            )
        old_rel = old.relative_to(old.parent).as_posix()
        new_rel = new.relative_to(new.parent).as_posix()
        artifacts = s.execute(
            select(Artifact).where(
                Artifact.user_id == user_id,
                Artifact.status == "active",
                artifact_subtree_clause(old_rel),
            )
        ).scalars().all()
        for artifact in artifacts:
            if artifact.current_path == old_rel:
                suffix = ""
            elif artifact.current_path.startswith(old_rel + "/"):
                suffix = artifact.current_path[len(old_rel):]
            else:
                continue
            artifact.current_path = new_rel + suffix
        try:
            old.rename(new)
        except OSError as e:
            raise WorkingDirRenameError(f"FS rename failed: {e}") from e

    return WorkingDirRenameResult(
        old_path=old,
        new_path=new,
        tasks_updated=len(tids),
    )
