"""Catalog Engine — write services.

All write operations go through these functions; views must NOT call ORM
save/delete directly on CatalogEngine models.
"""

from __future__ import annotations

from django.db import transaction
from django.utils import timezone

from simorgh.apps.catalog.models import (
    CatalogCategory,
    CatalogItem,
    CatalogItemStatus,
    CatalogItemVersion,
)
from simorgh.core.audit import record_service_event

# ── Categories ──────────────────────────────────────────────────────────────

def create_category(
    *,
    tenant_id: int,
    organization_node_id: int,
    name: str,
    slug: str,
    catalog_type: str,
    parent_id: int | None = None,
    description: str = "",
    icon: str = "",
    sort_order: int = 0,
) -> CatalogCategory:
    from simorgh.apps.events.bus import dispatch

    category = CatalogCategory.objects.create(
        tenant_id=tenant_id,
        organization_node_id=organization_node_id,
        name=name.strip(),
        slug=slug.strip(),
        catalog_type=catalog_type,
        parent_id=parent_id,
        description=description,
        icon=icon,
        sort_order=sort_order,
    )
    record_service_event("catalog.category_created", resource=category, after={"name": name, "catalog_type": catalog_type})
    dispatch("catalog.category_created", {
        "category_id": str(category.public_id),
        "tenant_id": tenant_id,
        "name": name,
        "catalog_type": catalog_type,
    })
    return category


def update_category(
    category: CatalogCategory,
    *,
    name: str | None = None,
    slug: str | None = None,
    catalog_type: str | None = None,
    parent_id: int | None | object = ...,  # type: ignore[assignment]
    description: str | None = None,
    icon: str | None = None,
    sort_order: int | None = None,
    is_active: bool | None = None,
) -> CatalogCategory:
    from simorgh.apps.events.bus import dispatch

    _SENTINEL = object()
    changed = False

    if name is not None:
        category.name = name.strip()
        changed = True
    if slug is not None:
        category.slug = slug.strip()
        changed = True
    if catalog_type is not None:
        category.catalog_type = catalog_type
        changed = True
    if parent_id is not _SENTINEL:
        category.parent_id = parent_id  # type: ignore[assignment]
        changed = True
    if description is not None:
        category.description = description
        changed = True
    if icon is not None:
        category.icon = icon
        changed = True
    if sort_order is not None:
        category.sort_order = sort_order
        changed = True
    if is_active is not None:
        category.is_active = is_active
        changed = True

    if changed:
        category.save()
        record_service_event("catalog.category_updated", resource=category)
        dispatch("catalog.category_updated", {
            "category_id": str(category.public_id),
            "tenant_id": category.tenant_id,
        })
    return category


def delete_category(category: CatalogCategory) -> None:
    from simorgh.apps.events.bus import dispatch

    public_id, tenant_id = str(category.public_id), category.tenant_id
    category.delete()
    record_service_event("catalog.category_deleted", resource=category)
    dispatch("catalog.category_deleted", {
        "category_id": public_id,
        "tenant_id": tenant_id,
    })


# ── Catalog Items ───────────────────────────────────────────────────────────

def create_catalog_item(
    *,
    tenant_id: int,
    organization_node_id: int,
    name: str,
    slug: str,
    catalog_type: str,
    owner_id: int | None = None,
    category_id: int | None = None,
    description: str = "",
    short_description: str = "",
    image_id: int | None = None,
    documentation_content_id: str | None = None,
) -> CatalogItem:
    from simorgh.apps.events.bus import dispatch

    item = CatalogItem.objects.create(
        tenant_id=tenant_id,
        organization_node_id=organization_node_id,
        name=name.strip(),
        slug=slug.strip(),
        catalog_type=catalog_type,
        owner_id=owner_id,
        category_id=category_id,
        description=description,
        short_description=short_description,
        status=CatalogItemStatus.DRAFT,
        image_id=image_id,
        documentation_content_id=documentation_content_id,
    )
    record_service_event("catalog.item_created", resource=item, after={
        "name": name,
        "catalog_type": catalog_type,
    })
    dispatch("catalog.item_created", {
        "item_id": str(item.public_id),
        "tenant_id": tenant_id,
        "catalog_type": catalog_type,
    })
    return item


def update_catalog_item(
    item: CatalogItem,
    *,
    name: str | None = None,
    slug: str | None = None,
    catalog_type: str | None = None,
    category_id: int | None | object = ...,  # type: ignore[assignment]
    description: str | None = None,
    short_description: str | None = None,
    owner_id: int | None | object = ...,  # type: ignore[assignment]
    image_id: int | None | object = ...,  # type: ignore[assignment]
    related_items: list | None = None,
    documentation_content_id: str | None | object = ...,  # type: ignore[assignment]
    ai_description: str | None = None,
    ai_semantic_type: str | None = None,
) -> CatalogItem:
    from simorgh.apps.events.bus import dispatch

    _SENTINEL = object()
    changed = False

    if name is not None:
        item.name = name.strip()
        changed = True
    if slug is not None:
        item.slug = slug.strip()
        changed = True
    if catalog_type is not None:
        item.catalog_type = catalog_type
        changed = True
    if category_id is not _SENTINEL:
        item.category_id = category_id  # type: ignore[assignment]
        changed = True
    if description is not None:
        item.description = description
        changed = True
    if short_description is not None:
        item.short_description = short_description
        changed = True
    if owner_id is not _SENTINEL:
        item.owner_id = owner_id  # type: ignore[assignment]
        changed = True
    if image_id is not _SENTINEL:
        item.image_id = image_id  # type: ignore[assignment]
        changed = True
    if related_items is not None:
        item.related_items = related_items
        changed = True
    if documentation_content_id is not _SENTINEL:
        item.documentation_content_id = documentation_content_id  # type: ignore[assignment]
        changed = True
    if ai_description is not None:
        item.ai_description = ai_description
        changed = True
    if ai_semantic_type is not None:
        item.ai_semantic_type = ai_semantic_type
        changed = True

    if changed:
        item.save()
        record_service_event("catalog.item_updated", resource=item)
        dispatch("catalog.item_updated", {
            "item_id": str(item.public_id),
            "tenant_id": item.tenant_id,
        })
    return item


