"""Component library application services — Phase 11.2."""
from __future__ import annotations

from datetime import date, datetime, timezone
from typing import Any
from uuid import UUID

from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

from app.events.publisher import get_event_publisher
from app.events.types import ExperienceEventType
from app.models.components import (
    ExperienceComponent,
    ExperienceComponentVersion,
    PageComponentPlacement,
)
from app.models.types import (
    AuditAction,
    ComponentStatus,
    ComponentVersionStatus,
)
from app.policies.components import (
    ComponentLifecyclePolicy,
    ComponentVersionPolicy,
    PagePlacementPolicy,
)
from app.repositories.components import (
    ExperienceComponentRepository,
    ExperienceComponentVersionRepository,
    PageComponentPlacementRepository,
)
from app.repositories.foundation import ExperienceWorkspaceRepository
from app.repositories.sites import ExperiencePageRepository, ExperienceSiteRepository
from app.schemas.components import (
    ExperienceComponentCreate,
    ExperienceComponentUpdate,
    ExperienceComponentVersionCreate,
    ExperienceComponentVersionUpdate,
    PageComponentPlacementCreate,
    PageComponentPlacementUpdate,
)
from app.services.audit_service import AuditService
from app.specifications.components import (
    ComponentListSpec,
    ComponentVersionListSpec,
    PlacementListSpec,
)
from app.validators import ensure_optimistic_version, validate_code, validate_non_empty
from app.validators.components import (
    ensure_schema_payload,
    validate_component_kind,
    validate_slot_key,
)
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


def _apply_update(entity, data: dict) -> None:
    for key, value in data.items():
        setattr(entity, key, value)


def _jsonable_changes(data: dict[str, Any]) -> dict[str, Any]:
    out: dict[str, Any] = {}
    for key, value in data.items():
        if isinstance(value, UUID):
            out[key] = str(value)
        elif isinstance(value, (datetime, date)):
            out[key] = value.isoformat()
        elif hasattr(value, "value"):
            out[key] = value.value
        else:
            out[key] = value
    return out


def _actor_id(actor: CurrentUser | None) -> str | None:
    return actor.user_id if actor else None


