"""Bundles, Core licensing references and feature toggles — Phase 11.8."""
from datetime import datetime, timezone
from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.events.publisher import get_event_publisher
from app.events.types import ExperienceEventType
from app.models.bundles import (
    ExperienceBundleDefinition,
    ExperienceFeatureToggle,
    ExperienceLicenseBinding,
    ExperienceTenantBundle,
)
from app.models.types import AuditAction, BundleDefinitionStatus, TenantBundleStatus
from app.policies.bundles import (
    BundleDefinitionPolicy,
    FeatureTogglePolicy,
    LicenseBindingPolicy,
    TenantBundlePolicy,
)
from app.repositories.bundles import (
    BundleDefinitionRepository,
    FeatureToggleRepository,
    LicenseBindingRepository,
    TenantBundleRepository,
)
from app.repositories.foundation import ExperienceWorkspaceRepository
from app.repositories.sites import ExperienceSiteRepository
from app.services.audit_service import AuditService
from app.validators import ensure_optimistic_version
from app.validators.bundles import validate_entitlement_ref, validate_metadata
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


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


def _jsonable(value):
    if isinstance(value, dict):
        return {key: _jsonable(item) for key, item in value.items()}
    if isinstance(value, (list, tuple)):
        return [_jsonable(item) for item in value]
    if isinstance(value, (UUID, datetime)):
        return value.isoformat() if isinstance(value, datetime) else str(value)
    if hasattr(value, "value"):
        return value.value
    return value


class ExperienceBundlesService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.definitions = BundleDefinitionRepository(session)
        self.tenant_bundles = TenantBundleRepository(session)
        self.licenses = LicenseBindingRepository(session)
        self.toggles = FeatureToggleRepository(session)
        self.workspaces = ExperienceWorkspaceRepository(session)
        self.sites = ExperienceSiteRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()
        self.policies = {
            "bundle_definition": BundleDefinitionPolicy(),
            "tenant_bundle": TenantBundlePolicy(),
            "license_binding": LicenseBindingPolicy(),
            "feature_toggle": FeatureTogglePolicy(),
        }

    async def _scope(self, tenant_id, workspace_id, site_id=None):
        if await self.workspaces.get(tenant_id, workspace_id) is None:
            raise NotFoundError("فضای کاری یافت نشد")
        if site_id is not None:
            site = await self.sites.get(tenant_id, site_id)
            if site is None or site.workspace_id != workspace_id:
                raise NotFoundError("سایت یافت نشد")

    async def _finish(self, entity, kind, action, event, actor, changes=None):
        await self.audit.record(
            tenant_id=entity.tenant_id,
            entity_type=kind,
            entity_id=entity.id,
            action=action,
            actor_user_id=_actor(actor),
            changes=_jsonable(changes or {}),
        )
        self.events.publish(
            event_type=event,
            aggregate_type=kind,
            aggregate_id=entity.id,
            tenant_id=entity.tenant_id,
            payload={"status": entity.status.value},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def _get(self, repo, tenant_id, entity_id, label):
        entity = await repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError(f"{label} یافت نشد")
        return entity

    async def _list(self, repo, tenant_id, *, offset, limit, spec):
        items, total = await repo.list_filtered(tenant_id, offset=offset, limit=limit, spec=spec)
        return list(items), total

    async def _update(self, repo, tenant_id, entity_id, body, kind, policy, event, actor):
        entity = await self._get(repo, tenant_id, entity_id, kind)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "metadata_json" in data:
            data["metadata_json"] = validate_metadata(data["metadata_json"])
        if "core_entitlement_ref" in data:
            data["core_entitlement_ref"] = validate_entitlement_ref(
                data["core_entitlement_ref"], required=kind == "license_binding"
            )
        if data.get("status") is not None:
            policy.assert_status_change(current=entity.status, target=data["status"])
        if kind == "tenant_bundle" and data.get("status") == TenantBundleStatus.ACTIVE:
            definition = await self._get(
                self.definitions, tenant_id, entity.bundle_definition_id, "bundle definition"
            )
            if definition.status != BundleDefinitionStatus.AVAILABLE:
                raise AppError("بسته غیرفعال قابل فعال‌سازی نیست", error_code="bundle_definition_unavailable", status_code=409)
            data["activated_at"] = datetime.now(timezone.utc)
            data["activated_by"] = _actor(actor)
        for field, value in data.items():
            setattr(entity, field, value)
        entity.updated_by = _actor(actor)
        entity.version += 1
        return await self._finish(entity, kind, AuditAction.UPDATE, event, actor, data)

    async def _delete(self, repo, tenant_id, entity_id, kind, event, actor):
        entity = await self._get(repo, tenant_id, entity_id, kind)
        await repo.soft_delete(entity, deleted_by=_actor(actor))
        entity.updated_by = _actor(actor)
        entity.version += 1
        return await self._finish(entity, kind, AuditAction.DELETE, event, actor)

    async def create_bundle_definition(self, tenant_id, body, *, actor=None):
        if body.workspace_id is not None:
            await self._scope(tenant_id, body.workspace_id)
        entity = ExperienceBundleDefinition(
            tenant_id=tenant_id, **body.model_dump(exclude={"metadata_json"}),
            metadata_json=validate_metadata(body.metadata_json), created_by=_actor(actor), updated_by=_actor(actor)
        )
        await self.definitions.add(entity)
        return await self._finish(entity, "bundle_definition", AuditAction.CREATE, ExperienceEventType.BUNDLE_DEFINITION_CREATED, actor)

    async def get_bundle_definition(self, tenant_id, entity_id):
        return await self._get(self.definitions, tenant_id, entity_id, "bundle definition")

    async def list_bundle_definitions(self, tenant_id, *, offset, limit, spec=None):
        return await self._list(self.definitions, tenant_id, offset=offset, limit=limit, spec=spec)

    async def update_bundle_definition(self, tenant_id, entity_id, body, *, actor=None):
        return await self._update(self.definitions, tenant_id, entity_id, body, "bundle_definition", self.policies["bundle_definition"], ExperienceEventType.BUNDLE_DEFINITION_UPDATED, actor)

    async def delete_bundle_definition(self, tenant_id, entity_id, *, actor=None):
        return await self._delete(self.definitions, tenant_id, entity_id, "bundle_definition", ExperienceEventType.BUNDLE_DEFINITION_DELETED, actor)

    async def create_tenant_bundle(self, tenant_id, body, *, actor=None):
        await self._scope(tenant_id, body.workspace_id, body.site_id)
        definition = await self.get_bundle_definition(tenant_id, body.bundle_definition_id)
        if body.status == TenantBundleStatus.ACTIVE and definition.status != BundleDefinitionStatus.AVAILABLE:
            raise AppError("بسته غیرفعال قابل فعال‌سازی نیست", error_code="bundle_definition_unavailable", status_code=409)
        now = datetime.now(timezone.utc) if body.status == TenantBundleStatus.ACTIVE else None
        entity = ExperienceTenantBundle(
            tenant_id=tenant_id, workspace_id=body.workspace_id, site_id=body.site_id,
            bundle_definition_id=definition.id, bundle_key=definition.bundle_key, status=body.status,
            core_entitlement_ref=validate_entitlement_ref(body.core_entitlement_ref),
            activated_at=now, activated_by=_actor(actor) if now else None, expires_at=body.expires_at,
            metadata_json=validate_metadata(body.metadata_json), created_by=_actor(actor), updated_by=_actor(actor)
        )
        await self.tenant_bundles.add(entity)
        return await self._finish(entity, "tenant_bundle", AuditAction.CREATE, ExperienceEventType.TENANT_BUNDLE_CREATED, actor)

    async def get_tenant_bundle(self, tenant_id, entity_id):
        return await self._get(self.tenant_bundles, tenant_id, entity_id, "tenant bundle")

    async def list_tenant_bundles(self, tenant_id, *, offset, limit, spec=None):
        return await self._list(self.tenant_bundles, tenant_id, offset=offset, limit=limit, spec=spec)

    async def update_tenant_bundle(self, tenant_id, entity_id, body, *, actor=None):
        return await self._update(self.tenant_bundles, tenant_id, entity_id, body, "tenant_bundle", self.policies["tenant_bundle"], ExperienceEventType.TENANT_BUNDLE_UPDATED, actor)

    async def delete_tenant_bundle(self, tenant_id, entity_id, *, actor=None):
        return await self._delete(self.tenant_bundles, tenant_id, entity_id, "tenant_bundle", ExperienceEventType.TENANT_BUNDLE_DELETED, actor)

    async def create_license_binding(self, tenant_id, body, *, actor=None):
        await self._scope(tenant_id, body.workspace_id)
        bundle_key = body.bundle_key
        if body.tenant_bundle_id is not None:
            bundle = await self.get_tenant_bundle(tenant_id, body.tenant_bundle_id)
            if bundle.workspace_id != body.workspace_id:
                raise AppError("بسته متعلق به فضای کاری نیست", error_code="bundle_workspace_mismatch", status_code=422)
            bundle_key = bundle.bundle_key
        entity = ExperienceLicenseBinding(
            tenant_id=tenant_id, workspace_id=body.workspace_id, tenant_bundle_id=body.tenant_bundle_id,
            bundle_key=bundle_key, core_entitlement_ref=validate_entitlement_ref(body.core_entitlement_ref, required=True),
            status=body.status, starts_at=body.starts_at, ends_at=body.ends_at,
            metadata_json=validate_metadata(body.metadata_json), created_by=_actor(actor), updated_by=_actor(actor)
        )
        await self.licenses.add(entity)
        return await self._finish(entity, "license_binding", AuditAction.CREATE, ExperienceEventType.LICENSE_BINDING_CREATED, actor)

    async def get_license_binding(self, tenant_id, entity_id):
        return await self._get(self.licenses, tenant_id, entity_id, "license binding")

    async def list_license_bindings(self, tenant_id, *, offset, limit, spec=None):
        return await self._list(self.licenses, tenant_id, offset=offset, limit=limit, spec=spec)

    async def update_license_binding(self, tenant_id, entity_id, body, *, actor=None):
        return await self._update(self.licenses, tenant_id, entity_id, body, "license_binding", self.policies["license_binding"], ExperienceEventType.LICENSE_BINDING_UPDATED, actor)

    async def delete_license_binding(self, tenant_id, entity_id, *, actor=None):
        return await self._delete(self.licenses, tenant_id, entity_id, "license_binding", ExperienceEventType.LICENSE_BINDING_DELETED, actor)

    async def create_feature_toggle(self, tenant_id, body, *, actor=None):
        await self._scope(tenant_id, body.workspace_id, body.site_id)
        entity = ExperienceFeatureToggle(
            tenant_id=tenant_id, **body.model_dump(exclude={"metadata_json"}),
            metadata_json=validate_metadata(body.metadata_json), created_by=_actor(actor), updated_by=_actor(actor)
        )
        await self.toggles.add(entity)
        return await self._finish(entity, "feature_toggle", AuditAction.CREATE, ExperienceEventType.FEATURE_TOGGLE_CREATED, actor)

    async def get_feature_toggle(self, tenant_id, entity_id):
        return await self._get(self.toggles, tenant_id, entity_id, "feature toggle")

    async def list_feature_toggles(self, tenant_id, *, offset, limit, spec=None):
        return await self._list(self.toggles, tenant_id, offset=offset, limit=limit, spec=spec)

    async def update_feature_toggle(self, tenant_id, entity_id, body, *, actor=None):
        return await self._update(self.toggles, tenant_id, entity_id, body, "feature_toggle", self.policies["feature_toggle"], ExperienceEventType.FEATURE_TOGGLE_UPDATED, actor)

    async def delete_feature_toggle(self, tenant_id, entity_id, *, actor=None):
        return await self._delete(self.toggles, tenant_id, entity_id, "feature_toggle", ExperienceEventType.FEATURE_TOGGLE_DELETED, actor)
