"""Template application services — Phase 11.4."""
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
from app.models.templates import (
    ExperienceTemplate,
    ExperienceTemplateVersion,
    TemplateInstantiation,
)
from app.models.types import (
    AuditAction,
    LayoutStatus,
    PageType,
    PublishStatus,
    TemplateInstantiationStatus,
    TemplateStatus,
    TemplateVersionStatus,
    ThemeStatus,
)
from app.policies.templates import (
    TemplateInstantiationPolicy,
    TemplateLifecyclePolicy,
    TemplateVersionPolicy,
)
from app.repositories.foundation import ExperienceWorkspaceRepository
from app.repositories.sites import ExperiencePageRepository, ExperienceSiteRepository
from app.repositories.templates import (
    ExperienceTemplateRepository,
    ExperienceTemplateVersionRepository,
    TemplateInstantiationRepository,
)
from app.repositories.themes import (
    ExperienceLayoutRepository,
    ExperienceThemeRepository,
)
from app.schemas.templates import (
    ExperienceTemplateCreate,
    ExperienceTemplateUpdate,
    ExperienceTemplateVersionCreate,
    ExperienceTemplateVersionUpdate,
    TemplateInstantiateRequest,
)
from app.services.audit_service import AuditService
from app.specifications.templates import (
    InstantiationListSpec,
    TemplateListSpec,
    TemplateVersionListSpec,
)
from app.validators import ensure_optimistic_version, validate_code, validate_non_empty
from app.validators.templates import (
    ensure_blueprint_json,
    validate_page_type,
)
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