class ExperienceComponentService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = ExperienceComponentRepository(session)
        self.versions = ExperienceComponentVersionRepository(session)
        self.placements = PageComponentPlacementRepository(session)
        self.workspaces = ExperienceWorkspaceRepository(session)
        self.sites = ExperienceSiteRepository(session)
        self.pages = ExperiencePageRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()
        self.lifecycle_policy = ComponentLifecyclePolicy()
        self.version_policy = ComponentVersionPolicy()
        self.placement_policy = PagePlacementPolicy()

    # --- Components ---

    async def create_component(
        self,
        tenant_id: UUID,
        body: ExperienceComponentCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceComponent:
        ws = await self.workspaces.get(tenant_id, body.workspace_id)
        if ws is None:
            raise NotFoundError("فضای کاری یافت نشد")
        code = validate_code(body.code)
        name = validate_non_empty(body.name, "name")
        kind = validate_component_kind(body.kind)
        if await self.repo.get_by_code(tenant_id, body.workspace_id, code):
            raise AppError(
                "کد کامپوننت تکراری است",
                error_code="duplicate_code",
                status_code=409,
            )
        entity = ExperienceComponent(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            code=code,
            name=name,
            description=body.description,
            kind=kind,
            status=body.status,
            category=body.category,
            tags=body.tags,
            metadata_json=body.metadata_json,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        try:
            await self.repo.add(entity)
        except IntegrityError as exc:
            raise AppError(
                "کد کامپوننت تکراری است",
                error_code="duplicate_code",
                status_code=409,
            ) from exc
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="component",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=ExperienceEventType.COMPONENT_CREATED,
            aggregate_type="component",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "code": entity.code,
                "workspace_id": str(entity.workspace_id),
                "kind": entity.kind.value,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get_component(
        self, tenant_id: UUID, component_id: UUID
    ) -> ExperienceComponent:
        entity = await self.repo.get(tenant_id, component_id)
        if entity is None:
            raise NotFoundError("کامپوننت یافت نشد")
        return entity

    async def list_components(
        self,
        tenant_id: UUID,
        *,
        offset: int,
        limit: int,
        spec: ComponentListSpec | None = None,
    ) -> tuple[list[ExperienceComponent], int]:
        items, total = await self.repo.list_filtered(
            tenant_id, offset=offset, limit=limit, spec=spec
        )
        return list(items), total

    async def update_component(
        self,
        tenant_id: UUID,
        component_id: UUID,
        body: ExperienceComponentUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceComponent:
        entity = await self.get_component(tenant_id, component_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "name" in data and data["name"] is not None:
            data["name"] = validate_non_empty(data["name"], "name")
        if "kind" in data and data["kind"] is not None:
            data["kind"] = validate_component_kind(data["kind"])
        if "status" in data and data["status"] is not None:
            self.lifecycle_policy.assert_status_change(
                current=entity.status, target=data["status"]
            )
        _apply_update(entity, data)
        entity.updated_by = _actor_id(actor)
        entity.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="component",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=ExperienceEventType.COMPONENT_UPDATED,
            aggregate_type="component",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "status": entity.status.value},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def activate_component(
        self,
        tenant_id: UUID,
        component_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceComponent:
        return await self._set_component_status(
            tenant_id,
            component_id,
            ComponentStatus.ACTIVE,
            ExperienceEventType.COMPONENT_ACTIVATED,
            actor=actor,
        )

    async def deprecate_component(
        self,
        tenant_id: UUID,
        component_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceComponent:
        return await self._set_component_status(
            tenant_id,
            component_id,
            ComponentStatus.DEPRECATED,
            ExperienceEventType.COMPONENT_DEPRECATED,
            actor=actor,
        )

    async def archive_component(
        self,
        tenant_id: UUID,
        component_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceComponent:
        return await self._set_component_status(
            tenant_id,
            component_id,
            ComponentStatus.ARCHIVED,
            ExperienceEventType.COMPONENT_ARCHIVED,
            actor=actor,
        )

    async def _set_component_status(
        self,
        tenant_id: UUID,
        component_id: UUID,
        target: ComponentStatus,
        event_type: ExperienceEventType,
        *,
        actor: CurrentUser | None,
    ) -> ExperienceComponent:
        entity = await self.get_component(tenant_id, component_id)
        self.lifecycle_policy.assert_status_change(
            current=entity.status, target=target
        )
        entity.status = target
        entity.updated_by = _actor_id(actor)
        entity.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="component",
            entity_id=entity.id,
            action=AuditAction.STATUS_CHANGE,
            actor_user_id=_actor_id(actor),
            changes={"status": target.value},
        )
        self.events.publish(
            event_type=event_type,
            aggregate_type="component",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "status": target.value},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def soft_delete_component(
        self,
        tenant_id: UUID,
        component_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceComponent:
        entity = await self.get_component(tenant_id, component_id)
        usage = await self.placements.count_by_component(tenant_id, component_id)
        if usage > 0:
            raise AppError(
                "کامپوننت دارای جایگذاری فعال است",
                error_code="component_in_use",
                status_code=409,
                details={"placement_count": usage},
            )
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        entity.updated_by = _actor_id(actor)
        entity.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="component",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
            changes={"is_deleted": True},
        )
        self.events.publish(
            event_type=ExperienceEventType.COMPONENT_UPDATED,
            aggregate_type="component",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "is_deleted": True},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    # --- Versions ---

    async def create_version(
        self,
        tenant_id: UUID,
        body: ExperienceComponentVersionCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceComponentVersion:
        component = await self.get_component(tenant_id, body.component_id)
        if component.workspace_id != body.workspace_id:
            raise AppError(
                "workspace_id با کامپوننت هم‌خوان نیست",
                error_code="workspace_mismatch",
                status_code=422,
            )
        next_number = (
            await self.versions.max_version_number(tenant_id, body.component_id) + 1
        )
        entity = ExperienceComponentVersion(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            component_id=body.component_id,
            version_number=next_number,
            status=ComponentVersionStatus.DRAFT,
            definition_schema=ensure_schema_payload(
                body.definition_schema, "definition_schema"
            ),
            props_defaults=ensure_schema_payload(
                body.props_defaults, "props_defaults"
            ),
            render_hint=ensure_schema_payload(body.render_hint, "render_hint"),
            changelog=body.changelog,
            metadata_json=body.metadata_json,
            is_immutable=False,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        await self.versions.add(entity)
        component.latest_version_number = next_number
        component.updated_by = _actor_id(actor)
        component.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="component_version",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(
                {"component_id": body.component_id, "version_number": next_number}
            ),
        )
        self.events.publish(
            event_type=ExperienceEventType.COMPONENT_VERSION_CREATED,
            aggregate_type="component_version",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "component_id": str(body.component_id),
                "version_number": next_number,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get_version(
        self, tenant_id: UUID, version_id: UUID
    ) -> ExperienceComponentVersion:
        entity = await self.versions.get(tenant_id, version_id)
        if entity is None:
            raise NotFoundError("نسخه کامپوننت یافت نشد")
        return entity

    async def list_versions(
        self,
        tenant_id: UUID,
        *,
        offset: int,
        limit: int,
        spec: ComponentVersionListSpec | None = None,
    ) -> tuple[list[ExperienceComponentVersion], int]:
        items, total = await self.versions.list_filtered(
            tenant_id, offset=offset, limit=limit, spec=spec
        )
        return list(items), total

    async def update_version(
        self,
        tenant_id: UUID,
        version_id: UUID,
        body: ExperienceComponentVersionUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceComponentVersion:
        entity = await self.get_version(tenant_id, version_id)
        self.version_policy.assert_can_update(entity)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        for field in ("definition_schema", "props_defaults", "render_hint"):
            if field in data:
                data[field] = ensure_schema_payload(data[field], field)
        _apply_update(entity, data)
        entity.updated_by = _actor_id(actor)
        entity.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="component_version",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=ExperienceEventType.COMPONENT_VERSION_UPDATED,
            aggregate_type="component_version",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "component_id": str(entity.component_id),
                "version_number": entity.version_number,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def publish_version(
        self,
        tenant_id: UUID,
        version_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceComponentVersion:
        entity = await self.get_version(tenant_id, version_id)
        self.version_policy.assert_publish(current=entity.status)
        component = await self.get_component(tenant_id, entity.component_id)
        now = datetime.now(timezone.utc)
        entity.status = ComponentVersionStatus.PUBLISHED
        entity.is_immutable = True
        entity.published_at = now
        entity.updated_by = _actor_id(actor)
        entity.version += 1
        component.published_version_id = entity.id
        if component.status == ComponentStatus.DRAFT:
            component.status = ComponentStatus.ACTIVE
        component.updated_by = _actor_id(actor)
        component.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="component_version",
            entity_id=entity.id,
            action=AuditAction.PUBLISH,
            actor_user_id=_actor_id(actor),
            changes={"status": "published", "is_immutable": True},
        )
        self.events.publish(
            event_type=ExperienceEventType.COMPONENT_VERSION_PUBLISHED,
            aggregate_type="component_version",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "component_id": str(entity.component_id),
                "version_number": entity.version_number,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def retire_version(
        self,
        tenant_id: UUID,
        version_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceComponentVersion:
        entity = await self.get_version(tenant_id, version_id)
        self.version_policy.assert_retire(current=entity.status)
        usage = await self.placements.count_by_version(tenant_id, version_id)
        if usage > 0:
            raise AppError(
                "نسخه دارای جایگذاری فعال است",
                error_code="component_version_in_use",
                status_code=409,
                details={"placement_count": usage},
            )
        component = await self.get_component(tenant_id, entity.component_id)
        entity.status = ComponentVersionStatus.RETIRED
        entity.retired_at = datetime.now(timezone.utc)
        entity.updated_by = _actor_id(actor)
        entity.version += 1
        if component.published_version_id == entity.id:
            component.published_version_id = None
            component.updated_by = _actor_id(actor)
            component.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="component_version",
            entity_id=entity.id,
            action=AuditAction.STATUS_CHANGE,
            actor_user_id=_actor_id(actor),
            changes={"status": "retired"},
        )
        self.events.publish(
            event_type=ExperienceEventType.COMPONENT_VERSION_RETIRED,
            aggregate_type="component_version",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "component_id": str(entity.component_id),
                "version_number": entity.version_number,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    # --- Placements ---

    async def create_placement(
        self,
        tenant_id: UUID,
        body: PageComponentPlacementCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> PageComponentPlacement:
        site = await self.sites.get(tenant_id, body.site_id)
        if site is None:
            raise NotFoundError("سایت یافت نشد")
        page = await self.pages.get(tenant_id, body.page_id)
        if page is None:
            raise NotFoundError("صفحه یافت نشد")
        if page.site_id != body.site_id:
            raise AppError(
                "page_id با site_id هم‌خوان نیست",
                error_code="page_site_mismatch",
                status_code=422,
            )
        component = await self.get_component(tenant_id, body.component_id)
        version = await self.get_version(tenant_id, body.component_version_id)
        if version.component_id != body.component_id:
            raise AppError(
                "component_version_id با component_id هم‌خوان نیست",
                error_code="version_component_mismatch",
                status_code=422,
            )
        self.placement_policy.assert_component_placeable(component)
        self.placement_policy.assert_version_placeable(version)
        slot_key = validate_slot_key(body.slot_key)
        if await self.placements.get_by_slot(tenant_id, body.page_id, slot_key):
            raise AppError(
                "slot_key در این صفحه تکراری است",
                error_code="duplicate_slot",
                status_code=409,
            )
        entity = PageComponentPlacement(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            site_id=body.site_id,
            page_id=body.page_id,
            component_id=body.component_id,
            component_version_id=body.component_version_id,
            slot_key=slot_key,
            sort_order=body.sort_order,
            props_override=ensure_schema_payload(
                body.props_override, "props_override"
            ),
            is_visible=body.is_visible,
            metadata_json=body.metadata_json,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        try:
            await self.placements.add(entity)
        except IntegrityError as exc:
            raise AppError(
                "slot_key در این صفحه تکراری است",
                error_code="duplicate_slot",
                status_code=409,
            ) from exc
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="page_component_placement",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=ExperienceEventType.PAGE_COMPONENT_PLACED,
            aggregate_type="page_component_placement",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "page_id": str(entity.page_id),
                "component_id": str(entity.component_id),
                "slot_key": entity.slot_key,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get_placement(
        self, tenant_id: UUID, placement_id: UUID
    ) -> PageComponentPlacement:
        entity = await self.placements.get(tenant_id, placement_id)
        if entity is None:
            raise NotFoundError("جایگذاری کامپوننت یافت نشد")
        return entity

    async def list_placements(
        self,
        tenant_id: UUID,
        *,
        offset: int,
        limit: int,
        spec: PlacementListSpec | None = None,
    ) -> tuple[list[PageComponentPlacement], int]:
        items, total = await self.placements.list_filtered(
            tenant_id, offset=offset, limit=limit, spec=spec
        )
        return list(items), total

    async def update_placement(
        self,
        tenant_id: UUID,
        placement_id: UUID,
        body: PageComponentPlacementUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> PageComponentPlacement:
        entity = await self.get_placement(tenant_id, placement_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "component_version_id" in data and data["component_version_id"] is not None:
            version = await self.get_version(tenant_id, data["component_version_id"])
            if version.component_id != entity.component_id:
                raise AppError(
                    "component_version_id با component_id هم‌خوان نیست",
                    error_code="version_component_mismatch",
                    status_code=422,
                )
            self.placement_policy.assert_version_placeable(version)
        if "slot_key" in data and data["slot_key"] is not None:
            slot_key = validate_slot_key(data["slot_key"])
            existing = await self.placements.get_by_slot(
                tenant_id, entity.page_id, slot_key
            )
            if existing is not None and existing.id != entity.id:
                raise AppError(
                    "slot_key در این صفحه تکراری است",
                    error_code="duplicate_slot",
                    status_code=409,
                )
            data["slot_key"] = slot_key
        if "props_override" in data:
            data["props_override"] = ensure_schema_payload(
                data["props_override"], "props_override"
            )
        _apply_update(entity, data)
        entity.updated_by = _actor_id(actor)
        entity.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="page_component_placement",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=ExperienceEventType.PAGE_COMPONENT_UPDATED,
            aggregate_type="page_component_placement",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "page_id": str(entity.page_id),
                "slot_key": entity.slot_key,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def soft_delete_placement(
        self,
        tenant_id: UUID,
        placement_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> PageComponentPlacement:
        entity = await self.get_placement(tenant_id, placement_id)
        await self.placements.soft_delete(entity, deleted_by=_actor_id(actor))
        entity.updated_by = _actor_id(actor)
        entity.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="page_component_placement",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
            changes={"is_deleted": True},
        )
        self.events.publish(
            event_type=ExperienceEventType.PAGE_COMPONENT_REMOVED,
            aggregate_type="page_component_placement",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "page_id": str(entity.page_id),
                "slot_key": entity.slot_key,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity
