"""Experience foundation application services — Phase 11.0."""
from __future__ import annotations

from datetime import date, datetime
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.foundation import (
    ExperienceConfiguration,
    ExperienceLocaleProfile,
    ExperienceWorkspace,
    ExperienceSetting,
    ExternalProviderConfig,
    RenderEngineRegistration,
)
from app.models.types import AuditAction
from app.repositories.foundation import (
    ExperienceConfigurationRepository,
    ExperienceLocaleProfileRepository,
    ExperienceWorkspaceRepository,
    ExperienceSettingRepository,
    ExternalProviderConfigRepository,
    RenderEngineRegistrationRepository,
)
from app.schemas.foundation import (
    ExperienceConfigurationCreate,
    ExperienceConfigurationUpdate,
    ExperienceLocaleProfileCreate,
    ExperienceLocaleProfileUpdate,
    ExperienceWorkspaceCreate,
    ExperienceWorkspaceUpdate,
    ExperienceSettingUpsert,
    ExternalProviderConfigCreate,
    ExternalProviderConfigUpdate,
    RenderEngineRegistrationCreate,
    RenderEngineRegistrationUpdate,
)
from app.services.audit_service import AuditService
from app.validators import (
    ensure_optimistic_version,
    validate_code,
    validate_currency_code,
    validate_non_empty,
)
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 ExperienceWorkspaceService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = ExperienceWorkspaceRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: ExperienceWorkspaceCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceWorkspace:
        code = validate_code(body.code)
        name = validate_non_empty(body.name, "name")
        currency = validate_currency_code(body.currency_code)
        if await self.repo.get_by_code(tenant_id, code):
            raise AppError(
                "کد فضای کاری تکراری است",
                error_code="duplicate_code",
                status_code=409,
            )
        entity = ExperienceWorkspace(
            tenant_id=tenant_id,
            code=code,
            name=name,
            description=body.description,
            status=body.status,
            legal_name=body.legal_name,
            timezone=body.timezone,
            language=body.language,
            currency_code=currency,
            settings=body.settings,
            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="workspace",
            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.WORKSPACE_CREATED,
            aggregate_type="workspace",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "name": entity.name},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> ExperienceWorkspace:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("فضای کاری یافت نشد")
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[ExperienceWorkspace]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: ExperienceWorkspaceUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceWorkspace:
        entity = await self.get(tenant_id, entity_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 "currency_code" in data and data["currency_code"] is not None:
            data["currency_code"] = validate_currency_code(data["currency_code"])
        _apply_update(entity, data)
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="workspace",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=ExperienceEventType.WORKSPACE_UPDATED,
            aggregate_type="workspace",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def soft_delete(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceWorkspace:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="workspace",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class ExperienceLocaleProfileService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = ExperienceLocaleProfileRepository(session)
        self.orgs = ExperienceWorkspaceRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: ExperienceLocaleProfileCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceLocaleProfile:
        org = await self.orgs.get(tenant_id, body.workspace_id)
        if org is None:
            raise NotFoundError("فضای کاری یافت نشد")
        code = validate_code(body.code)
        name = validate_non_empty(body.name, "name")
        entity = ExperienceLocaleProfile(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            code=code,
            name=name,
            description=body.description,
            status=body.status,
            language=body.language,
            timezone=body.timezone,
            text_direction=body.text_direction,
            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="locale_profile",
            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.LOCALE_PROFILE_CREATED,
            aggregate_type="locale_profile",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "workspace_id": str(entity.workspace_id)},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> ExperienceLocaleProfile:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("پروفایل زبان یافت نشد")
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[ExperienceLocaleProfile]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: ExperienceLocaleProfileUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceLocaleProfile:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "name" in data and data["name"] is not None:
            data["name"] = validate_non_empty(data["name"], "name")
        _apply_update(entity, data)
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="locale_profile",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=ExperienceEventType.LOCALE_PROFILE_UPDATED,
            aggregate_type="locale_profile",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def soft_delete(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceLocaleProfile:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="locale_profile",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class ExternalProviderConfigService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = ExternalProviderConfigRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: ExternalProviderConfigCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExternalProviderConfig:
        code = validate_code(body.code)
        name = validate_non_empty(body.name, "name")
        adapter_key = validate_non_empty(body.adapter_key, "adapter_key")
        if await self.repo.get_by_code(tenant_id, code):
            raise AppError(
                "کد ارائه‌دهنده تکراری است",
                error_code="duplicate_code",
                status_code=409,
            )
        entity = ExternalProviderConfig(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            code=code,
            name=name,
            provider_kind=body.provider_kind,
            adapter_key=adapter_key,
            status=body.status,
            priority=body.priority,
            credentials_ref=body.credentials_ref,
            config=body.config,
            capabilities=body.capabilities,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="external_provider",
            entity_id=entity.id,
            action=AuditAction.REGISTER,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=ExperienceEventType.EXTERNAL_PROVIDER_REGISTERED,
            aggregate_type="external_provider_config",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "adapter_key": entity.adapter_key},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> ExternalProviderConfig:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("ارائه‌دهنده یافت نشد")
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[ExternalProviderConfig]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: ExternalProviderConfigUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExternalProviderConfig:
        entity = await self.get(tenant_id, entity_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 "adapter_key" in data and data["adapter_key"] is not None:
            data["adapter_key"] = validate_non_empty(data["adapter_key"], "adapter_key")
        _apply_update(entity, data)
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="external_provider",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=ExperienceEventType.EXTERNAL_PROVIDER_UPDATED,
            aggregate_type="external_provider_config",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def soft_delete(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExternalProviderConfig:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="external_provider",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class RenderEngineRegistrationService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = RenderEngineRegistrationRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: RenderEngineRegistrationCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> RenderEngineRegistration:
        code = validate_code(body.code)
        name = validate_non_empty(body.name, "name")
        adapter_key = validate_non_empty(body.adapter_key, "adapter_key")
        entity = RenderEngineRegistration(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            code=code,
            name=name,
            adapter_key=adapter_key,
            status=body.status,
            is_default=body.is_default,
            config=body.config,
            capabilities=body.capabilities,
            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="render_engine",
            entity_id=entity.id,
            action=AuditAction.REGISTER,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=ExperienceEventType.RENDER_ENGINE_REGISTERED,
            aggregate_type="render_engine",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "adapter_key": entity.adapter_key},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> RenderEngineRegistration:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("موتور مسیریابی یافت نشد")
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[RenderEngineRegistration]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: RenderEngineRegistrationUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> RenderEngineRegistration:
        entity = await self.get(tenant_id, entity_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 "adapter_key" in data and data["adapter_key"] is not None:
            data["adapter_key"] = validate_non_empty(data["adapter_key"], "adapter_key")
        _apply_update(entity, data)
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="render_engine",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=ExperienceEventType.RENDER_ENGINE_UPDATED,
            aggregate_type="render_engine",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def soft_delete(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> RenderEngineRegistration:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="render_engine",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class ExperienceConfigurationService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = ExperienceConfigurationRepository(session)
        self.orgs = ExperienceWorkspaceRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: ExperienceConfigurationCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceConfiguration:
        org = await self.orgs.get(tenant_id, body.workspace_id)
        if org is None:
            raise NotFoundError("فضای کاری یافت نشد")
        code = validate_code(body.code)
        currency = validate_currency_code(body.currency_code)
        entity = ExperienceConfiguration(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            locale_profile_id=body.locale_profile_id,
            code=code,
            working_hours=body.working_hours,
            publish_policies=body.publish_policies,
            seo_policies=body.seo_policies,
            analytics_policies=body.analytics_policies,
            custom_fields=body.custom_fields,
            timezone=body.timezone,
            language=body.language,
            currency_code=currency,
            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="configuration",
            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.CONFIGURATION_CREATED,
            aggregate_type="configuration",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> ExperienceConfiguration:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("پیکربندی یافت نشد")
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[ExperienceConfiguration]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: ExperienceConfigurationUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceConfiguration:
        entity = await self.get(tenant_id, entity_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "currency_code" in data and data["currency_code"] is not None:
            data["currency_code"] = validate_currency_code(data["currency_code"])
        _apply_update(entity, data)
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="configuration",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=ExperienceEventType.CONFIGURATION_UPDATED,
            aggregate_type="configuration",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def soft_delete(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceConfiguration:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="configuration",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class ExperienceSettingService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = ExperienceSettingRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def upsert(
        self,
        tenant_id: UUID,
        body: ExperienceSettingUpsert,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceSetting:
        key = validate_non_empty(body.key, "key")
        existing = await self.repo.get_by_key(
            tenant_id,
            key,
            workspace_id=body.workspace_id,
            locale_profile_id=body.locale_profile_id,
        )
        if existing is None:
            entity = ExperienceSetting(
                tenant_id=tenant_id,
                workspace_id=body.workspace_id,
                locale_profile_id=body.locale_profile_id,
                key=key,
                value=body.value,
                description=body.description,
                created_by=_actor_id(actor),
                updated_by=_actor_id(actor),
            )
            await self.repo.add(entity)
        else:
            entity = existing
            entity.value = body.value
            entity.description = body.description
            entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="setting",
            entity_id=entity.id,
            action=AuditAction.UPDATE if existing else AuditAction.CREATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=ExperienceEventType.SETTING_UPSERTED,
            aggregate_type="setting",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"key": entity.key},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[ExperienceSetting]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))
