"""Commercial registry service — CRUD, lifecycle, serialization, events."""
from __future__ import annotations

import re
import uuid
from datetime import datetime, timezone
from typing import Any

from sqlalchemy.ext.asyncio import AsyncSession

from app.commercial.kinds import BUILTIN_REGISTRY_KINDS, KIND_BY_KIND, KIND_BY_PATH
from app.commercial.repository import CommercialRegistryRepository
from app.models.audit import AuditLog
from app.models.commercial import CommercialRegistryKind, CommercialRegistryObject
from app.models.events import OutboxEvent
from shared.events import EventStatus
from shared.exceptions import ConflictError, NotFoundError, ValidationAppError


def _slugify(value: str) -> str:
    s = value.strip().lower()
    s = re.sub(r"[^a-z0-9]+", "-", s)
    return s.strip("-")[:180] or str(uuid.uuid4())


class CommercialRegistryService:
    def __init__(self, db: AsyncSession) -> None:
        self.db = db
        self.repo = CommercialRegistryRepository(db)

    async def ensure_builtin_kinds(self) -> None:
        for i, spec in enumerate(BUILTIN_REGISTRY_KINDS):
            await self.repo.upsert_kind(
                CommercialRegistryKind(
                    kind=spec.kind,
                    path=spec.path,
                    display_name=spec.display_name,
                    description=spec.description,
                    code_field=spec.code_field,
                    list_envelope=spec.list_envelope,
                    public_read=spec.public_read,
                    enabled=True,
                    sort_order=i * 10,
                )
            )
        await self.db.commit()

    async def resolve_kind(self, path_or_kind: str) -> CommercialRegistryKind:
        kind = await self.repo.get_kind_by_path(path_or_kind)
        if kind is None:
            kind = await self.repo.get_kind(path_or_kind)
        if kind is None:
            # fall back to builtins without requiring prior seed
            spec = KIND_BY_PATH.get(path_or_kind) or KIND_BY_KIND.get(path_or_kind)
            if spec:
                kind = await self.repo.upsert_kind(
                    CommercialRegistryKind(
                        kind=spec.kind,
                        path=spec.path,
                        display_name=spec.display_name,
                        description=spec.description,
                        code_field=spec.code_field,
                        list_envelope=spec.list_envelope,
                        public_read=spec.public_read,
                        enabled=True,
                    )
                )
                await self.db.commit()
        if kind is None or not kind.enabled:
            raise NotFoundError(f"رجیستری «{path_or_kind}» یافت نشد")
        return kind

    async def discovery_catalog(self) -> dict[str, Any]:
        kinds = await self.repo.list_kinds(enabled_only=True)
        if not kinds:
            await self.ensure_builtin_kinds()
            kinds = await self.repo.list_kinds(enabled_only=True)
        return {
            "contract_version": "commercial.v1.2",
            "registries": [
                {
                    "kind": k.kind,
                    "path": k.path,
                    "href": f"/api/v1/commercial/{k.path}",
                    "generic_href": f"/api/v1/commercial/registries/{k.kind}",
                    "display_name": k.display_name,
                    "description": k.description,
                    "code_field": k.code_field,
                    "list_envelope": k.list_envelope,
                    "public_read": k.public_read,
                }
                for k in kinds
            ],
        }

    def serialize(self, obj: CommercialRegistryObject, code_field: str) -> dict[str, Any]:
        data = {
            "registry_object_id": str(obj.id),
            "id": str(obj.id),
            "kind": obj.kind,
            "slug": obj.slug,
            "stable_code": obj.stable_code,
            code_field: obj.stable_code,
            "version_code": obj.version_code,
            "version": obj.version_code,
            "display_name": obj.display_name,
            "description": obj.description,
            "status": obj.status,
            "visibility": obj.visibility,
            "tenant_id": str(obj.tenant_id) if obj.tenant_id else None,
            "metadata": obj.meta_data or {},
            "created_by": str(obj.created_by) if obj.created_by else None,
            "updated_by": str(obj.updated_by) if obj.updated_by else None,
            "created_at": obj.created_at.isoformat() if obj.created_at else None,
            "updated_at": obj.updated_at.isoformat() if obj.updated_at else None,
            "published_at": obj.published_at.isoformat() if obj.published_at else None,
            "archived_at": obj.archived_at.isoformat() if obj.archived_at else None,
            "is_deleted": obj.is_deleted,
            "clone_of_id": str(obj.clone_of_id) if obj.clone_of_id else None,
        }
        # Flatten payload for FE adapters (product_code, default_route, …)
        payload = dict(obj.payload or {})
        for key, value in payload.items():
            if key not in data:
                data[key] = value
        data["payload"] = payload
        return data

    async def list(
        self,
        path_or_kind: str,
        *,
        q: str | None = None,
        status: str | None = "published",
        visibility: str | None = None,
        include_deleted: bool = False,
        sort: str = "display_name",
        order: str = "asc",
        offset: int = 0,
        limit: int = 100,
        tenant_id: uuid.UUID | None = None,
        admin: bool = False,
    ) -> dict[str, Any]:
        kind = await self.resolve_kind(path_or_kind)
        # Public list defaults to published; admins may pass status=all
        status_filter = None if (admin and status in (None, "all")) else status
        rows, total = await self.repo.list_objects(
            kind.kind,
            tenant_id=tenant_id,
            include_deleted=include_deleted and admin,
            status=status_filter,
            visibility=visibility,
            q=q,
            sort=sort,
            order=order,
            offset=offset,
            limit=limit,
            platform_only=tenant_id is None,
        )
        items = [self.serialize(r, kind.code_field) for r in rows]
        envelope = kind.list_envelope or "items"
        return {
            envelope: items,
            "items": items,
            "total": total,
            "kind": kind.kind,
            "path": kind.path,
        }

    async def get(self, path_or_kind: str, object_id: uuid.UUID) -> dict[str, Any]:
        kind = await self.resolve_kind(path_or_kind)
        obj = await self.repo.get(object_id)
        if obj is None or obj.kind != kind.kind:
            raise NotFoundError("شیء رجیستری یافت نشد")
        return self.serialize(obj, kind.code_field)

    async def create(
        self,
        path_or_kind: str,
        body: dict[str, Any],
        *,
        actor: uuid.UUID | None,
        tenant_id: uuid.UUID | None = None,
    ) -> dict[str, Any]:
        kind = await self.resolve_kind(path_or_kind)
        code = (
            body.get(kind.code_field)
            or body.get("stable_code")
            or body.get("code")
            or body.get("slug")
        )
        if not code:
            raise ValidationAppError(f"فیلد «{kind.code_field}» الزامی است")
        code = str(code)
        display_name = str(body.get("display_name") or body.get("name") or code)
        version_code = str(body.get("version_code") or body.get("version") or "1")
        existing = await self.repo.get_by_code(
            kind.kind, code, version_code=version_code, tenant_id=tenant_id
        )
        if existing and not existing.is_deleted:
            raise ConflictError(f"کد «{code}» نسخه {version_code} از قبل وجود دارد")

        reserved = {
            "registry_object_id",
            "id",
            "kind",
            "slug",
            "stable_code",
            kind.code_field,
            "version_code",
            "version",
            "display_name",
            "description",
            "status",
            "visibility",
            "tenant_id",
            "metadata",
            "payload",
            "created_by",
            "updated_by",
        }
        payload = dict(body.get("payload") or {})
        for k, v in body.items():
            if k not in reserved:
                payload[k] = v

        obj = CommercialRegistryObject(
            kind=kind.kind,
            slug=_slugify(str(body.get("slug") or code)),
            stable_code=code,
            version_code=version_code,
            display_name=display_name,
            description=body.get("description"),
            status=str(body.get("status") or "draft"),
            visibility=str(body.get("visibility") or "public"),
            tenant_id=tenant_id,
            payload=payload,
            meta_data=dict(body.get("metadata") or {}),
            created_by=actor,
            updated_by=actor,
        )
        if obj.status == "published":
            obj.published_at = datetime.now(timezone.utc)
        await self.repo.create(obj)
        await self._audit(actor, "commercial.registry.created", obj)
        await self._event("commercial.registry.created", obj)
        await self.db.commit()
        await self.db.refresh(obj)
        return self.serialize(obj, kind.code_field)

    async def update(
        self,
        path_or_kind: str,
        object_id: uuid.UUID,
        body: dict[str, Any],
        *,
        actor: uuid.UUID | None,
    ) -> dict[str, Any]:
        kind = await self.resolve_kind(path_or_kind)
        obj = await self.repo.get(object_id)
        if obj is None or obj.kind != kind.kind or obj.is_deleted:
            raise NotFoundError("شیء رجیستری یافت نشد")
        if "display_name" in body:
            obj.display_name = str(body["display_name"])
        if "description" in body:
            obj.description = body.get("description")
        if "visibility" in body:
            obj.visibility = str(body["visibility"])
        if "version_code" in body or "version" in body:
            obj.version_code = str(body.get("version_code") or body.get("version"))
        if "slug" in body:
            obj.slug = _slugify(str(body["slug"]))
        if "metadata" in body and isinstance(body["metadata"], dict):
            obj.meta_data = dict(body["metadata"])
        reserved = {
            "registry_object_id",
            "id",
            "kind",
            "slug",
            "stable_code",
            kind.code_field,
            "version_code",
            "version",
            "display_name",
            "description",
            "status",
            "visibility",
            "tenant_id",
            "metadata",
            "payload",
            "created_by",
            "updated_by",
            "created_at",
            "updated_at",
        }
        payload = dict(obj.payload or {})
        if isinstance(body.get("payload"), dict):
            payload.update(body["payload"])
        for k, v in body.items():
            if k not in reserved:
                payload[k] = v
        obj.payload = payload
        obj.updated_by = actor
        await self.repo.save(obj)
        await self._audit(actor, "commercial.registry.updated", obj)
        await self._event("commercial.registry.updated", obj)
        await self.db.commit()
        await self.db.refresh(obj)
        return self.serialize(obj, kind.code_field)

    async def lifecycle(
        self,
        path_or_kind: str,
        object_id: uuid.UUID,
        action: str,
        *,
        actor: uuid.UUID | None,
    ) -> dict[str, Any]:
        kind = await self.resolve_kind(path_or_kind)
        obj = await self.repo.get(object_id)
        if obj is None or obj.kind != kind.kind:
            raise NotFoundError("شیء رجیستری یافت نشد")
        now = datetime.now(timezone.utc)
        action = action.lower()
        if action == "publish":
            obj.status = "published"
            obj.published_at = now
            obj.is_deleted = False
            event = "commercial.registry.published"
        elif action == "draft":
            obj.status = "draft"
            event = "commercial.registry.updated"
        elif action == "archive":
            obj.status = "archived"
            obj.archived_at = now
            event = "commercial.registry.archived"
        elif action == "delete":
            await self.repo.soft_delete(obj, actor)
            event = "commercial.registry.deleted"
        elif action == "restore":
            await self.repo.restore(obj, actor)
            event = "commercial.registry.updated"
        elif action == "clone":
            clone = CommercialRegistryObject(
                kind=obj.kind,
                slug=_slugify(f"{obj.slug}-copy"),
                stable_code=f"{obj.stable_code}.copy.{uuid.uuid4().hex[:6]}",
                version_code=obj.version_code,
                display_name=f"{obj.display_name} (copy)",
                description=obj.description,
                status="draft",
                visibility=obj.visibility,
                tenant_id=obj.tenant_id,
                payload=dict(obj.payload or {}),
                meta_data=dict(obj.meta_data or {}),
                created_by=actor,
                updated_by=actor,
                clone_of_id=obj.id,
            )
            await self.repo.create(clone)
            await self._audit(actor, "commercial.registry.cloned", clone)
            await self._event("commercial.registry.created", clone)
            await self.db.commit()
            await self.db.refresh(clone)
            return self.serialize(clone, kind.code_field)
        else:
            raise ValidationAppError(f"اکشن «{action}» پشتیبانی نمی‌شود")

        obj.updated_by = actor
        await self.repo.save(obj)
        await self._audit(actor, event, obj)
        await self._event(event, obj)
        await self.db.commit()
        await self.db.refresh(obj)
        return self.serialize(obj, kind.code_field)

    async def _audit(
        self, actor: uuid.UUID | None, action: str, obj: CommercialRegistryObject
    ) -> None:
        self.db.add(
            AuditLog(
                tenant_id=obj.tenant_id,
                actor_user_id=actor,
                action=action,
                resource_type=f"commercial.{obj.kind}",
                resource_id=str(obj.id),
                meta_data={
                    "stable_code": obj.stable_code,
                    "version_code": obj.version_code,
                    "status": obj.status,
                },
            )
        )

    async def _event(self, event_type: str, obj: CommercialRegistryObject) -> None:
        self.db.add(
            OutboxEvent(
                event_type=event_type,
                aggregate_type=f"commercial.{obj.kind}",
                aggregate_id=str(obj.id),
                tenant_id=obj.tenant_id,
                payload={
                    "registry_object_id": str(obj.id),
                    "kind": obj.kind,
                    "stable_code": obj.stable_code,
                    "version_code": obj.version_code,
                    "status": obj.status,
                },
                status=EventStatus.PENDING,
            )
        )
