"""Sites & page application services — Phase 11.1."""
from __future__ import annotations

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

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.sites import ExperiencePage, ExperienceSite, ExperienceSiteDomain
from app.models.types import AuditAction, PublishStatus
from app.repositories.foundation import ExperienceWorkspaceRepository
from app.repositories.sites import (
    ExperiencePageRepository,
    ExperienceSiteDomainRepository,
    ExperienceSiteRepository,
)
from app.schemas.sites import (
    ExperiencePageCreate,
    ExperiencePageUpdate,
    ExperienceSiteCreate,
    ExperienceSiteDomainCreate,
    ExperienceSiteDomainUpdate,
    ExperienceSiteUpdate,
)
from app.services.audit_service import AuditService
from app.validators import ensure_optimistic_version, validate_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


def _normalize_slug(slug: str) -> str:
    slug = validate_non_empty(slug, "slug").strip().lower().replace(" ", "-")
    if "/" in slug or "\\" in slug:
        raise AppError("slug نامعتبر است", error_code="invalid_slug", status_code=422)
    return slug


def _normalize_host(host: str) -> str:
    host = validate_non_empty(host, "host").strip().lower()
    if "://" in host or "/" in host or " " in host:
        raise AppError("host نامعتبر است", error_code="invalid_host", status_code=422)
    return host