class ExperienceTemplateService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = ExperienceTemplateRepository(session)
        self.versions = ExperienceTemplateVersionRepository(session)
        self.instantiations = TemplateInstantiationRepository(session)
        self.workspaces = ExperienceWorkspaceRepository(session)
        self.sites = ExperienceSiteRepository(session)
        self.pages = ExperiencePageRepository(session)
        self.themes = ExperienceThemeRepository(session)
        self.layouts = ExperienceLayoutRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()
        self.lifecycle_policy = TemplateLifecyclePolicy()
        self.version_policy = TemplateVersionPolicy()
        self.instantiation_policy = TemplateInstantiationPolicy()

    async def create_template(
        self,
        tenant_id: UUID,
        body: ExperienceTemplateCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceTemplate:
        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")
        page_type = validate_page_type(body.page_type)
        if await self.repo.get_by_code(tenant_id, body.workspace_id, code):
            raise AppError(
                "کد قالب تکراری است", error_code="duplicate_code", status_code=409
            )
        entity = ExperienceTemplate(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            code=code,
            name=name,
            description=body.description,
            page_type=page_type,
            status=body.status,
            category=body.category,
            tags=body.tags,
            is_starter_pack=body.is_starter_pack,
            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="template",
            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.TEMPLATE_CREATED,
            aggregate_type="template",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "code": entity.code,
                "page_type": entity.page_type,
                "workspace_id": str(entity.workspace_id),
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get_template(
        self, tenant_id: UUID, template_id: UUID
    ) -> ExperienceTemplate:
        entity = await self.repo.get(tenant_id, template_id)
        if entity is None:
            raise NotFoundError("قالب یافت نشد")
        return entity

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

    async def update_template(
        self,
        tenant_id: UUID,
        template_id: UUID,
        body: ExperienceTemplateUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceTemplate:
        entity = await self.get_template(tenant_id, template_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 "page_type" in data and data["page_type"] is not None:
            data["page_type"] = validate_page_type(data["page_type"])
        if "status" in data and data["status"] is not None:
            self.lifecycle_policy.assert_status_change(
                current=entity.status, target=data["status"]
            )
        _apply_update(entity, data)
        entity.updated_by = _actor_id(actor)
        entity.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="template",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=ExperienceEventType.TEMPLATE_UPDATED,
            aggregate_type="template",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "status": entity.status.value},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def activate_template(
        self, tenant_id: UUID, template_id: UUID, *, actor: CurrentUser | None = None
    ) -> ExperienceTemplate:
        return await self._set_template_status(
            tenant_id,
            template_id,
            TemplateStatus.ACTIVE,
            ExperienceEventType.TEMPLATE_ACTIVATED,
            actor=actor,
        )

    async def deprecate_template(
        self, tenant_id: UUID, template_id: UUID, *, actor: CurrentUser | None = None
    ) -> ExperienceTemplate:
        return await self._set_template_status(
            tenant_id,
            template_id,
            TemplateStatus.DEPRECATED,
            ExperienceEventType.TEMPLATE_DEPRECATED,
            actor=actor,
        )

    async def archive_template(
        self, tenant_id: UUID, template_id: UUID, *, actor: CurrentUser | None = None
    ) -> ExperienceTemplate:
        return await self._set_template_status(
            tenant_id,
            template_id,
            TemplateStatus.ARCHIVED,
            ExperienceEventType.TEMPLATE_ARCHIVED,
            actor=actor,
        )

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

    async def soft_delete_template(
        self, tenant_id: UUID, template_id: UUID, *, actor: CurrentUser | None = None
    ) -> ExperienceTemplate:
        entity = await self.get_template(tenant_id, template_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        entity.updated_by = _actor_id(actor)
        entity.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="template",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
            changes={"is_deleted": True},
        )
        self.events.publish(
            event_type=ExperienceEventType.TEMPLATE_UPDATED,
            aggregate_type="template",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "is_deleted": True},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    # --- Versions ---

    async def create_version(
        self,
        tenant_id: UUID,
        body: ExperienceTemplateVersionCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceTemplateVersion:
        template = await self.get_template(tenant_id, body.template_id)
        if template.workspace_id != body.workspace_id:
            raise AppError(
                "workspace_id با قالب هم‌خوان نیست",
                error_code="workspace_mismatch",
                status_code=422,
            )
        next_number = (
            await self.versions.max_version_number(tenant_id, body.template_id) + 1
        )
        entity = ExperienceTemplateVersion(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            template_id=body.template_id,
            version_number=next_number,
            status=TemplateVersionStatus.DRAFT,
            blueprint_json=ensure_blueprint_json(body.blueprint_json),
            changelog=body.changelog,
            metadata_json=body.metadata_json,
            is_immutable=False,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        await self.versions.add(entity)
        template.latest_version_number = next_number
        template.updated_by = _actor_id(actor)
        template.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="template_version",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(
                {"template_id": body.template_id, "version_number": next_number}
            ),
        )
        self.events.publish(
            event_type=ExperienceEventType.TEMPLATE_VERSION_CREATED,
            aggregate_type="template_version",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "template_id": str(body.template_id),
                "version_number": next_number,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

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

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

    async def update_version(
        self,
        tenant_id: UUID,
        version_id: UUID,
        body: ExperienceTemplateVersionUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExperienceTemplateVersion:
        entity = await self.get_version(tenant_id, version_id)
        self.version_policy.assert_can_update(entity)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "blueprint_json" in data:
            data["blueprint_json"] = ensure_blueprint_json(data["blueprint_json"])
        _apply_update(entity, data)
        entity.updated_by = _actor_id(actor)
        entity.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="template_version",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=ExperienceEventType.TEMPLATE_VERSION_UPDATED,
            aggregate_type="template_version",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "template_id": str(entity.template_id),
                "version_number": entity.version_number,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def publish_version(
        self, tenant_id: UUID, version_id: UUID, *, actor: CurrentUser | None = None
    ) -> ExperienceTemplateVersion:
        entity = await self.get_version(tenant_id, version_id)
        self.version_policy.assert_publish(current=entity.status)
        if not entity.blueprint_json:
            raise AppError(
                "blueprint_json برای انتشار الزامی است",
                error_code="blueprint_required",
                status_code=422,
            )
        template = await self.get_template(tenant_id, entity.template_id)
        now = datetime.now(timezone.utc)
        entity.status = TemplateVersionStatus.PUBLISHED
        entity.is_immutable = True
        entity.published_at = now
        entity.updated_by = _actor_id(actor)
        entity.version += 1
        template.published_version_id = entity.id
        if template.status == TemplateStatus.DRAFT:
            template.status = TemplateStatus.ACTIVE
        template.updated_by = _actor_id(actor)
        template.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="template_version",
            entity_id=entity.id,
            action=AuditAction.PUBLISH,
            actor_user_id=_actor_id(actor),
            changes={"status": "published", "is_immutable": True},
        )
        self.events.publish(
            event_type=ExperienceEventType.TEMPLATE_VERSION_PUBLISHED,
            aggregate_type="template_version",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "template_id": str(entity.template_id),
                "version_number": entity.version_number,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def retire_version(
        self, tenant_id: UUID, version_id: UUID, *, actor: CurrentUser | None = None
    ) -> ExperienceTemplateVersion:
        entity = await self.get_version(tenant_id, version_id)
        self.version_policy.assert_retire(current=entity.status)
        template = await self.get_template(tenant_id, entity.template_id)
        entity.status = TemplateVersionStatus.RETIRED
        entity.retired_at = datetime.now(timezone.utc)
        entity.updated_by = _actor_id(actor)
        entity.version += 1
        if template.published_version_id == entity.id:
            template.published_version_id = None
            template.updated_by = _actor_id(actor)
            template.version += 1
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="template_version",
            entity_id=entity.id,
            action=AuditAction.STATUS_CHANGE,
            actor_user_id=_actor_id(actor),
            changes={"status": "retired"},
        )
        self.events.publish(
            event_type=ExperienceEventType.TEMPLATE_VERSION_RETIRED,
            aggregate_type="template_version",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "template_id": str(entity.template_id),
                "version_number": entity.version_number,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    # --- Instantiation ---

    async def instantiate(
        self,
        tenant_id: UUID,
        body: TemplateInstantiateRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> tuple[TemplateInstantiation, ExperiencePage | None]:
        template = await self.get_template(tenant_id, body.template_id)
        self.instantiation_policy.assert_template_instantiable(template)
        version_id = body.template_version_id or template.published_version_id
        if version_id is None:
            raise AppError(
                "نسخه منتشرشده‌ای برای قالب وجود ندارد",
                error_code="template_no_published_version",
                status_code=409,
            )
        version = await self.get_version(tenant_id, version_id)
        if version.template_id != template.id:
            raise AppError(
                "template_version_id با template_id هم‌خوان نیست",
                error_code="version_template_mismatch",
                status_code=422,
            )
        self.instantiation_policy.assert_version_instantiable(version)

        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,
            )

        record = TemplateInstantiation(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            template_id=template.id,
            template_version_id=version.id,
            site_id=body.site_id,
            status=TemplateInstantiationStatus.PENDING,
            metadata_json=body.metadata_json,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        await self.instantiations.add(record)

        try:
            page, result = await self._build_from_blueprint(
                tenant_id,
                template=template,
                version=version,
                body=body,
                actor=actor,
            )
            record.page_id = page.id
            record.status = TemplateInstantiationStatus.COMPLETED
            record.result_json = result
            record.completed_at = datetime.now(timezone.utc)
            record.updated_by = _actor_id(actor)
            record.version += 1
            await self.audit.record(
                tenant_id=tenant_id,
                entity_type="template_instantiation",
                entity_id=record.id,
                action=AuditAction.CREATE,
                actor_user_id=_actor_id(actor),
                changes={
                    "page_id": str(page.id),
                    "template_id": str(template.id),
                    "status": "completed",
                },
            )
            self.events.publish(
                event_type=ExperienceEventType.TEMPLATE_INSTANTIATED,
                aggregate_type="template_instantiation",
                aggregate_id=record.id,
                tenant_id=tenant_id,
                payload={
                    "template_id": str(template.id),
                    "page_id": str(page.id),
                    "site_id": str(body.site_id),
                },
            )
            await self.session.commit()
            await self.session.refresh(record)
            await self.session.refresh(page)
            return record, page
        except Exception as exc:
            record.status = TemplateInstantiationStatus.FAILED
            record.error_message = str(exc)[:1000]
            record.completed_at = datetime.now(timezone.utc)
            record.updated_by = _actor_id(actor)
            record.version += 1
            await self.session.commit()
            await self.session.refresh(record)
            if isinstance(exc, AppError):
                raise
            raise AppError(
                "نمونه‌سازی قالب ناموفق بود",
                error_code="template_instantiation_failed",
                status_code=500,
                details={"instantiation_id": str(record.id)},
            ) from exc

    async def _build_from_blueprint(
        self,
        tenant_id: UUID,
        *,
        template: ExperienceTemplate,
        version: ExperienceTemplateVersion,
        body: TemplateInstantiateRequest,
        actor: CurrentUser | None,
    ) -> tuple[ExperiencePage, dict[str, Any]]:
        blueprint = version.blueprint_json or {}
        page_type_raw = blueprint.get("page_type") or template.page_type
        page_type = PageType(validate_page_type(str(page_type_raw)))
        code = validate_code(body.page_code)
        slug = _normalize_slug(body.page_slug)
        title = validate_non_empty(body.page_title, "page_title")
        if await self.pages.get_by_code(tenant_id, body.site_id, code):
            raise AppError(
                "کد صفحه تکراری است", error_code="duplicate_code", status_code=409
            )
        if await self.pages.get_by_slug(tenant_id, body.site_id, slug):
            raise AppError(
                "slug صفحه تکراری است", error_code="duplicate_slug", status_code=409
            )

        locale = str(blueprint.get("default_locale") or "fa")
        direction = str(blueprint.get("default_direction") or "rtl")
        if direction not in ("rtl", "ltr"):
            direction = "rtl"

        page = ExperiencePage(
            id=uuid4(),
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            site_id=body.site_id,
            code=code,
            slug=slug,
            title=title,
            summary=blueprint.get("summary"),
            page_type=page_type,
            publish_status=PublishStatus.DRAFT,
            locale=locale,
            text_direction=direction,
            content_shell=blueprint.get("content_shell"),
            seo_shell=blueprint.get("seo_shell"),
            external_refs=blueprint.get("external_refs"),
            metadata_json={
                "from_template_id": str(template.id),
                "from_template_version_id": str(version.id),
            },
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        await self.pages.add(page)

        result: dict[str, Any] = {
            "page_id": str(page.id),
            "theme_id": None,
            "layout_id": None,
            "theme_ready": False,
            "layout_ready": False,
            "placement_shells": [],
        }

        theme_id = blueprint.get("theme_id")
        if body.apply_theme and theme_id:
            theme = await self.themes.get(tenant_id, UUID(str(theme_id)))
            if theme is not None and theme.status == ThemeStatus.ACTIVE:
                result["theme_id"] = str(theme.id)
                result["theme_ready"] = True
            else:
                result["theme_ready"] = False

        layout_id = blueprint.get("layout_id")
        if body.apply_layout and layout_id:
            layout = await self.layouts.get(tenant_id, UUID(str(layout_id)))
            if layout is not None and layout.status == LayoutStatus.ACTIVE:
                result["layout_id"] = str(layout.id)
                result["layout_ready"] = True
            else:
                result["layout_ready"] = False

        if body.apply_placements:
            shells = blueprint.get("placement_shells") or []
            if isinstance(shells, list):
                result["placement_shells"] = shells

        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="page",
            entity_id=page.id,
            action=AuditAction.CREATE,
            actor_user_id=_actor_id(actor),
            changes={
                "from_template": str(template.id),
                "code": page.code,
                "page_type": page.page_type.value,
            },
        )
        self.events.publish(
            event_type=ExperienceEventType.PAGE_CREATED,
            aggregate_type="page",
            aggregate_id=page.id,
            tenant_id=tenant_id,
            payload={
                "code": page.code,
                "site_id": str(page.site_id),
                "from_template_id": str(template.id),
            },
        )
        return page, result

    async def get_instantiation(
        self, tenant_id: UUID, instantiation_id: UUID
    ) -> TemplateInstantiation:
        entity = await self.instantiations.get(tenant_id, instantiation_id)
        if entity is None:
            raise NotFoundError("نمونه‌سازی قالب یافت نشد")
        return entity

    async def list_instantiations(
        self,
        tenant_id: UUID,
        *,
        offset: int,
        limit: int,
        spec: InstantiationListSpec | None = None,
    ) -> tuple[list[TemplateInstantiation], int]:
        items, total = await self.instantiations.list_filtered(
            tenant_id, offset=offset, limit=limit, spec=spec
        )
        return list(items), total
