from __future__ import annotations from datetime import UTC, datetime from hashlib import sha256 from uuid import NAMESPACE_URL, UUID, uuid5 from src.domain.contracts import ( CurrentUser, ScriptConfigVersionAuditEventListResponse, ScriptConfigVersionAuditEventSummary, ScriptConfigVersionCreateRequest, ScriptConfigVersionResponse, ScriptConfigVersionStatus, ScriptConfigVersionSummary, TargetSiteConfigCreateRequest, TargetSiteConfigResponse, TargetSiteConfigUpdateRequest, ) _VERSION_CREATED_EVENT_TYPE = "SCRIPT_CONFIG_VERSION_CREATED" _VERSION_ACTIVATED_EVENT_TYPE = "SCRIPT_CONFIG_VERSION_ACTIVATED" _VERSION_ROLLBACK_EVENT_TYPE = "SCRIPT_CONFIG_VERSION_ROLLBACK" def create_target_site( repository: object, request: TargetSiteConfigCreateRequest, ) -> TargetSiteConfigResponse: now = _now() site = repository.target_sites.upsert( site_id=_stable_uuid(f"target-site:{request.slug}"), name=request.name, slug=request.slug, publishing_type=request.publishing_type, default_language=request.default_language, brand_voice=request.brand_voice, audience=request.audience, seo_rules=request.seo_rules, visual_rules=request.visual_rules, source_rules=request.source_rules, publishing_rules=request.publishing_rules, active_script_config_version_id=request.active_script_config_version_id, created_at=now, updated_at=now, ) return TargetSiteConfigResponse(site=site) def get_target_site( repository: object, site_id: UUID, ) -> TargetSiteConfigResponse: site = repository.target_sites.get_by_id(site_id) return TargetSiteConfigResponse(site=site) def update_target_site( repository: object, site_id: UUID, request: TargetSiteConfigUpdateRequest, ) -> TargetSiteConfigResponse: existing = repository.target_sites.get_by_id(site_id) changes = request.model_dump(exclude_unset=True) publishing_rules = ( request.publishing_rules if "publishing_rules" in changes else existing.publishing_rules ) now = _now() site = repository.target_sites.update( site_id=existing.id, name=changes.get("name", existing.name), slug=changes.get("slug", existing.slug), publishing_type=changes.get("publishing_type", existing.publishing_type), default_language=changes.get("default_language", existing.default_language), brand_voice=changes.get("brand_voice", existing.brand_voice), audience=changes.get("audience", existing.audience), seo_rules=changes.get("seo_rules", existing.seo_rules), visual_rules=changes.get("visual_rules", existing.visual_rules), source_rules=changes.get("source_rules", existing.source_rules), publishing_rules=publishing_rules, active_script_config_version_id=changes.get( "active_script_config_version_id", existing.active_script_config_version_id, ), updated_at=now, ) return TargetSiteConfigResponse(site=site) def create_script_config_version( repository: object, *, site_id: UUID, current_user: CurrentUser, request: ScriptConfigVersionCreateRequest, ) -> ScriptConfigVersionResponse: repository.target_sites.get_by_id(site_id) if request.rollback_target_version_id is not None: rollback_target = repository.script_config_versions.get_by_id( request.rollback_target_version_id ) if str(rollback_target["target_site_id"]) != str(site_id): raise LookupError( f"Rollback target version is not for site: {request.rollback_target_version_id}" ) existing_versions = repository.script_config_versions.list_for_site(site_id) version = request.version or _next_version(existing_versions) now = _now() version_id = _stable_uuid(f"script-config-version:{site_id}:{version}") status = ( ScriptConfigVersionStatus.ACTIVE if request.activate else ScriptConfigVersionStatus.DRAFT ) row = repository.script_config_versions.upsert( version_id=version_id, target_site_id=site_id, version=version, status=status, created_by=current_user.id, created_at=now, updated_at=now, diff=request.diff, rollback_target_version_id=request.rollback_target_version_id, activated_at=now if request.activate else None, publishing_yaml=request.publishing_yaml, publishing_yaml_hash=_sha256(request.publishing_yaml), transform_script=request.transform_script, transform_script_hash=_sha256(request.transform_script), ) if request.activate: row = repository.script_config_versions.activate( target_site_id=site_id, version_id=version_id, activated_at=now, rollback_target_version_id=request.rollback_target_version_id, ) repository.script_config_version_events.create( target_site_id=site_id, version_id=version_id, event_type=_VERSION_ACTIVATED_EVENT_TYPE, actor_user_id=current_user.id, payload={"action": "activate", "version": version}, created_at=now, ) else: repository.script_config_version_events.create( target_site_id=site_id, version_id=version_id, event_type=_VERSION_CREATED_EVENT_TYPE, actor_user_id=current_user.id, payload={"action": "create", "version": version}, created_at=now, ) return ScriptConfigVersionResponse(version=_script_version_summary(row)) def activate_script_config_version( repository: object, *, site_id: UUID, version_id: UUID, current_user: CurrentUser, ) -> ScriptConfigVersionResponse: repository.target_sites.get_by_id(site_id) row = repository.script_config_versions.activate( target_site_id=site_id, version_id=version_id, activated_at=_now(), ) repository.script_config_version_events.create( target_site_id=site_id, version_id=version_id, event_type=_VERSION_ACTIVATED_EVENT_TYPE, actor_user_id=current_user.id, payload={"action": "activate", "version_id": str(version_id)}, created_at=_now(), ) return ScriptConfigVersionResponse(version=_script_version_summary(row)) def rollback_script_config_version( repository: object, *, site_id: UUID, version_id: UUID, current_user: CurrentUser, ) -> ScriptConfigVersionResponse: target_site = repository.target_sites.get_by_id(site_id) version_row = repository.script_config_versions.get_by_id(version_id) if str(version_row["target_site_id"]) != str(site_id): raise LookupError( f"Script config version not found for site: {site_id} {version_id}" ) rollback_target_id = target_site.active_script_config_version_id if rollback_target_id == version_id: rollback_target_id = None row = repository.script_config_versions.activate( target_site_id=site_id, version_id=version_id, activated_at=_now(), rollback_target_version_id=rollback_target_id, ) repository.script_config_version_events.create( target_site_id=site_id, version_id=version_id, event_type=_VERSION_ROLLBACK_EVENT_TYPE, actor_user_id=current_user.id, payload={ "action": "rollback", "rollback_target_version_id": ( str(rollback_target_id) if rollback_target_id is not None else None ), }, created_at=_now(), ) return ScriptConfigVersionResponse(version=_script_version_summary(row)) def list_script_config_versions( repository: object, site_id: UUID, ) -> list[ScriptConfigVersionSummary]: repository.target_sites.get_by_id(site_id) rows = repository.script_config_versions.list_for_site(site_id) return [_script_version_summary(row) for row in rows] def list_script_config_audit_events( repository: object, site_id: UUID, ) -> ScriptConfigVersionAuditEventListResponse: repository.target_sites.get_by_id(site_id) version_rows = repository.script_config_versions.list_for_site(site_id) version_by_id = {str(row["id"]): row for row in version_rows} events = repository.script_config_version_events.list_for_site(site_id) summaries: list[ScriptConfigVersionAuditEventSummary] = [] for event in events: version_row = version_by_id.get(str(event["version_id"])) payload = event["payload"] if isinstance(event["payload"], dict) else {} summaries.append( ScriptConfigVersionAuditEventSummary( id=event["id"], target_site_id=event["target_site_id"], version_id=event["version_id"], event_type=event["event_type"], actor_user_id=event["actor_user_id"], payload=payload, created_at=event["created_at"], version=( int(version_row["version"]) if version_row is not None and version_row.get("version") is not None else None ), diff=( version_row["diff"] if version_row is not None and isinstance(version_row.get("diff"), dict) else {} ), rollback_target_version_id=( version_row["rollback_target_version_id"] if version_row is not None else None ), ) ) return ScriptConfigVersionAuditEventListResponse(events=summaries) def _script_version_summary(row: dict[str, object]) -> ScriptConfigVersionSummary: return ScriptConfigVersionSummary( id=row["id"], target_site_id=row["target_site_id"], version=row["version"], status=row["status"], diff=row["diff"], rollback_target_version_id=row["rollback_target_version_id"], publishing_yaml_hash=row["publishing_yaml_hash"], transform_script_hash=row["transform_script_hash"], created_by=row["created_by"], created_at=row["created_at"], activated_at=row["activated_at"], ) def _next_version(rows: list[dict[str, object]]) -> int: versions = [int(row["version"]) for row in rows] if not versions: return 1 return max(versions) + 1 def _stable_uuid(value: str) -> UUID: return uuid5(NAMESPACE_URL, f"ai-content-pipeline:{value}") def _sha256(value: str) -> str: return sha256(value.encode("utf-8")).hexdigest() def _now() -> datetime: return datetime.now(UTC)