"""CreateService — content creation pipeline.

Pipeline: VALIDATE → GENERATE → PERSIST → INDEX → EVENT → VECTOR INDEX → RESPOND
Post-create reweave is plugin-driven via the ``post_create`` hook.
(DESIGN.md Section 4)
"""

from __future__ import annotations

import json
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from sqlalchemy import insert, select

if TYPE_CHECKING:
    from sqlalchemy import Connection

    from ztlctl.infrastructure.vault import VaultTransaction

from ztlctl.domain.content import get_content_model
from ztlctl.domain.ids import TYPE_PREFIXES, generate_content_hash
from ztlctl.infrastructure.database.counters import next_sequential_id
from ztlctl.infrastructure.database.schema import nodes
from ztlctl.services._helpers import now_iso, today_iso
from ztlctl.services.base import BaseService
from ztlctl.services.result import ServiceError, ServiceResult
from ztlctl.services.telemetry import trace_span, traced


@dataclass(frozen=True)
class _CreatedContent:
    """Committed content metadata used for post-commit side effects."""

    content_type: str
    content_id: str
    title: str
    path: str
    tags: list[str]
    body: str


class _BatchAbort(RuntimeError):
    """Internal sentinel used to roll back an all-or-nothing batch."""


class CreateService(BaseService):
    """Handles content creation for all types."""

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    @traced
    def create_note(
        self,
        title: str,
        *,
        subtype: str | None = None,
        tags: list[str] | None = None,
        topic: str | None = None,
        session: str | None = None,
        maturity: str | None = None,
        body: str | None = None,
        key_points: list[str] | None = None,
        links: dict[str, list[str]] | None = None,
        aliases: list[str] | None = None,
        dispatch_post_create: bool = True,
    ) -> ServiceResult:
        """Create a new note (plain, knowledge, or decision subtype)."""
        extra: dict[str, Any] = {}
        if body is not None:
            extra["body"] = body
        if key_points is not None:
            extra["key_points"] = key_points
        if links is not None:
            extra["links"] = links
        if aliases is not None:
            extra["aliases"] = aliases
        return self._create_content(
            content_type="note",
            title=title,
            subtype=subtype,
            tags=tags,
            topic=topic,
            session=session,
            maturity=maturity,
            dispatch_post_create=dispatch_post_create,
            **extra,
        )

    @traced
    def create_reference(
        self,
        title: str,
        *,
        url: str | None = None,
        canonical_url: str | None = None,
        subtype: str | None = None,
        tags: list[str] | None = None,
        topic: str | None = None,
        session: str | None = None,
        aliases: list[str] | None = None,
        links: dict[str, list[str]] | None = None,
        key_points: list[str] | None = None,
        body: str | None = None,
        summary: str | None = None,
        excerpts: list[str] | None = None,
        notes: str | None = None,
        provenance: list[str] | None = None,
        source_provider: str | None = None,
        source_type: str | None = None,
        source_kind: str | None = None,
        modalities: list[str] | None = None,
        capture_agent: str | None = None,
        capture_method: str | None = None,
        citations: list[str] | None = None,
        artifacts: list[dict[str, Any]] | None = None,
        source_bundle_path: str | None = None,
        retrieved_at: str | None = None,
        content_hash: str | None = None,
        language: str | None = None,
        dispatch_post_create: bool = True,
    ) -> ServiceResult:
        """Create a new reference to an external source."""
        extra: dict[str, Any] = {}
        if aliases is not None:
            extra["aliases"] = aliases
        if links is not None:
            extra["links"] = links
        if key_points is not None:
            extra["key_points"] = key_points
        if body is not None:
            extra["body"] = body
        if summary is not None:
            extra["summary"] = summary
        if excerpts is not None:
            extra["excerpts"] = excerpts
        if notes is not None:
            extra["notes"] = notes
        if provenance is not None:
            extra["provenance"] = provenance
        if canonical_url is not None:
            extra["canonical_url"] = canonical_url
        if source_provider is not None:
            extra["source_provider"] = source_provider
        if source_type is not None:
            extra["source_type"] = source_type
        if source_kind is not None:
            extra["source_kind"] = source_kind
        if modalities is not None:
            extra["modalities"] = modalities
        if capture_agent is not None:
            extra["capture_agent"] = capture_agent
        if capture_method is not None:
            extra["capture_method"] = capture_method
        if citations is not None:
            extra["citations"] = citations
        if artifacts is not None:
            extra["artifacts"] = artifacts
        if source_bundle_path is not None:
            extra["source_bundle_path"] = source_bundle_path
        if retrieved_at is not None:
            extra["retrieved_at"] = retrieved_at
        if content_hash is not None:
            extra["content_hash"] = content_hash
        if language is not None:
            extra["language"] = language
        return self._create_content(
            content_type="reference",
            title=title,
            subtype=subtype,
            tags=tags,
            topic=topic,
            session=session,
            url=url,
            dispatch_post_create=dispatch_post_create,
            **extra,
        )

    @traced
    def create_task(
        self,
        title: str,
        *,
        priority: str = "medium",
        impact: str = "medium",
        effort: str = "medium",
        tags: list[str] | None = None,
        session: str | None = None,
    ) -> ServiceResult:
        """Create a new task with priority/impact/effort matrix."""
        return self._create_content(
            content_type="task",
            title=title,
            tags=tags,
            session=session,
            priority=priority,
            impact=impact,
            effort=effort,
        )

    @traced
    def create_batch(
        self,
        items: list[dict[str, object]],
        *,
        partial: bool = False,
    ) -> ServiceResult:
        """Create multiple items. All-or-nothing unless *partial* is True."""
        results: list[dict[str, Any]] = []
        errors: list[dict[str, Any]] = []
        warnings: list[str] = []

        if partial:
            for i, item in enumerate(items):
                content_type = str(item.get("type", "note"))
                title = str(item.get("title", ""))
                kwargs: dict[str, Any] = {
                    k: v for k, v in item.items() if k not in ("type", "title")
                }
                result = self._create_content(content_type=content_type, title=title, **kwargs)
                if result.ok:
                    results.append(result.data)
                    warnings.extend(result.warnings)
                else:
                    errors.append(
                        {"index": i, "error": result.error.message if result.error else ""}
                    )
                    warnings.extend(result.warnings)

            all_ok = len(errors) == 0
            return ServiceResult(
                ok=all_ok,
                op="create_batch",
                data={"created": results, "errors": errors},
                warnings=warnings,
                error=ServiceError(
                    code="BATCH_PARTIAL",
                    message=f"{len(errors)} of {len(items)} items failed",
                )
                if not all_ok
                else None,
            )

        created_items: list[_CreatedContent] = []

        try:
            with self._vault.transaction() as txn:
                for i, item in enumerate(items):
                    content_type = str(item.get("type", "note"))
                    title = str(item.get("title", ""))
                    kwargs = {k: v for k, v in item.items() if k not in ("type", "title")}
                    result, created = self._create_content_in_txn(
                        txn,
                        content_type=content_type,
                        title=title,
                        **kwargs,
                    )
                    if result.ok and created is not None:
                        results.append(result.data)
                        warnings.extend(result.warnings)
                        created_items.append(created)
                        continue

                    errors.append(
                        {"index": i, "error": result.error.message if result.error else ""}
                    )
                    raise _BatchAbort()
        except _BatchAbort:
            return ServiceResult(
                ok=False,
                op="create_batch",
                error=ServiceError(
                    code="BATCH_FAILED",
                    message=f"Item {errors[-1]['index']} failed: {errors[-1]['error']}",
                ),
                data={"created": [], "errors": errors},
            )

        for created in created_items:
            self._dispatch_event(
                "post_create",
                {
                    "content_type": created.content_type,
                    "content_id": created.content_id,
                    "title": created.title,
                    "path": created.path,
                    "tags": created.tags,
                },
                warnings,
            )
            self._vector_index_created(created, warnings)

        return ServiceResult(
            ok=True,
            op="create_batch",
            data={"created": results, "errors": errors},
            warnings=warnings,
        )

    # ------------------------------------------------------------------
    # Six-stage pipeline (private)
    # ------------------------------------------------------------------

    def _create_content(
        self,
        *,
        content_type: str,
        title: str,
        subtype: str | None = None,
        tags: list[str] | None = None,
        topic: str | None = None,
        session: str | None = None,
        dispatch_post_create: bool = True,
        **extra: Any,
    ) -> ServiceResult:
        """Shared creation pipeline.

        VALIDATE → GENERATE → PERSIST → INDEX → EVENT → VECTOR INDEX → RESPOND
        """
        with self._vault.transaction() as txn:
            result, created = self._create_content_in_txn(
                txn,
                content_type=content_type,
                title=title,
                subtype=subtype,
                tags=tags,
                topic=topic,
                session=session,
                **extra,
            )

        if not result.ok or created is None:
            return result

        warnings = list(result.warnings)

        if dispatch_post_create:
            with trace_span("dispatch_event"):
                self._dispatch_event(
                    "post_create",
                    {
                        "content_type": created.content_type,
                        "content_id": created.content_id,
                        "title": created.title,
                        "path": created.path,
                        "tags": created.tags,
                    },
                    warnings,
                )

        self._vector_index_created(created, warnings)

        return ServiceResult(
            ok=True,
            op=result.op,
            data=result.data,
            warnings=warnings,
        )

    def _create_content_in_txn(
        self,
        txn: VaultTransaction,
        *,
        content_type: str,
        title: str,
        subtype: str | None = None,
        tags: list[str] | None = None,
        topic: str | None = None,
        session: str | None = None,
        **extra: Any,
    ) -> tuple[ServiceResult, _CreatedContent | None]:
        """Create content inside an existing transaction without side effects."""
        op = f"create_{content_type}"
        warnings: list[str] = []
        tags = tags or []
        today = today_iso()
        now = now_iso()

        with trace_span("validate"):
            try:
                model_cls = get_content_model(content_type, subtype)
            except KeyError:
                return (
                    ServiceResult(
                        ok=False,
                        op=op,
                        error=ServiceError(
                            code="UNKNOWN_TYPE",
                            message=(
                                f"Unknown content type: {content_type!r} / subtype: {subtype!r}"
                            ),
                        ),
                    ),
                    None,
                )

            initial_status = self._initial_status(content_type, subtype)
            validate_data: dict[str, Any] = {
                "title": title,
                "status": initial_status,
                "tags": tags,
                **extra,
            }
            vr = model_cls.validate_create(validate_data)
            if not vr.valid:
                return (
                    ServiceResult(
                        ok=False,
                        op=op,
                        error=ServiceError(code="VALIDATION_FAILED", message="; ".join(vr.errors)),
                    ),
                    None,
                )
            warnings.extend(vr.warnings)

            for tag in tags:
                if "/" not in tag:
                    warnings.append(
                        f"Tag '{tag}' missing domain/scope format (e.g. 'domain/scope')"
                    )

        with trace_span("generate"):
            content_id = self._generate_id(txn.conn, content_type, title)
            if content_id is None:
                return (
                    ServiceResult(
                        ok=False,
                        op=op,
                        error=ServiceError(
                            code="UNKNOWN_TYPE",
                            message=f"No ID prefix for content type: {content_type!r}",
                        ),
                    ),
                    None,
                )

            existing = txn.conn.execute(select(nodes.c.id).where(nodes.c.id == content_id)).first()
            if existing is not None:
                return (
                    ServiceResult(
                        ok=False,
                        op=op,
                        error=ServiceError(
                            code="ID_COLLISION",
                            message=f"Content with ID '{content_id}' already exists",
                        ),
                    ),
                    None,
                )

        with trace_span("persist"):
            model_data: dict[str, Any] = {
                "id": content_id,
                "type": content_type,
                "status": initial_status,
                "title": title,
                "created": today,
                "modified": today,
            }
            if subtype:
                model_data["subtype"] = subtype
            if tags:
                model_data["tags"] = tags
            if topic:
                model_data["topic"] = topic
            if session:
                model_data["session"] = session
            model_data.update(extra)

            model = model_cls.model_validate(model_data)
            body = model.write_body(template_root=self._vault.root, **extra)
            fm = model.to_frontmatter()

            path = txn.resolve_path(content_type, content_id, topic=topic)
            txn.write_content(path, fm, body)

        with trace_span("index"):
            rel_path = str(path.relative_to(self._vault.root))
            node_row: dict[str, Any] = {
                "id": content_id,
                "title": title,
                "type": content_type,
                "subtype": subtype,
                "status": initial_status,
                "path": rel_path,
                "created": today,
                "modified": today,
                "created_at": now,
                "modified_at": now,
            }
            if topic:
                node_row["topic"] = topic
            if session:
                node_row["session"] = session
            maturity = extra.get("maturity")
            if maturity:
                node_row["maturity"] = maturity
            aliases = fm.get("aliases")
            if isinstance(aliases, list):
                node_row["aliases"] = json.dumps(aliases)
            txn.conn.execute(insert(nodes).values(**node_row))

            txn.upsert_fts(content_id, title, body)
            txn.index_tags(content_id, tags, today)

            fm_links = fm.get("links", {})
            if isinstance(fm_links, dict):
                txn.index_links(content_id, fm_links, body, today)

        result = ServiceResult(
            ok=True,
            op=op,
            data={
                "id": content_id,
                "path": rel_path,
                "title": title,
                "type": content_type,
            },
            warnings=warnings,
        )
        created = _CreatedContent(
            content_type=content_type,
            content_id=content_id,
            title=title,
            path=rel_path,
            tags=list(tags),
            body=body,
        )
        return result, created

    def _vector_index_created(self, created: _CreatedContent, warnings: list[str]) -> None:
        """Best-effort vector indexing for newly created content."""
        if not self._vault.settings.search.semantic_enabled:
            return

        with trace_span("vector_index"):
            try:
                from ztlctl.services.vector import VectorService

                vec_svc = VectorService(self._vault)
                if vec_svc.is_available():
                    vec_svc.ensure_table()
                    vec_svc.index_node(created.content_id, f"{created.title} {created.body}")
            except Exception as exc:
                warnings.append(f"Vector indexing skipped: {exc}")

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _initial_status(content_type: str, subtype: str | None = None) -> str:
        """Determine the correct initial status for a content type."""
        if subtype == "decision":
            return "proposed"
        return {
            "note": "draft",
            "reference": "captured",
            "task": "inbox",
            "log": "open",
        }.get(content_type, "draft")

    @staticmethod
    def _generate_id(conn: Connection, content_type: str, title: str) -> str | None:
        """Generate the appropriate ID for the content type.

        Returns None if the content type has no registered ID prefix.
        """
        prefix = TYPE_PREFIXES.get(content_type)
        if prefix is None:
            return None

        if content_type in ("log", "task"):
            return next_sequential_id(conn, prefix)
        return generate_content_hash(title, prefix)
