"""Localization & media-ref application services — Phase 11.5."""
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.localization import (
    ExperienceLocalization,
    ExperienceMediaRef,
    SiteLocaleBinding,
)
from app.models.types import (
    AuditAction,
    LocalizationStatus,
    MediaRefStatus,
    TextDirection,
)
from app.policies.localization import (
    LocalizationPolicy,
    MediaRefPolicy,
    SiteLocaleBindingPolicy,
)
from app.repositories.foundation import (
    ExperienceLocaleProfileRepository,
    ExperienceWorkspaceRepository,
)
from app.repositories.localization import (
    ExperienceLocalizationRepository,
    ExperienceMediaRefRepository,
    SiteLocaleBindingRepository,
)
from app.repositories.sites import ExperiencePageRepository, ExperienceSiteRepository
from app.schemas.localization import (
    ExperienceLocalizationUpdate,
    ExperienceLocalizationUpsert,
    ExperienceMediaRefCreate,
    ExperienceMediaRefUpdate,
    SiteLocaleBindingCreate,
    SiteLocaleBindingUpdate,
)
from app.services.audit_service import AuditService
from app.specifications.localization import (
    LocalizationListSpec,
    MediaRefListSpec,
    SiteLocaleBindingListSpec,
)
from app.validators import ensure_optimistic_version
from app.validators.localization import (
    ensure_content_json,
    ensure_no_binary_media_payload,
    validate_locale_code,
    validate_localization_target,
    validate_media_kind,
    validate_storage_file_ref,
    validate_text_direction,
)
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 ExperienceLocalizationService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.bindings = SiteLocaleBindingRepository(session)
        self.localizations = ExperienceLocalizationRepository(session)
        self.media_refs = ExperienceMediaRefRepository(session)
        self.workspaces = ExperienceWorkspaceRepository(session)
        self.locale_profiles = ExperienceLocaleProfileRepository(session)
        self.sites = ExperienceSiteRepository(session)
        self.pages = ExperiencePageRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()
        self.binding_policy = SiteLocaleBindingPolicy()
        self.localization_policy = LocalizationPolicy()
        self.media_policy = MediaRefPolicy()

    # ── Site locale bindings ──────────────────────────────────────────

    async def create_site_locale_binding(
        self,
        tenant_id: UUID,
        body: SiteLocaleBindingCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> SiteLocaleBinding:
        ws = await self.workspaces.get(tenant_id, body.workspace_id)
        if ws is None:
            raise NotFoundError("فضای کاری یافت نشد")
        site = await self.sites.get(tenant_id, body.site_id)
        if site is None or site.workspace_id != body.workspace_id:
            raise NotFoundError("سایت یافت نشد")
        profile = await self.locale_profiles.get(tenant_id, body.locale_profile_id)
        self.binding_policy.assert_profile_usable(profile)
        if profile.workspace_id != body.workspace_id:
            raise AppError(
                "پروفایل locale متعلق به این فضای کاری نیست",
                error_code="locale_profile_workspace_mismatch",
                status_code=422,
            )
        if await self.bindings.get_by_site_profile(
            tenant_id, body.site_id, body.locale_profile_id
        ):
            raise AppError(
                "این پروفایل locale قبلاً به سایت متصل شده",
                error_code="duplicate_site_locale_binding",
                status_code=409,
            )

        direction = validate_text_direction(
            body.text_direction
            if body.text_direction is not None
            else getattr(profile, "text_direction", TextDirection.RTL)
        )
        locale_code = validate_locale_code(
            getattr(profile, "language", None) or getattr(profile, "code", "fa")
        )

        if body.is_default:
            await self.bindings.clear_default_for_site(tenant_id, body.site_id)

        entity = SiteLocaleBinding(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            site_id=body.site_id,
            locale_profile_id=body.locale_profile_id,
            locale_code=locale_code,
            text_direction=direction,
            is_default=body.is_default,
            is_active=body.is_active,
            sort_order=body.sort_order,
            metadata_json=body.metadata_json,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        try:
            await self.bindings.add(entity)
        except IntegrityError as exc:
            raise AppError(
                "اتصال locale تکراری است",
                error_code="duplicate_site_locale_binding",
                status_code=409,
            ) from exc

        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="site_locale_binding",
            entity_id=entity.id,
            action=AuditAction.ASSIGN,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=ExperienceEventType.SITE_LOCALE_ASSIGNED,
            aggregate_type="site_locale_binding",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "site_id": str(entity.site_id),
                "locale_profile_id": str(entity.locale_profile_id),
                "locale_code": entity.locale_code,
                "text_direction": entity.text_direction.value,
                "is_default": entity.is_default,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get_site_locale_binding(
        self, tenant_id: UUID, binding_id: UUID
    ) -> SiteLocaleBinding:
        entity = await self.bindings.get(tenant_id, binding_id)
        if entity is None:
            raise NotFoundError("اتصال locale سایت یافت نشد")
        return entity

    async def list_site_locale_bindings(
        self,
        tenant_id: UUID,
        *,
        offset: int,
        limit: int,
        spec: SiteLocaleBindingListSpec | None = None,
    ) -> tuple[list[SiteLocaleBinding], int]:
        items, total = await self.bindings.list_filtered(
            tenant_id, offset=offset, limit=limit, spec=spec
        )
        return list(items), total

    async def update_site_locale_binding(
        self,
        tenant_id: UUID,
        binding_id: UUID,
        body: SiteLocaleBindingUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> SiteLocaleBinding:
        entity = await self.get_site_locale_binding(tenant_id, binding_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "text_direction" in data and data["text_direction"] is not None:
            data["text_direction"] = validate_text_direction(data["text_direction"])
        becoming_default = data.get("is_default") is True and not entity.is_default
        if becoming_default:
            await self.bindings.clear_default_for_site(
                tenant_id, entity.site_id, except_id=entity.id
            )
        _apply_update(entity, data)
        entity.updated_by = _actor_id(actor)
        entity.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="site_locale_binding",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        event = (
            ExperienceEventType.SITE_LOCALE_REPLACED
            if becoming_default
            else ExperienceEventType.SITE_LOCALE_UPDATED
        )
        self.events.publish(
            event_type=event,
            aggregate_type="site_locale_binding",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "site_id": str(entity.site_id),
                "locale_code": entity.locale_code,
                "text_direction": entity.text_direction.value,
                "is_default": entity.is_default,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def soft_delete_site_locale_binding(
        self,
        tenant_id: UUID,
        binding_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> SiteLocaleBinding:
        entity = await self.get_site_locale_binding(tenant_id, binding_id)
        await self.bindings.soft_delete(entity, deleted_by=_actor_id(actor))
        entity.is_active = False
        entity.updated_by = _actor_id(actor)
        entity.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="site_locale_binding",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
            changes={"is_deleted": True},
        )
        self.events.publish(
            event_type=ExperienceEventType.SITE_LOCALE_UNASSIGNED,
            aggregate_type="site_locale_binding",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "site_id": str(entity.site_id),
                "locale_code": entity.locale_code,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    # ── Localizations ─────────────────────────────────────────────────

    async def upsert_localization(
        self,
        tenant_id: UUID,
        body: ExperienceLocalizationUpsert,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceLocalization:
        ws = await self.workspaces.get(tenant_id, body.workspace_id)
        if ws is None:
            raise NotFoundError("فضای کاری یافت نشد")
        target_type = validate_localization_target(body.target_type)
        locale = validate_locale_code(body.locale)
        direction = validate_text_direction(body.text_direction)
        content_json = ensure_content_json(body.content_json)

        existing = await self.localizations.get_by_target_locale(
            tenant_id, target_type, body.target_id, locale
        )
        if existing is not None:
            if body.version is not None:
                ensure_optimistic_version(existing, body.version)
            if body.status is not None and body.status != existing.status:
                self.localization_policy.assert_status_change(
                    current=existing.status, target=body.status
                )
            existing.text_direction = direction
            existing.title = body.title
            existing.summary = body.summary
            existing.content_json = content_json
            existing.status = body.status
            existing.metadata_json = body.metadata_json
            existing.updated_by = _actor_id(actor)
            existing.version += 1
            await self.audit.record(
                tenant_id=tenant_id,
                entity_type="localization",
                entity_id=existing.id,
                action=AuditAction.UPDATE,
                actor_user_id=_actor_id(actor),
                changes=_jsonable_changes(body.model_dump(exclude={"version"})),
            )
            self.events.publish(
                event_type=ExperienceEventType.LOCALIZATION_UPSERTED,
                aggregate_type="localization",
                aggregate_id=existing.id,
                tenant_id=tenant_id,
                payload={
                    "target_type": existing.target_type.value,
                    "target_id": str(existing.target_id),
                    "locale": existing.locale,
                    "text_direction": existing.text_direction.value,
                },
            )
            await self.session.commit()
            await self.session.refresh(existing)
            return existing

        entity = ExperienceLocalization(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            target_type=target_type,
            target_id=body.target_id,
            locale=locale,
            text_direction=direction,
            title=body.title,
            summary=body.summary,
            content_json=content_json,
            status=body.status,
            metadata_json=body.metadata_json,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        try:
            await self.localizations.add(entity)
        except IntegrityError as exc:
            raise AppError(
                "localization تکراری است",
                error_code="duplicate_localization",
                status_code=409,
            ) from exc
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="localization",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump(exclude={"version"})),
        )
        self.events.publish(
            event_type=ExperienceEventType.LOCALIZATION_UPSERTED,
            aggregate_type="localization",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "target_type": entity.target_type.value,
                "target_id": str(entity.target_id),
                "locale": entity.locale,
                "text_direction": entity.text_direction.value,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get_localization(
        self, tenant_id: UUID, localization_id: UUID
    ) -> ExperienceLocalization:
        entity = await self.localizations.get(tenant_id, localization_id)
        if entity is None:
            raise NotFoundError("localization یافت نشد")
        return entity

    async def list_localizations(
        self,
        tenant_id: UUID,
        *,
        offset: int,
        limit: int,
        spec: LocalizationListSpec | None = None,
    ) -> tuple[list[ExperienceLocalization], int]:
        items, total = await self.localizations.list_filtered(
            tenant_id, offset=offset, limit=limit, spec=spec
        )
        return list(items), total

    async def update_localization(
        self,
        tenant_id: UUID,
        localization_id: UUID,
        body: ExperienceLocalizationUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceLocalization:
        entity = await self.get_localization(tenant_id, localization_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "text_direction" in data and data["text_direction"] is not None:
            data["text_direction"] = validate_text_direction(data["text_direction"])
        if "content_json" in data:
            data["content_json"] = ensure_content_json(data["content_json"])
        if "status" in data and data["status"] is not None:
            self.localization_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="localization",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=ExperienceEventType.LOCALIZATION_UPDATED,
            aggregate_type="localization",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "locale": entity.locale,
                "text_direction": entity.text_direction.value,
                "status": entity.status.value,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def soft_delete_localization(
        self,
        tenant_id: UUID,
        localization_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceLocalization:
        entity = await self.get_localization(tenant_id, localization_id)
        await self.localizations.soft_delete(entity, deleted_by=_actor_id(actor))
        entity.status = LocalizationStatus.ARCHIVED
        entity.updated_by = _actor_id(actor)
        entity.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="localization",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
            changes={"is_deleted": True},
        )
        self.events.publish(
            event_type=ExperienceEventType.LOCALIZATION_ARCHIVED,
            aggregate_type="localization",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"locale": entity.locale},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    # ── Media refs ────────────────────────────────────────────────────

    async def create_media_ref(
        self,
        tenant_id: UUID,
        body: ExperienceMediaRefCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceMediaRef:
        ws = await self.workspaces.get(tenant_id, body.workspace_id)
        if ws is None:
            raise NotFoundError("فضای کاری یافت نشد")
        if body.site_id is not None:
            site = await self.sites.get(tenant_id, body.site_id)
            if site is None or site.workspace_id != body.workspace_id:
                raise NotFoundError("سایت یافت نشد")
        if body.page_id is not None:
            page = await self.pages.get(tenant_id, body.page_id)
            if page is None:
                raise NotFoundError("صفحه یافت نشد")
            if body.site_id is not None and page.site_id != body.site_id:
                raise AppError(
                    "صفحه متعلق به سایت اعلام‌شده نیست",
                    error_code="page_site_mismatch",
                    status_code=422,
                )

        storage_file_ref = self.media_policy.assert_storage_ref_only(
            storage_file_ref=body.storage_file_ref,
            metadata_json=body.metadata_json,
        )
        kind = validate_media_kind(body.kind)
        direction = validate_text_direction(body.text_direction, required=False)
        locale = validate_locale_code(body.locale) if body.locale else None

        entity = ExperienceMediaRef(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            site_id=body.site_id,
            page_id=body.page_id,
            owner_type=body.owner_type,
            owner_id=body.owner_id,
            kind=kind,
            storage_file_ref=storage_file_ref,
            alt_text=body.alt_text,
            caption=body.caption,
            locale=locale,
            text_direction=direction,
            sort_order=body.sort_order,
            status=body.status,
            metadata_json=ensure_no_binary_media_payload(body.metadata_json),
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        await self.media_refs.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="media_ref",
            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.MEDIA_REF_CREATED,
            aggregate_type="media_ref",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "storage_file_ref": entity.storage_file_ref,
                "kind": entity.kind.value,
                "locale": entity.locale,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get_media_ref(
        self, tenant_id: UUID, media_ref_id: UUID
    ) -> ExperienceMediaRef:
        entity = await self.media_refs.get(tenant_id, media_ref_id)
        if entity is None:
            raise NotFoundError("media ref یافت نشد")
        return entity

    async def list_media_refs(
        self,
        tenant_id: UUID,
        *,
        offset: int,
        limit: int,
        spec: MediaRefListSpec | None = None,
    ) -> tuple[list[ExperienceMediaRef], int]:
        items, total = await self.media_refs.list_filtered(
            tenant_id, offset=offset, limit=limit, spec=spec
        )
        return list(items), total

    async def update_media_ref(
        self,
        tenant_id: UUID,
        media_ref_id: UUID,
        body: ExperienceMediaRefUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceMediaRef:
        entity = await self.get_media_ref(tenant_id, media_ref_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "storage_file_ref" in data and data["storage_file_ref"] is not None:
            data["storage_file_ref"] = validate_storage_file_ref(
                data["storage_file_ref"]
            )
        if "metadata_json" in data:
            data["metadata_json"] = ensure_no_binary_media_payload(
                data["metadata_json"]
            )
        if "kind" in data and data["kind"] is not None:
            data["kind"] = validate_media_kind(data["kind"])
        if "locale" in data and data["locale"] is not None:
            data["locale"] = validate_locale_code(data["locale"])
        if "text_direction" in data:
            data["text_direction"] = validate_text_direction(
                data["text_direction"], required=False
            )
        if "status" in data and data["status"] is not None:
            self.media_policy.assert_status_change(
                current=entity.status, target=data["status"]
            )
        # Re-validate combined storage/binary constraint after merge.
        next_ref = data.get("storage_file_ref", entity.storage_file_ref)
        next_meta = data.get("metadata_json", entity.metadata_json)
        self.media_policy.assert_storage_ref_only(
            storage_file_ref=next_ref, metadata_json=next_meta
        )
        _apply_update(entity, data)
        entity.updated_by = _actor_id(actor)
        entity.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="media_ref",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=ExperienceEventType.MEDIA_REF_UPDATED,
            aggregate_type="media_ref",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "storage_file_ref": entity.storage_file_ref,
                "kind": entity.kind.value,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def soft_delete_media_ref(
        self,
        tenant_id: UUID,
        media_ref_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceMediaRef:
        entity = await self.get_media_ref(tenant_id, media_ref_id)
        await self.media_refs.soft_delete(entity, deleted_by=_actor_id(actor))
        entity.status = MediaRefStatus.ARCHIVED
        entity.updated_by = _actor_id(actor)
        entity.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="media_ref",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
            changes={"is_deleted": True},
        )
        self.events.publish(
            event_type=ExperienceEventType.MEDIA_REF_ARCHIVED,
            aggregate_type="media_ref",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"storage_file_ref": entity.storage_file_ref},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity
