"""DMS repositories — service layer.

All mutation logic for Repository and Folder lives here.
Views must not bypass this layer.

Public surface
--------------
Repositories
  create_repository   — validate slug + create
  update_repository   — rename / redescribe
  delete_repository   — soft delete (cascades to folders via DB CASCADE)

Folders
  create_folder       — validate name uniqueness + compute materialized path
  rename_folder       — update name + invalidate full_path cache if any
  move_folder         — update path + depth for folder and all descendants
  delete_folder       — soft delete folder + all living descendants
"""

from __future__ import annotations

import uuid as _uuid

from django.db import transaction
from django.utils.translation import gettext_lazy as _

from simorgh.apps.dms.repositories.models import Folder, Repository


# ---------------------------------------------------------------------------
# Custom exceptions (domain-level, raised to views)
# ---------------------------------------------------------------------------

class RepositoryError(Exception):
    """Base error for repository operations."""


class FolderError(Exception):
    """Base error for folder operations."""


# ---------------------------------------------------------------------------
# Repository services
# ---------------------------------------------------------------------------

def create_repository(
    *,
    name: str,
    slug: str,
    tenant_id: int,
    organization_node_id: int,
    description: str = "",
    workspace_id: int | None = None,
    is_default: bool = False,
    max_size_bytes: int | None = None,
) -> Repository:
    """Create and return a new Repository.

    Raises ``RepositoryError`` if the slug is already taken by a live
    repository in the same tenant.
    """
    slug = slug.strip().lower()
    if Repository.objects.filter(tenant_id=tenant_id, slug=slug, is_deleted=False).exists():
        raise RepositoryError(
            _("A repository with slug '%(slug)s' already exists.") % {"slug": slug}
        )

    return Repository.objects.create(
        name=name.strip(),
        slug=slug,
        description=description,
        tenant_id=tenant_id,
        organization_node_id=organization_node_id,
        workspace_id=workspace_id,
        is_default=is_default,
        max_size_bytes=max_size_bytes,
    )


def update_repository(
    repo: Repository,
    *,
    name: str | None = None,
    description: str | None = None,
    max_size_bytes: int | None = ...,  # type: ignore[assignment]
) -> Repository:
    """Update mutable fields on a Repository.

    Pass ``max_size_bytes=None`` explicitly to clear the quota.
    Omit the argument (leave as ``...``) to leave it unchanged.
    """
    update_fields: list[str] = ["updated_at"]

    if name is not None:
        repo.name = name.strip()
        update_fields.append("name")

    if description is not None:
        repo.description = description
        update_fields.append("description")

    if max_size_bytes is not ...:  # type: ignore[comparison-overlap]
        repo.max_size_bytes = max_size_bytes
        update_fields.append("max_size_bytes")

    repo.save(update_fields=update_fields)
    return repo


def delete_repository(repo: Repository) -> None:
    """Soft-delete the repository.

    Folder rows are CASCADE-deleted at the DB level when the repository is
    hard-deleted; for soft delete we rely on the DB cascade only if the
    caller later hard-deletes.  For now we also soft-delete all live folders
    to keep query behaviour consistent.
    """
    with transaction.atomic():
        # Soft-delete all live folders in this repository.
        Folder.objects.filter(repository=repo, is_deleted=False).update(is_deleted=True)
        repo.delete()


# ---------------------------------------------------------------------------
# Folder services
# ---------------------------------------------------------------------------

def _assert_unique_name(
    repository: Repository,
    name: str,
    parent: Folder | None,
    exclude_pk: int | None = None,
) -> None:
    """Raise FolderError if a live folder with the same name already exists."""
    qs = Folder.objects.filter(
        repository=repository,
        name=name,
        is_deleted=False,
    )
    if parent is None:
        qs = qs.filter(parent__isnull=True)
    else:
        qs = qs.filter(parent=parent)

    if exclude_pk is not None:
        qs = qs.exclude(pk=exclude_pk)

    if qs.exists():
        location = (
            _("under '%(parent)s'") % {"parent": parent.name}
            if parent
            else _("at root")
        )
        raise FolderError(
            _("A folder named '%(name)s' already exists %(location)s.") % {
                "name": name, "location": location
            }
        )


