"""Persistence domain service.

StudioDomainService wraps StudioStore with business logic, validation, and invariants.
All methods accept and return Pydantic models — never raw dicts.
"""

from __future__ import annotations

import logging
import uuid
from typing import TYPE_CHECKING, Any

from persistence.errors import ConflictError, NotFoundError, ValidationError
from persistence.models import (
    DocType,
    Document,
    Folder,
    Project,
    Revision,
    RunEvent,
    RunSummary,
    TreeChildren,
    TreeDocumentNode,
    TreeFolderNode,
    Workspace,
    WorkspaceTree,
)

if TYPE_CHECKING:
    from persistence.store import StudioStore

logger = logging.getLogger(__name__)


class StudioDomainService:
    """Domain service with validation and business logic.

    Wraps StudioStore protocol with:
    - Input validation
    - Business invariants (uniqueness, parent chains, etc.)
    - All methods use Pydantic models end-to-end
    """

    def __init__(self, *, store: StudioStore):
        self._store = store

    # =========================================================================
    # WORKSPACES
    # =========================================================================

    def create_workspace(self, *, name: str, metadata: dict[str, Any] | None = None) -> Workspace:
        ws_id = f"ws_{uuid.uuid4().hex[:12]}"
        ws = Workspace(id=ws_id, name=self._assert_non_empty_name(name), metadata=metadata or {})
        return self._store.upsert_workspace(ws)

    def list_workspaces(self) -> list[Workspace]:
        return self._store.list_workspaces()

    # =========================================================================
    # PROJECTS
    # =========================================================================

    def create_project(
        self,
        *,
        workspace_id: str,
        name: str,
        metadata: dict[str, Any] | None = None,
    ) -> Project:
        self._assert_workspace_exists(workspace_id)
        validated_name = self._assert_non_empty_name(name)
        self._assert_unique_name(
            workspace_id=workspace_id, parent_id=None, kind="folder", name=validated_name,
        )

        project_id = f"prj_{uuid.uuid4().hex[:12]}"
        folder_id = f"fld_{uuid.uuid4().hex[:12]}"
        folder_meta = {"kind": "project", **(metadata or {})}

        self._store.upsert_folder(Folder(
            id=folder_id,
            workspace_id=workspace_id,
            project_id=project_id,
            name=validated_name,
            parent_id=None,
            metadata=folder_meta,
        ))

        project = Project(
            id=project_id,
            workspace_id=workspace_id,
            name=validated_name,
            root_folder_id=folder_id,
            metadata=metadata or {},
        )
        return self._store.upsert_project(project)

    def get_project(self, *, project_id: str) -> Project:
        proj = self._store.get_project(project_id)
        if not proj:
            raise NotFoundError(f"project not found: {project_id}")
        return proj

    def list_projects(self, *, workspace_id: str) -> list[Project]:
        self._assert_workspace_exists(workspace_id)
        return self._store.list_projects(workspace_id=workspace_id)

    # =========================================================================
    # FOLDERS
    # =========================================================================

    def create_folder(
        self,
        *,
        workspace_id: str,
        name: str,
        parent_id: str | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> Folder:
        self._assert_workspace_exists(workspace_id)
        parent_id = self._normalize_parent_id(workspace_id=workspace_id, parent_id=parent_id)
        self._assert_parent_folder(workspace_id=workspace_id, parent_id=parent_id)
        self._assert_unique_name(
            workspace_id=workspace_id, parent_id=parent_id, kind="folder", name=name,
        )

        folder_id = f"fld_{uuid.uuid4().hex[:12]}"
        project_id: str | None = None
        try:
            if parent_id is not None:
                parent = self._store.get_folder(parent_id)
                if parent and parent.workspace_id == workspace_id:
                    project_id = parent.project_id or None
        except Exception:
            logger.debug("Failed to resolve project_id for new folder", exc_info=True)
            project_id = None

        folder = Folder(
            id=folder_id,
            workspace_id=workspace_id,
            project_id=project_id,
            name=str(name).strip(),
            parent_id=parent_id,
            metadata=metadata or {},
        )
        return self._store.upsert_folder(folder)

    def list_children(
        self, *, workspace_id: str, parent_id: str | None = None,
    ) -> tuple[list[Folder], list[Document]]:
        """Return child folders and documents under a parent."""
        folders = self._store.list_folders(workspace_id=workspace_id, parent_id=parent_id)
        docs = self._store.list_documents(workspace_id=workspace_id, parent_id=parent_id)
        return folders, docs

    # =========================================================================
    # DOCUMENTS
    # =========================================================================

    def create_document(
        self,
        *,
        doc_id: str | None = None,
        doc_type: str,
        name: str,
        parent_id: str | None = None,
        workspace_id: str,
        metadata: dict[str, Any] | None = None,
    ) -> Document:
        try:
            dtype = DocType(str(doc_type))
        except Exception:
            raise ValidationError(
                "type must be 'pathway', 'agent', 'artifact', 'text', or 'asset'"
            )

        self._assert_workspace_exists(str(workspace_id))
        parent_id = self._normalize_parent_id(workspace_id=str(workspace_id), parent_id=parent_id)
        self._assert_parent_folder(workspace_id=str(workspace_id), parent_id=parent_id)

        project_id: str | None = None
        try:
            if parent_id is not None:
                parent = self._store.get_folder(parent_id)
                if parent and parent.workspace_id == str(workspace_id):
                    project_id = parent.project_id or None
        except Exception:
            logger.debug("Failed to resolve project_id for new document", exc_info=True)
            project_id = None

        validated_name = self._assert_non_empty_name(name)

        if doc_id is not None:
            did = str(doc_id).strip()
            if not did:
                raise ValidationError("document.id must be non-empty when provided")
            if not did.startswith("doc_"):
                raise ValidationError("document.id must start with 'doc_'")

            existing = self._store.get_document(did)
            if existing:
                if (
                    existing.id == did
                    and (existing.workspace_id or "") == str(workspace_id)
                    and (existing.parent_id or "") == str(parent_id or "")
                    and existing.type.value == dtype.value
                    and existing.name == validated_name
                    and existing.metadata == (metadata or {})
                ):
                    return existing
                raise ConflictError(
                    "document id already exists", doc_id=did, conflict_code="id_exists",
                )
            doc_id = did
        else:
            doc_id = f"doc_{uuid.uuid4().hex[:12]}"

        self._assert_unique_name(
            workspace_id=str(workspace_id),
            parent_id=parent_id,
            kind="document",
            name=name,
            exclude_id=None,
        )

        doc = Document(
            id=doc_id,
            type=dtype,
            name=validated_name,
            parent_id=parent_id,
            workspace_id=str(workspace_id),
            project_id=project_id,
            metadata=metadata or {},
            head_rev=None,
        )
        return self._store.upsert_document(doc)

    def get_document(self, *, doc_id: str) -> Document:
        doc = self._store.get_document(doc_id)
        if not doc:
            raise NotFoundError(f"document not found: {doc_id}")
        return doc

    def save_revision(
        self, *, doc_id: str, content: dict[str, Any], rev_id: str | None = None,
    ) -> Revision:
        doc = self.get_document(doc_id=doc_id)
        rid = rev_id or f"rev_{uuid.uuid4().hex[:12]}"
        rev = self._store.write_revision(doc_id=doc_id, rev_id=rid, content=content)
        # Update head_rev on the document
        updated_doc = doc.model_copy(update={"head_rev": rid})
        self._store.upsert_document(updated_doc)
        return rev

    def get_head_content(self, *, doc_id: str) -> dict[str, Any]:
        doc = self.get_document(doc_id=doc_id)
        head = doc.head_rev
        if not head:
            raise NotFoundError(f"head revision not set for document: {doc_id}")
        rev = self._store.get_revision(doc_id=doc_id, rev_id=str(head))
        if not rev:
            raise NotFoundError(f"revision not found: {head}")
        if not isinstance(rev.content, dict):
            raise ValidationError("revision content is invalid")
        return rev.content

    def get_revision_content(self, *, doc_id: str, rev_id: str) -> dict[str, Any]:
        rev = self._store.get_revision(doc_id=doc_id, rev_id=str(rev_id))
        if not rev:
            raise NotFoundError(f"revision not found: {rev_id}")
        if not isinstance(rev.content, dict):
            raise ValidationError("revision content is invalid")
        return rev.content

    # =========================================================================
    # TREE
    # =========================================================================

    def get_tree(self, *, workspace_id: str) -> WorkspaceTree:
        ws = self._assert_workspace_exists(workspace_id)

        def build(parent_id: str | None) -> TreeChildren:
            folders, docs = self.list_children(workspace_id=workspace_id, parent_id=parent_id)
            folder_nodes = [
                TreeFolderNode(
                    id=f.id,
                    name=f.name,
                    metadata=f.metadata,
                    children=build(f.id),
                )
                for f in folders
            ]
            doc_nodes = [
                TreeDocumentNode(
                    id=d.id,
                    name=d.name,
                    doc_type=d.type,
                    metadata=d.metadata,
                )
                for d in docs
            ]
            return TreeChildren(folders=folder_nodes, documents=doc_nodes)

        return WorkspaceTree(workspace=ws, tree=build(None))

    # =========================================================================
    # RUNS
    # =========================================================================

    def upsert_run_summary(self, *, summary: RunSummary) -> RunSummary:
        return self._store.upsert_run_summary(summary=summary)

    def get_run_summary(self, *, run_id: str) -> RunSummary | None:
        return self._store.get_run_summary(run_id=str(run_id))

    def list_run_summaries(
        self,
        *,
        workspace_id: str | None = None,
        doc_id: str | None = None,
        limit: int = 50,
    ) -> list[RunSummary]:
        return self._store.list_run_summaries(
            workspace_id=str(workspace_id) if workspace_id else None,
            doc_id=str(doc_id) if doc_id else None,
            limit=int(limit),
        )

    def append_run_event(self, *, event: RunEvent) -> RunEvent:
        return self._store.append_run_event(event=event)

    def list_run_events(
        self,
        *,
        run_id: str,
        after_seq: int | None = None,
        limit: int = 500,
    ) -> list[RunEvent]:
        return self._store.list_run_events(
            run_id=str(run_id),
            after_seq=int(after_seq) if after_seq is not None else None,
            limit=int(limit),
        )

    # =========================================================================
    # INVARIANTS & HELPERS
    # =========================================================================

    def _assert_non_empty_name(self, name: str) -> str:
        n = name.strip()
        if not n:
            raise ValidationError("name must be non-empty")
        return n

    def _assert_workspace_exists(self, workspace_id: str) -> Workspace:
        ws = self._store.get_workspace(workspace_id)
        if not ws:
            raise NotFoundError(f"workspace not found: {workspace_id}")
        return ws

    def _normalize_parent_id(self, *, workspace_id: str, parent_id: str | None) -> str | None:
        """Accept either folder id or project id as parent_id."""
        pid = parent_id.strip() if parent_id else None
        if pid is None:
            return None
        folder = self._store.get_folder(pid)
        if folder:
            return pid
        # Maybe it's a project ID — resolve to root folder
        if pid.startswith("prj_"):
            prj = self._store.get_project(pid)
            if prj and prj.workspace_id == str(workspace_id):
                root = prj.root_folder_id
                if root:
                    return root
        return pid

    def _assert_parent_folder(self, *, workspace_id: str, parent_id: str | None) -> None:
        if parent_id is None:
            return
        parent = self._store.get_folder(parent_id)
        if not parent:
            raise NotFoundError(f"parent folder not found: {parent_id}")
        if parent.workspace_id != workspace_id:
            raise ValidationError("parent folder workspace mismatch")

    def _assert_unique_name(
        self,
        *,
        workspace_id: str,
        parent_id: str | None,
        kind: str,
        name: str,
        exclude_id: str | None = None,
    ) -> None:
        """Enforce unique names among siblings."""
        n = self._assert_non_empty_name(name)
        if kind == "folder":
            siblings = self._store.list_folders(workspace_id=workspace_id, parent_id=parent_id)
            for s in siblings:
                if exclude_id and s.id == exclude_id:
                    continue
                if s.name == n:
                    raise ConflictError("name already exists in folder")
            return
        if kind == "document":
            siblings = self._store.list_documents(workspace_id=workspace_id, parent_id=parent_id)
            for s in siblings:
                if exclude_id and s.id == exclude_id:
                    continue
                if s.name == n:
                    raise ConflictError("name already exists in folder")
            return
        raise ValidationError(f"unknown kind: {kind}")


__all__ = ["StudioDomainService"]