class ExperienceSiteService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = ExperienceSiteRepository(session)
        self.workspaces = ExperienceWorkspaceRepository(session)
        self.pages = ExperiencePageRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: ExperienceSiteCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceSite:
        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")
        if await self.repo.get_by_code(tenant_id, body.workspace_id, code):
            raise AppError(
                "کد سایت تکراری است",
                error_code="duplicate_code",
                status_code=409,
            )
        entity = ExperienceSite(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            code=code,
            name=name,
            description=body.description,
            publish_status=body.publish_status,
            default_locale=body.default_locale,
            default_direction=body.default_direction,
            primary_domain=body.primary_domain,
            settings=body.settings,
            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="site",
            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.SITE_CREATED,
            aggregate_type="site",
            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, site_id: UUID) -> ExperienceSite:
        entity = await self.repo.get(tenant_id, site_id)
        if entity is None:
            raise NotFoundError("سایت یافت نشد")
        return entity

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

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

    async def update(
        self,
        tenant_id: UUID,
        site_id: UUID,
        body: ExperienceSiteUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceSite:
        entity = await self.get(tenant_id, site_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "home_page_id" in data and data["home_page_id"] is not None:
            page = await self.pages.get(tenant_id, data["home_page_id"])
            if page is None or page.site_id != site_id:
                raise AppError(
                    "صفحه اصلی متعلق به این سایت نیست",
                    error_code="invalid_home_page",
                    status_code=422,
                )
        _apply_update(entity, data)
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="site",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=ExperienceEventType.SITE_UPDATED,
            aggregate_type="site",
            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 publish(
        self,
        tenant_id: UUID,
        site_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceSite:
        entity = await self.get(tenant_id, site_id)
        if entity.publish_status == PublishStatus.ARCHIVED:
            raise AppError(
                "سایت آرشیو شده قابل انتشار نیست",
                error_code="invalid_status",
                status_code=409,
            )
        entity.publish_status = PublishStatus.PUBLISHED
        entity.published_at = datetime.now(timezone.utc)
        entity.archived_at = None
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="site",
            entity_id=entity.id,
            action=AuditAction.PUBLISH,
            actor_user_id=_actor_id(actor),
        )
        self.events.publish(
            event_type=ExperienceEventType.SITE_PUBLISHED,
            aggregate_type="site",
            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 unpublish(
        self,
        tenant_id: UUID,
        site_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceSite:
        entity = await self.get(tenant_id, site_id)
        entity.publish_status = PublishStatus.DRAFT
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="site",
            entity_id=entity.id,
            action=AuditAction.UNPUBLISH,
            actor_user_id=_actor_id(actor),
        )
        self.events.publish(
            event_type=ExperienceEventType.SITE_UNPUBLISHED,
            aggregate_type="site",
            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 archive(
        self,
        tenant_id: UUID,
        site_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceSite:
        entity = await self.get(tenant_id, site_id)
        entity.publish_status = PublishStatus.ARCHIVED
        entity.archived_at = datetime.now(timezone.utc)
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="site",
            entity_id=entity.id,
            action=AuditAction.ARCHIVE,
            actor_user_id=_actor_id(actor),
        )
        self.events.publish(
            event_type=ExperienceEventType.SITE_ARCHIVED,
            aggregate_type="site",
            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,
        site_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceSite:
        entity = await self.get(tenant_id, site_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="site",
            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 ExperiencePageService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = ExperiencePageRepository(session)
        self.sites = ExperienceSiteRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: ExperiencePageCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperiencePage:
        site = await self.sites.get(tenant_id, body.site_id)
        if site is None:
            raise NotFoundError("سایت یافت نشد")
        if site.workspace_id != body.workspace_id:
            raise AppError(
                "workspace با سایت هم‌خوان نیست",
                error_code="workspace_mismatch",
                status_code=422,
            )
        code = validate_code(body.code)
        slug = _normalize_slug(body.slug)
        title = validate_non_empty(body.title, "title")
        if await self.repo.get_by_code(tenant_id, body.site_id, code):
            raise AppError(
                "کد صفحه تکراری است",
                error_code="duplicate_code",
                status_code=409,
            )
        if await self.repo.get_by_slug(tenant_id, body.site_id, slug):
            raise AppError(
                "slug صفحه تکراری است",
                error_code="duplicate_slug",
                status_code=409,
            )
        entity = ExperiencePage(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            site_id=body.site_id,
            code=code,
            slug=slug,
            title=title,
            summary=body.summary,
            page_type=body.page_type,
            publish_status=body.publish_status,
            locale=body.locale,
            text_direction=body.text_direction,
            sort_order=body.sort_order,
            is_home=body.is_home,
            content_shell=body.content_shell,
            seo_shell=body.seo_shell,
            external_refs=body.external_refs,
            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_page",
                status_code=409,
            ) from exc
        if body.is_home:
            site.home_page_id = entity.id
            site.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="page",
            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_CREATED,
            aggregate_type="page",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "code": entity.code,
                "site_id": str(entity.site_id),
                "page_type": entity.page_type.value,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, page_id: UUID) -> ExperiencePage:
        entity = await self.repo.get(tenant_id, page_id)
        if entity is None:
            raise NotFoundError("صفحه یافت نشد")
        return entity

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

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

    async def update(
        self,
        tenant_id: UUID,
        page_id: UUID,
        body: ExperiencePageUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperiencePage:
        entity = await self.get(tenant_id, page_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "slug" in data and data["slug"] is not None:
            data["slug"] = _normalize_slug(data["slug"])
            existing = await self.repo.get_by_slug(tenant_id, entity.site_id, data["slug"])
            if existing and existing.id != entity.id:
                raise AppError(
                    "slug صفحه تکراری است",
                    error_code="duplicate_slug",
                    status_code=409,
                )
        _apply_update(entity, data)
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        if data.get("is_home") is True:
            site = await self.sites.get(tenant_id, entity.site_id)
            if site:
                site.home_page_id = entity.id
                site.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="page",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=ExperienceEventType.PAGE_UPDATED,
            aggregate_type="page",
            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 publish(
        self,
        tenant_id: UUID,
        page_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperiencePage:
        entity = await self.get(tenant_id, page_id)
        if entity.publish_status == PublishStatus.ARCHIVED:
            raise AppError(
                "صفحه آرشیو شده قابل انتشار نیست",
                error_code="invalid_status",
                status_code=409,
            )
        entity.publish_status = PublishStatus.PUBLISHED
        entity.published_at = datetime.now(timezone.utc)
        entity.archived_at = None
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="page",
            entity_id=entity.id,
            action=AuditAction.PUBLISH,
            actor_user_id=_actor_id(actor),
        )
        self.events.publish(
            event_type=ExperienceEventType.PAGE_PUBLISHED,
            aggregate_type="page",
            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 unpublish(
        self,
        tenant_id: UUID,
        page_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperiencePage:
        entity = await self.get(tenant_id, page_id)
        entity.publish_status = PublishStatus.DRAFT
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="page",
            entity_id=entity.id,
            action=AuditAction.UNPUBLISH,
            actor_user_id=_actor_id(actor),
        )
        self.events.publish(
            event_type=ExperienceEventType.PAGE_UNPUBLISHED,
            aggregate_type="page",
            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 archive(
        self,
        tenant_id: UUID,
        page_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperiencePage:
        entity = await self.get(tenant_id, page_id)
        entity.publish_status = PublishStatus.ARCHIVED
        entity.archived_at = datetime.now(timezone.utc)
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="page",
            entity_id=entity.id,
            action=AuditAction.ARCHIVE,
            actor_user_id=_actor_id(actor),
        )
        self.events.publish(
            event_type=ExperienceEventType.PAGE_ARCHIVED,
            aggregate_type="page",
            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,
        page_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperiencePage:
        entity = await self.get(tenant_id, page_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="page",
            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 ExperienceSiteDomainService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = ExperienceSiteDomainRepository(session)
        self.sites = ExperienceSiteRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: ExperienceSiteDomainCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceSiteDomain:
        site = await self.sites.get(tenant_id, body.site_id)
        if site is None:
            raise NotFoundError("سایت یافت نشد")
        if site.workspace_id != body.workspace_id:
            raise AppError(
                "workspace با سایت هم‌خوان نیست",
                error_code="workspace_mismatch",
                status_code=422,
            )
        host = _normalize_host(body.host)
        if await self.repo.get_by_host(tenant_id, host):
            raise AppError(
                "دامنه تکراری است",
                error_code="duplicate_host",
                status_code=409,
            )
        entity = ExperienceSiteDomain(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            site_id=body.site_id,
            host=host,
            domain_kind=body.domain_kind,
            is_primary=body.is_primary,
            verification_token=uuid4().hex[:32],
            path_prefix=body.path_prefix,
            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_host",
                status_code=409,
            ) from exc
        if body.is_primary:
            site.primary_domain = host
            site.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="site_domain",
            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.SITE_DOMAIN_BOUND,
            aggregate_type="site_domain",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"host": entity.host, "site_id": str(entity.site_id)},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

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

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

    async def update(
        self,
        tenant_id: UUID,
        domain_id: UUID,
        body: ExperienceSiteDomainUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceSiteDomain:
        entity = await self.get(tenant_id, domain_id)
        data = body.model_dump(exclude_unset=True)
        if "host" in data and data["host"] is not None:
            data["host"] = _normalize_host(data["host"])
            existing = await self.repo.get_by_host(tenant_id, data["host"])
            if existing and existing.id != entity.id:
                raise AppError(
                    "دامنه تکراری است",
                    error_code="duplicate_host",
                    status_code=409,
                )
        _apply_update(entity, data)
        entity.updated_by = _actor_id(actor)
        if data.get("is_primary") is True:
            site = await self.sites.get(tenant_id, entity.site_id)
            if site:
                site.primary_domain = entity.host
                site.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="site_domain",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=ExperienceEventType.SITE_DOMAIN_UPDATED,
            aggregate_type="site_domain",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"host": entity.host},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

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