def create_folder(
    *,
    repository: Repository,
    name: str,
    parent: Folder | None = None,
    tenant_id: int,
    organization_node_id: int,
    sort_order: int = 0,
    color: str = "",
    icon: str = "",
    is_system: bool = False,
) -> Folder:
    """Create and return a new Folder inside ``repository``.

    * Validates name uniqueness within the target parent.
    * Pre-computes ``materialized_path`` and ``depth`` before the INSERT so
      no second UPDATE is needed.
    """
    name = name.strip()
    _assert_unique_name(repository, name, parent)

    # Pre-generate the public_id so we can compute the materialized path.
    new_uid = _uuid.uuid4()

    if parent is None:
        materialized_path = str(new_uid)
        depth = 0
    else:
        materialized_path = parent.materialized_path + "/" + str(new_uid)
        depth = parent.depth + 1

    return Folder.objects.create(
        public_id=new_uid,
        repository=repository,
        parent=parent,
        name=name,
        materialized_path=materialized_path,
        depth=depth,
        sort_order=sort_order,
        color=color,
        icon=icon,
        is_system=is_system,
        tenant_id=tenant_id,
        organization_node_id=organization_node_id,
    )


def rename_folder(folder: Folder, *, new_name: str) -> Folder:
    """Rename a folder.

    Validates uniqueness within the same parent before saving.
    Does NOT update ``materialized_path`` (which is UUID-based and
    independent of names).
    """
    new_name = new_name.strip()
    if new_name == folder.name:
        return folder

    _assert_unique_name(folder.repository, new_name, folder.parent, exclude_pk=folder.pk)
    folder.name = new_name
    folder.save(update_fields=["name", "updated_at"])
    return folder


def move_folder(folder: Folder, *, new_parent: Folder | None) -> Folder:
    """Move ``folder`` (and all its descendants) under ``new_parent``.

    Raises ``FolderError`` on cycle (moving into own descendant) or if a
    folder with the same name already exists at the target location.

    All descendant ``materialized_path`` and ``depth`` values are updated
    atomically inside a transaction.
    """
    # No-op if parent is unchanged.
    if (new_parent is None and folder.parent_id is None) or (
        new_parent is not None and folder.parent_id == new_parent.pk
    ):
        return folder

    # Cycle check: new_parent must not be the folder itself or a descendant.
    if new_parent is not None:
        if new_parent.pk == folder.pk:
            raise FolderError("Cannot move a folder into itself.")
        if new_parent.materialized_path.startswith(folder.materialized_path + "/"):
            raise FolderError(
                "Cannot move a folder into one of its own descendants."
            )
        if new_parent.repository_id != folder.repository_id:
            raise FolderError("Cannot move a folder across repositories.")

    _assert_unique_name(folder.repository, folder.name, new_parent, exclude_pk=folder.pk)

    old_path = folder.materialized_path

    if new_parent is None:
        new_path = str(folder.public_id)
        new_depth_base = 0
    else:
        new_path = new_parent.materialized_path + "/" + str(folder.public_id)
        new_depth_base = new_parent.depth + 1

    depth_diff = new_depth_base - folder.depth

    with transaction.atomic():
        # Update all live descendants first (path prefix replacement).
        descendants = Folder.objects.filter(
            repository=folder.repository,
            materialized_path__startswith=old_path + "/",
            is_deleted=False,
        )
        for desc in descendants:
            desc.materialized_path = new_path + desc.materialized_path[len(old_path):]
            desc.depth = desc.depth + depth_diff
            desc.save(update_fields=["materialized_path", "depth", "updated_at"])

        folder.parent = new_parent
        folder.materialized_path = new_path
        folder.depth = new_depth_base
        folder.save(update_fields=["parent", "materialized_path", "depth", "updated_at"])

    return folder


def delete_folder(folder: Folder) -> None:
    """Soft-delete the folder and all its living descendants."""
    with transaction.atomic():
        # Descendants (not including the folder itself)
        Folder.objects.filter(
            repository=folder.repository,
            materialized_path__startswith=folder.materialized_path + "/",
            is_deleted=False,
        ).update(is_deleted=True)
        folder.delete()
