from __future__ import annotations from typing import Any, List, Optional from fastapi import ( APIRouter, Body, Depends, HTTPException, Query, Response, Security, status, ) from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database.crud.faq import get_faq_page_by_id from app.database.crud.rules import ( clear_all_rules, create_or_update_rules, get_all_rules_versions, get_rules_by_language, restore_rules_version, ) from app.database.models import ServiceRule from app.services.faq_service import FaqService from app.services.privacy_policy_service import PrivacyPolicyService from app.services.public_offer_service import PublicOfferService from ..dependencies import get_db_session, require_api_token from ..schemas.pages import ( FaqPageCreateRequest, FaqPageListResponse, FaqPageResponse, FaqPageUpdateRequest, FaqReorderRequest, FaqStatusResponse, FaqStatusUpdateRequest, RichTextPageResponse, RichTextPageUpdateRequest, ServiceRulesHistoryResponse, ServiceRulesResponse, ServiceRulesUpdateRequest, ) router = APIRouter() def _serialize_rich_page( *, requested_language: str, content: str, language: str, is_enabled: Optional[bool], created_at, updated_at, splitter, ) -> RichTextPageResponse: pages = splitter(content or "") return RichTextPageResponse( requested_language=requested_language, language=language, is_enabled=is_enabled, content=content or "", content_pages=pages, created_at=created_at, updated_at=updated_at, ) def _serialize_faq_page(page) -> FaqPageResponse: return FaqPageResponse( id=page.id, language=page.language, title=page.title, content=page.content, content_pages=FaqService.split_content_into_pages(page.content), display_order=page.display_order, is_active=page.is_active, created_at=page.created_at, updated_at=page.updated_at, ) def _serialize_rules(rule: ServiceRule) -> ServiceRulesResponse: return ServiceRulesResponse( id=rule.id, title=rule.title, content=rule.content, language=rule.language, is_active=rule.is_active, created_at=rule.created_at, updated_at=rule.updated_at, ) @router.get("/public-offer", response_model=RichTextPageResponse) async def get_public_offer( _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), language: str = Query("ru", min_length=2, max_length=10), fallback: bool = Query(True, description="Использовать запасной язык, если контента нет"), include_disabled: bool = Query( True, description="Возвращать контент даже если страница выключена", ), ) -> RichTextPageResponse: requested_lang = PublicOfferService.normalize_language(language) offer = await PublicOfferService.get_offer(db, requested_lang, fallback=fallback) if not offer: raise HTTPException(status.HTTP_404_NOT_FOUND, "Public offer not found") if not include_disabled and not offer.is_enabled: raise HTTPException(status.HTTP_404_NOT_FOUND, "Public offer disabled") return _serialize_rich_page( requested_language=requested_lang, language=offer.language, is_enabled=offer.is_enabled, content=offer.content or "", created_at=offer.created_at, updated_at=offer.updated_at, splitter=PublicOfferService.split_content_into_pages, ) @router.put("/public-offer", response_model=RichTextPageResponse) async def update_public_offer( payload: RichTextPageUpdateRequest, _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> RichTextPageResponse: lang = PublicOfferService.normalize_language(payload.language) offer = await PublicOfferService.save_offer(db, lang, payload.content) if payload.is_enabled is not None: offer = await PublicOfferService.set_enabled(db, lang, payload.is_enabled) refreshed = await PublicOfferService.get_offer(db, lang, fallback=False) offer = refreshed or offer return _serialize_rich_page( requested_language=lang, language=offer.language, is_enabled=offer.is_enabled, content=offer.content or "", created_at=offer.created_at, updated_at=offer.updated_at, splitter=PublicOfferService.split_content_into_pages, ) @router.get("/privacy-policy", response_model=RichTextPageResponse) async def get_privacy_policy( _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), language: str = Query("ru", min_length=2, max_length=10), fallback: bool = Query(True), include_disabled: bool = Query(True), ) -> RichTextPageResponse: requested_lang = PrivacyPolicyService.normalize_language(language) policy = await PrivacyPolicyService.get_policy(db, requested_lang, fallback=fallback) if not policy: raise HTTPException(status.HTTP_404_NOT_FOUND, "Privacy policy not found") if not include_disabled and not policy.is_enabled: raise HTTPException(status.HTTP_404_NOT_FOUND, "Privacy policy disabled") return _serialize_rich_page( requested_language=requested_lang, language=policy.language, is_enabled=policy.is_enabled, content=policy.content or "", created_at=policy.created_at, updated_at=policy.updated_at, splitter=PrivacyPolicyService.split_content_into_pages, ) @router.put("/privacy-policy", response_model=RichTextPageResponse) async def update_privacy_policy( payload: RichTextPageUpdateRequest, _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> RichTextPageResponse: lang = PrivacyPolicyService.normalize_language(payload.language) policy = await PrivacyPolicyService.save_policy(db, lang, payload.content) if payload.is_enabled is not None: policy = await PrivacyPolicyService.set_enabled(db, lang, payload.is_enabled) refreshed = await PrivacyPolicyService.get_policy(db, lang, fallback=False) policy = refreshed or policy return _serialize_rich_page( requested_language=lang, language=policy.language, is_enabled=policy.is_enabled, content=policy.content or "", created_at=policy.created_at, updated_at=policy.updated_at, splitter=PrivacyPolicyService.split_content_into_pages, ) @router.get("/faq", response_model=FaqPageListResponse) async def list_faq_pages( _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), language: str = Query("ru", min_length=2, max_length=10), include_inactive: bool = Query(True), fallback: bool = Query(True), ) -> FaqPageListResponse: requested_lang = FaqService.normalize_language(language) pages = await FaqService.get_pages( db, requested_lang, include_inactive=include_inactive, fallback=fallback, ) resolved_language = requested_lang if pages: resolved_language = pages[0].language setting = await FaqService.get_setting(db, requested_lang, fallback=fallback) is_enabled = bool(setting.is_enabled) if setting else False if setting: resolved_language = setting.language serialized = [_serialize_faq_page(page) for page in pages] return FaqPageListResponse( requested_language=requested_lang, language=resolved_language, is_enabled=is_enabled, total=len(serialized), items=serialized, ) @router.get("/faq/status", response_model=FaqStatusResponse) async def get_faq_status( _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), language: str = Query("ru", min_length=2, max_length=10), fallback: bool = Query(True), ) -> FaqStatusResponse: requested_lang = FaqService.normalize_language(language) setting = await FaqService.get_setting(db, requested_lang, fallback=fallback) if not setting: return FaqStatusResponse( requested_language=requested_lang, language=requested_lang, is_enabled=False, ) return FaqStatusResponse( requested_language=requested_lang, language=setting.language, is_enabled=bool(setting.is_enabled), ) @router.put("/faq/status", response_model=FaqStatusResponse) async def update_faq_status( payload: Optional[FaqStatusUpdateRequest] = Body(None), language: str = Query("ru", min_length=2, max_length=10), is_enabled: Optional[bool] = Query(None), _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> FaqStatusResponse: resolved_language = FaqService.normalize_language( payload.language if payload and payload.language else language ) enabled_status = payload.is_enabled if payload else is_enabled if enabled_status is None: raise HTTPException( status.HTTP_400_BAD_REQUEST, "Parameter 'is_enabled' is required" ) setting = await FaqService.set_enabled(db, resolved_language, enabled_status) return FaqStatusResponse( requested_language=resolved_language, language=setting.language, is_enabled=bool(setting.is_enabled), ) @router.post("/faq", response_model=FaqPageResponse, status_code=status.HTTP_201_CREATED) async def create_faq_page( payload: FaqPageCreateRequest, _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> FaqPageResponse: lang = FaqService.normalize_language(payload.language) is_active = True if payload.is_active is None else payload.is_active page = await FaqService.create_page( db, language=lang, title=payload.title, content=payload.content, display_order=payload.display_order, is_active=is_active, ) return _serialize_faq_page(page) @router.get("/faq/{page_id}", response_model=FaqPageResponse) async def get_faq_page( page_id: int, _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), language: str = Query("ru", min_length=2, max_length=10), include_inactive: bool = Query(True), ) -> FaqPageResponse: requested_lang = FaqService.normalize_language(language) page = await FaqService.get_page( db, page_id, requested_lang, include_inactive=include_inactive, fallback=True, ) if not page: raise HTTPException(status.HTTP_404_NOT_FOUND, "FAQ page not found") return _serialize_faq_page(page) @router.put("/faq/{page_id}", response_model=FaqPageResponse) async def update_faq_page( page_id: int, payload: FaqPageUpdateRequest, _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> FaqPageResponse: page = await get_faq_page_by_id(db, page_id) if not page: raise HTTPException(status.HTTP_404_NOT_FOUND, "FAQ page not found") updated = await FaqService.update_page( db, page, title=payload.title, content=payload.content, display_order=payload.display_order, is_active=payload.is_active, ) return _serialize_faq_page(updated) @router.delete("/faq/{page_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_faq_page( page_id: int, _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> Response: page = await get_faq_page_by_id(db, page_id) if not page: raise HTTPException(status.HTTP_404_NOT_FOUND, "FAQ page not found") await FaqService.delete_page(db, page_id) return Response(status_code=status.HTTP_204_NO_CONTENT) @router.post("/faq/reorder", response_model=FaqPageListResponse) async def reorder_faq_pages( payload: FaqReorderRequest, _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> FaqPageListResponse: lang = FaqService.normalize_language(payload.language) ordered_payload = sorted(payload.items, key=lambda item: item.display_order) existing_pages = await FaqService.get_pages( db, lang, include_inactive=True, fallback=False, ) pages_by_id = {page.id: page for page in existing_pages} pages: List[Any] = [] for item in ordered_payload: page = pages_by_id.get(item.id) if not page: raise HTTPException( status.HTTP_404_NOT_FOUND, f"FAQ page {item.id} not found for language {lang}", ) pages.append(page) ordered_ids = {item.id for item in ordered_payload} remaining = [page for page in existing_pages if page.id not in ordered_ids] pages.extend(sorted(remaining, key=lambda page: (page.display_order, page.id))) await FaqService.reorder_pages(db, lang, pages) updated_pages = await FaqService.get_pages( db, lang, include_inactive=True, fallback=False, ) setting = await FaqService.get_setting(db, lang, fallback=False) serialized = [_serialize_faq_page(page) for page in updated_pages] return FaqPageListResponse( requested_language=lang, language=lang, is_enabled=bool(setting.is_enabled) if setting else False, total=len(serialized), items=serialized, ) @router.get("/service-rules", response_model=ServiceRulesResponse) async def get_service_rules( _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), language: str = Query("ru", min_length=2, max_length=10), fallback: bool = Query(True), ) -> ServiceRulesResponse: requested_lang = language.split("-")[0].lower() rules = await get_rules_by_language(db, requested_lang) if not rules and fallback: default_lang = (settings.DEFAULT_LANGUAGE or "ru").split("-")[0].lower() if default_lang != requested_lang: rules = await get_rules_by_language(db, default_lang) if not rules: raise HTTPException(status.HTTP_404_NOT_FOUND, "Service rules not found") return _serialize_rules(rules) @router.put("/service-rules", response_model=ServiceRulesResponse) async def update_service_rules( payload: ServiceRulesUpdateRequest, _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), ) -> ServiceRulesResponse: lang = payload.language.split("-")[0].lower() title = payload.title or "Правила сервиса" rules = await create_or_update_rules( db, content=payload.content, language=lang, title=title, ) return _serialize_rules(rules) @router.delete("/service-rules", status_code=status.HTTP_204_NO_CONTENT) async def clear_service_rules( _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), language: str = Query("ru", min_length=2, max_length=10), ) -> Response: lang = language.split("-")[0].lower() await clear_all_rules(db, lang) return Response(status_code=status.HTTP_204_NO_CONTENT) @router.get("/service-rules/history", response_model=ServiceRulesHistoryResponse) async def get_service_rules_history( _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), language: str = Query("ru", min_length=2, max_length=10), limit: int = Query(10, ge=1, le=100), ) -> ServiceRulesHistoryResponse: lang = language.split("-")[0].lower() history = await get_all_rules_versions(db, lang, limit=limit) items = [_serialize_rules(item) for item in history] return ServiceRulesHistoryResponse( language=lang, total=len(items), items=items, ) @router.post( "/service-rules/history/{rule_id}/restore", response_model=ServiceRulesResponse, status_code=status.HTTP_201_CREATED, ) async def restore_service_rules_version( rule_id: int, _: object = Security(require_api_token), db: AsyncSession = Depends(get_db_session), language: str = Query("ru", min_length=2, max_length=10), ) -> ServiceRulesResponse: lang = language.split("-")[0].lower() restored = await restore_rules_version(db, rule_id, language=lang) if not restored: raise HTTPException(status.HTTP_404_NOT_FOUND, "Rules version not found") return _serialize_rules(restored)