"""Database-backed lifecycle operations for published workspace artifacts."""
from __future__ import annotations

import hashlib
import mimetypes
import os
import shutil
from datetime import datetime, timezone
from pathlib import Path
from uuid import UUID, uuid4

from sqlalchemy import or_, select
from sqlalchemy.dialects.postgresql import insert as pg_insert

from .artifacts import ARTIFACT_TRASH_DIR, ArtifactRef
from .storage import session_scope
from .storage.models import Artifact


def _rel(root: Path, path: Path) -> str:
    return Path(path).resolve().relative_to(Path(root).resolve()).as_posix()


def _hash_file(path: Path) -> str:
    digest = hashlib.sha256()
    with Path(path).open("rb") as stream:
        for chunk in iter(lambda: stream.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def artifact_subtree_clause(path: str):
    """精确匹配文件/目录自身及其子树，并转义 LIKE 元字符。"""
    escaped = path.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
    return or_(
        Artifact.current_path == path,
        Artifact.current_path.like(escaped + "/%", escape="\\"),
    )


def register_published_artifacts(
    *,
    user_id: UUID,
    task_id: UUID,
    user_root: Path,
    working_dir: Path,
    refs: tuple[dict, ...],
    preserve_existing_labels: bool = False,
    software_job_id: UUID | None = None,
) -> tuple[dict, ...]:
    """Upsert active artifact identities and return version-2 message refs."""
    root = Path(user_root).resolve()
    wd = Path(working_dir).resolve()
    output: list[dict] = []
    with session_scope() as session:
        for ref in refs:
            task_path = str(ref.get("path") or "")
            path = (wd / Path(task_path)).resolve()
            path.relative_to(wd)
            if not path.is_file():
                continue
            current_path = _rel(root, path)
            label = str(ref.get("label") or "")
            media_type = str(ref.get("media_type") or "") or mimetypes.guess_type(path.name)[0]
            size_bytes = path.stat().st_size
            content_sha256 = _hash_file(path)
            update_values = {
                "media_type": media_type,
                "size_bytes": size_bytes,
                "content_sha256": content_sha256,
                "updated_at": datetime.now(timezone.utc),
            }
            if not preserve_existing_labels:
                update_values["label"] = label
            if software_job_id is not None:
                update_values["software_job_id"] = software_job_id
            statement = pg_insert(Artifact).values(
                user_id=user_id,
                origin_task_id=task_id,
                software_job_id=software_job_id,
                current_path=current_path,
                label=label,
                media_type=media_type,
                size_bytes=size_bytes,
                content_sha256=content_sha256,
            ).on_conflict_do_update(
                index_elements=[Artifact.user_id, Artifact.current_path],
                index_where=Artifact.status == "active",
                set_=update_values,
            ).returning(Artifact.artifact_id)
            artifact_id = session.execute(statement).scalar_one()
            output.append(ArtifactRef(
                path=task_path,
                label=label,
                artifact_id=artifact_id,
                version=2,
            ).as_dict())
    return tuple(output)


def register_workspace_artifact(
    *,
    user_id: UUID,
    task_id: UUID,
    user_root: Path,
    working_dir: Path,
    path: str,
    label: str = "",
) -> dict:
    """Register one task-relative file for an internal platform workflow.

    Unlike ``publish_artifacts``, this only creates or refreshes the stable artifact
    identity. It does not attach the file to an assistant message or present it as a
    final deliverable.
    """
    refs = register_published_artifacts(
        user_id=user_id,
        task_id=task_id,
        user_root=user_root,
        working_dir=working_dir,
        refs=({"path": path, "label": label},),
        preserve_existing_labels=True,
    )
    if len(refs) != 1 or not refs[0].get("artifact_id"):
        raise ValueError("workspace artifact could not be registered")
    return refs[0]


def rename_active_artifacts(
    *,
    user_id: UUID,
    user_root: Path,
    old_path: Path,
    new_path: Path,
) -> int:
    """Rewrite active artifact paths for one file or directory subtree."""
    root = Path(user_root).resolve()
    old_rel = _rel(root, old_path)
    new_rel = _rel(root, new_path)
    with session_scope() as session:
        rows = session.execute(
            select(Artifact).where(
                Artifact.user_id == user_id,
                Artifact.status == "active",
                artifact_subtree_clause(old_rel),
            )
        ).scalars().all()
        changed = 0
        for row in rows:
            if row.current_path == old_rel:
                suffix = ""
            elif row.current_path.startswith(old_rel + "/"):
                suffix = row.current_path[len(old_rel):]
            else:
                continue
            row.current_path = new_rel + suffix
            changed += 1
        return changed


def copy_active_artifacts(
    *,
    user_id: UUID,
    user_root: Path,
    source: Path,
    target: Path,
) -> int:
    """Create independent artifact identities for copied files."""
    root = Path(user_root).resolve()
    source_rel = _rel(root, source)
    target_rel = _rel(root, target)
    with session_scope() as session:
        sources = session.execute(
            select(Artifact).where(
                Artifact.user_id == user_id,
                Artifact.status == "active",
                artifact_subtree_clause(source_rel),
            )
        ).scalars().all()
        created = 0
        for original in sources:
            if original.current_path == source_rel:
                suffix = ""
            elif original.current_path.startswith(source_rel + "/"):
                suffix = original.current_path[len(source_rel):]
            else:
                continue
            copied_path = target_rel + suffix
            session.add(Artifact(
                user_id=user_id,
                origin_task_id=original.origin_task_id,
                copied_from_artifact_id=original.artifact_id,
                software_job_id=original.software_job_id,
                current_path=copied_path,
                label=(original.label + " 副本").strip(),
                media_type=original.media_type,
                size_bytes=original.size_bytes,
                content_sha256=original.content_sha256,
            ))
            created += 1
        return created


def trash_active_artifacts(
    *,
    user_id: UUID,
    user_root: Path,
    target: Path,
) -> int:
    """Move matching active artifacts to hidden trash and mark them deleted."""
    root = Path(user_root).resolve()
    source = Path(target).resolve()
    source_rel = _rel(root, source)
    entry = (
        root / ARTIFACT_TRASH_DIR
        / datetime.now(timezone.utc).strftime("%Y/%m/%d")
        / uuid4().hex
    )
    moved: list[tuple[Path, Path]] = []
    try:
        with session_scope() as session:
            rows = session.execute(
                select(Artifact).where(
                    Artifact.user_id == user_id,
                    Artifact.status == "active",
                    artifact_subtree_clause(source_rel),
                ).with_for_update()
            ).scalars().all()
            matches = [
                row for row in rows
                if row.current_path == source_rel
                or row.current_path.startswith(source_rel + "/")
            ]
            if not matches:
                return 0
            for row in matches:
                original = root / Path(row.current_path)
                if not original.is_file():
                    continue
                destination = entry / "files" / Path(row.current_path)
                destination.parent.mkdir(parents=True, exist_ok=True)
                os.replace(original, destination)
                moved.append((original, destination))
                row.status = "deleted"
                row.deleted_at = datetime.now(timezone.utc)
                row.trash_path = _rel(root, destination)
        return len(moved)
    except Exception:
        for original, destination in reversed(moved):
            try:
                original.parent.mkdir(parents=True, exist_ok=True)
                os.replace(destination, original)
            except OSError:
                pass
        shutil.rmtree(entry, ignore_errors=True)
        raise