@transaction.atomic
def submit_for_review(item: CatalogItem, *, actor_id: int) -> CatalogItem:
    from simorgh.apps.events.bus import dispatch

    if item.status != CatalogItemStatus.DRAFT:
        raise ValueError("Only draft catalog items can be submitted for review.")

    item.status = CatalogItemStatus.REVIEW
    item.save(update_fields=["status", "updated_at"])

    _ensure_workflow_started(item, actor_id)

    record_service_event("catalog.item_submitted_for_review", resource=item, after={"actor_id": actor_id})
    dispatch("catalog.item_submitted_for_review", {
        "item_id": str(item.public_id),
        "tenant_id": item.tenant_id,
        "actor_id": actor_id,
    })
    return item


@transaction.atomic
def publish_item(item: CatalogItem, *, actor_id: int, change_summary: str = "") -> CatalogItem:
    from simorgh.apps.events.bus import dispatch

    if item.status not in (CatalogItemStatus.REVIEW, CatalogItemStatus.DRAFT):
        raise ValueError("Only review or draft items can be published.")

    item.status = CatalogItemStatus.PUBLISHED
    item.published_at = timezone.now()
    item.save(update_fields=["status", "published_at", "updated_at"])

    next_version = item.versions.count() + 1
    CatalogItemVersion.objects.create(
        tenant_id=item.tenant_id,
        organization_node_id=item.organization_node_id,
        catalog_item=item,
        version=next_version,
        name=item.name,
        description=item.description,
        status=item.status,
        changed_by_id=actor_id,
        change_summary=change_summary,
    )

    record_service_event("catalog.item_published", resource=item, after={"actor_id": actor_id})
    dispatch("catalog.item_published", {
        "item_id": str(item.public_id),
        "tenant_id": item.tenant_id,
        "actor_id": actor_id,
    })
    dispatch("catalog.version_created", {
        "item_id": str(item.public_id),
        "tenant_id": item.tenant_id,
        "version": next_version,
    })
    return item


@transaction.atomic
def retire_item(item: CatalogItem, *, actor_id: int) -> CatalogItem:
    from simorgh.apps.events.bus import dispatch

    if item.status != CatalogItemStatus.PUBLISHED:
        raise ValueError("Only published catalog items can be retired.")

    item.status = CatalogItemStatus.RETIRED
    item.retired_at = timezone.now()
    item.save(update_fields=["status", "retired_at", "updated_at"])

    record_service_event("catalog.item_retired", resource=item, after={"actor_id": actor_id})
    dispatch("catalog.item_retired", {
        "item_id": str(item.public_id),
        "tenant_id": item.tenant_id,
        "actor_id": actor_id,
    })
    return item


@transaction.atomic
def revert_to_draft(item: CatalogItem, *, actor_id: int) -> CatalogItem:
    from simorgh.apps.events.bus import dispatch

    if item.status not in (CatalogItemStatus.PUBLISHED, CatalogItemStatus.RETIRED, CatalogItemStatus.REVIEW):
        raise ValueError("Only published, retired, or review items can be reverted to draft.")

    item.status = CatalogItemStatus.DRAFT
    item.published_at = None
    item.retired_at = None
    item.save(update_fields=["status", "published_at", "retired_at", "updated_at"])

    record_service_event("catalog.item_reverted_to_draft", resource=item, after={"actor_id": actor_id})
    dispatch("catalog.item_reverted_to_draft", {
        "item_id": str(item.public_id),
        "tenant_id": item.tenant_id,
        "actor_id": actor_id,
    })
    return item


def delete_catalog_item(item: CatalogItem) -> None:
    item.delete()


# ── Helpers ─────────────────────────────────────────────────────────────────

def _ensure_workflow_started(item: CatalogItem, actor_id: int) -> None:
    if item.workflow_instance_id:
        return
    try:
        from simorgh.apps.workflow.engine import start_instance
        instance = start_instance(
            definition_name="catalog_approval",
            subject=item,
            actor_id=actor_id,
        )
        item.workflow_instance = instance
        item.save(update_fields=["workflow_instance"])
    except Exception:
        import structlog
        _log = structlog.get_logger("simorgh.catalog")
        _log.warning("catalog.workflow_start_failed", item_id=str(item.public_id), exc_info=True)
