from collections.abc import Iterable from typing import Annotated, Any from anydi.ext.fastapi import Inject from fansifter_common.api.schemas import response_errors from fastapi import APIRouter, BackgroundTasks, Path, Query from starlette import status from starlette.responses import Response from dmp.api import auth from dmp.api.meta import schemas from dmp.api.meta.schemas import GetMetaAdAccountsInput from dmp.api.schemas import ( AdReportingConnectionCredentials, LimitOffsetPage, SyncFivetranTablesStateInput, ) from dmp.app_connections.exceptions import AppConnectionNotFoundError from dmp.audiences.exceptions import AudienceShareActiveConnectionNotFoundError from dmp.meta.dtos import AssignMetaUserAdAccountLabelData, MetaUserConnectionResult from dmp.meta.exceptions import ( MetaAdAccountNotFoundError, MetaAdAccountPermissionsError, MetaAdAccountTermsOfServiceError, MetaAdReportingConnectionConflictError, MetaAdReportingConnectionNotFoundError, MetaAdReportingConnectionSyncingError, MetaAudienceNotFoundError, MetaConnectionNotFoundError, MetaUserAdAccountNotFoundError, MetaUserConnectionNotFoundError, ) from dmp.meta.handlers import ( AssignLabelsToMetaUserAdAccountsHandler, AssignLabelsToMetaUserAdAccountsRequest, ConnectMetaAdReportingHandler, ConnectMetaAdReportingRequest, ConnectMetaUserHandler, ConnectMetaUserRequest, DeleteMetaAdAccountsHandler, DeleteMetaAdAccountsRequest, DeleteMetaUserAdAccountHandler, DeleteMetaUserAdAccountRequest, GetMetaAdAccountsHandler, GetMetaAdAccountsRequest, GetMetaAdAccountsV2Handler, GetMetaAdAccountsV2Request, GetMetaAdReportingConnectionHandler, GetMetaAdReportingConnectionRequest, GetMetaAudienceShareHandler, GetMetaAudienceShareRequest, GetMetaAudienceShareStatusHandler, GetMetaAudienceShareStatusRequest, GetMetaFivetranConnectionsHandler, GetMetaUserAdAccountsHandler, GetMetaUserAdAccountsRequest, GetMetaUserConnectionHandler, GetMetaUserConnectionRequest, NotifyMetaAdReportingSyncCompletedHandler, ReconnectMetaAdReportingHandler, ReconnectMetaAdReportingRequest, RefreshMetaAdReportingConnectionHandler, RefreshMetaAdReportingConnectionRequest, ShareMetaAudienceHandler, ShareMetaAudienceRequest, SyncMetaFivetranTablesStateHandler, SyncMetaFivetranTablesStateRequest, ) router = APIRouter(prefix="/meta", tags=["Meta"]) @router.get( "/user/connection", operation_id="getMetaUserConnection", response_model=schemas.MetaUserConnection, response_model_by_alias=True, ) def get_meta_user_connection( identity_id: auth.IdentityId, handler: Annotated[GetMetaUserConnectionHandler, Inject()], ) -> MetaUserConnectionResult: return handler.handle(GetMetaUserConnectionRequest(identity_id=identity_id)) @router.post( "/user/connection", operation_id="connectMetaUser", response_model=schemas.MetaUserConnection, response_model_by_alias=True, ) def connect_meta_user( identity_id: auth.IdentityId, data: schemas.ConnectMetaUserInput, handler: Annotated[ConnectMetaUserHandler, Inject()], ) -> MetaUserConnectionResult: return handler.handle( ConnectMetaUserRequest( identity_id=identity_id, user_id=data.user_id, access_token=data.access_token, ) ) @router.get( "/user/ad-accounts", operation_id="getMetaUserAdAccounts", response_model=list[schemas.MetaUserAdAccount], responses=response_errors( MetaConnectionNotFoundError, ), ) def get_user_ad_accounts( identity_id: auth.IdentityId, handler: Annotated[GetMetaUserAdAccountsHandler, Inject()], ) -> Any: return handler.handle(GetMetaUserAdAccountsRequest(identity_id=identity_id)) @router.post( "/user/ad-accounts", operation_id="assignLabelsToMetaUserAdAccounts", response_class=Response, status_code=status.HTTP_204_NO_CONTENT, ) def assign_labels_to_user_ad_accounts( identity_id: auth.IdentityId, data: schemas.AssignLabelsToMetaUserAdAccountsInput, handler: Annotated[AssignLabelsToMetaUserAdAccountsHandler, Inject()], ) -> None: handler.handle( AssignLabelsToMetaUserAdAccountsRequest( identity_id=identity_id, assignments=[ AssignMetaUserAdAccountLabelData( ad_account_id=assignment.ad_account_id, vendor_id=assignment.vendor_id, subaccount_id=assignment.subaccount_id, ) for assignment in data ], ) ) return None @router.delete( "/user/ad-accounts/{adAccountId}", operation_id="deleteMetaUserAdAccount", status_code=status.HTTP_204_NO_CONTENT, responses=response_errors( MetaAdAccountNotFoundError, ), ) def delete_meta_user_ad_account( identity_id: auth.IdentityId, ad_account_id: Annotated[str, Path(alias="adAccountId")], handler: Annotated[DeleteMetaUserAdAccountHandler, Inject()], ) -> None: handler.handle( DeleteMetaUserAdAccountRequest( identity_id=identity_id, ad_account_id=ad_account_id, ) ) @router.get( "/ad-accounts", operation_id="getMetaAdAccounts", response_model=list[schemas.MetaAdAccount], response_model_by_alias=True, ) def get_meta_ad_accounts( identity_id: auth.IdentityId, handler: Annotated[GetMetaAdAccountsHandler, Inject()], ) -> Any: return handler.handle(GetMetaAdAccountsRequest(identity_id=identity_id)) @router.get( "/ad-accounts-v2", operation_id="getMetaAdAccountsV2", response_model=LimitOffsetPage[schemas.MetaAdAccount], response_model_by_alias=True, ) def get_meta_ad_accounts_v2( identity_id: auth.IdentityId, handler: Annotated[GetMetaAdAccountsV2Handler, Inject()], data: Annotated[GetMetaAdAccountsInput, Query()], ) -> Any: limit = data.limit or GetMetaAdAccountsV2Request.DEFAULT_LIMIT offset = data.offset or GetMetaAdAccountsV2Request.DEFAULT_OFFSET order_by = data.order_by or GetMetaAdAccountsV2Request.DEFAULT_ORDER_BY response = handler.handle( GetMetaAdAccountsV2Request( identity_id=identity_id, vendor_id=data.vendor_id, subaccount_id=data.subaccount_id, limit=limit, offset=offset, order_by=order_by, ), ) return LimitOffsetPage( items=response.items, total=response.total, limit=limit, offset=offset, ) @router.delete( "/ad-accounts/{adAccountId}", operation_id="deleteMetaAdAccount", status_code=status.HTTP_204_NO_CONTENT, responses=response_errors( MetaAdAccountNotFoundError, MetaUserAdAccountNotFoundError, ), ) def delete_meta_ad_account( identity_id: auth.IdentityId, ad_account_id: Annotated[str, Path(alias="adAccountId")], data: Annotated[schemas.DeleteMetaAdAccountsInput, Query()], handler: Annotated[DeleteMetaAdAccountsHandler, Inject()], ) -> None: handler.handle( DeleteMetaAdAccountsRequest( identity_id=identity_id, ad_account_id=ad_account_id, vendor_id=data.vendor_id, subaccount_id=data.subaccount_id, ) ) @router.post( "/ad-reporting/connection", operation_id="connectMetaAdReporting", response_model=AdReportingConnectionCredentials, response_model_by_alias=True, responses=response_errors( MetaAdReportingConnectionConflictError, MetaAdReportingConnectionSyncingError, ), ) def connect_meta_ad_reporting( identity_id: auth.IdentityId, data: schemas.ConnectMetaAdReportingInput, handler: Annotated[ConnectMetaAdReportingHandler, Inject()], ) -> dict[str, Any]: connection, connect_card = handler.handle( ConnectMetaAdReportingRequest( identity_id=identity_id, redirect_uri=str(data.redirect_uri), ) ) return { "connectionId": connection.id, "connectCardUri": connect_card.uri, } @router.get( "/ad-reporting/connection", operation_id="GetMetaAdReportingConnection", response_model=schemas.MetaAdReportingConnection, response_model_by_alias=True, ) def get_meta_ad_reporting_connection( identity_id: auth.IdentityId, handler: Annotated[GetMetaAdReportingConnectionHandler, Inject()], ) -> Any: return handler.handle(GetMetaAdReportingConnectionRequest(identity_id=identity_id)) @router.post( "/ad-reporting/connection/reconnect", operation_id="reconnectMetaAdReporting", response_model=AdReportingConnectionCredentials, response_model_by_alias=True, ) def reconnect_meta_ad_reporting( identity_id: auth.IdentityId, data: schemas.ConnectMetaAdReportingInput, handler: Annotated[ReconnectMetaAdReportingHandler, Inject()], ) -> Any: return handler.handle( ReconnectMetaAdReportingRequest( identity_id=identity_id, redirect_uri=str(data.redirect_uri), ) ) @router.put( "/ad-reporting/connection/refresh", operation_id="refreshMetaAdReportingConnection", tags=["internal"], summary="Refresh Meta Ad Reporting connection. Internal use only.", response_class=Response, status_code=status.HTTP_204_NO_CONTENT, responses=response_errors( MetaAdReportingConnectionNotFoundError, ), ) def refresh_meta_ad_reporting_connection( data: schemas.RefreshMetaAdReportingConnectionInput, handler: Annotated[RefreshMetaAdReportingConnectionHandler, Inject()], ) -> None: handler.handle( RefreshMetaAdReportingConnectionRequest( fivetran_connection_id=data.fivetran_connection_id ) ) @router.post( "/audiences", operation_id="shareMetaAudience", response_model=schemas.MetaAudience, response_model_by_alias=True, responses=response_errors( MetaAdAccountPermissionsError, MetaAdAccountTermsOfServiceError, AudienceShareActiveConnectionNotFoundError, ), status_code=status.HTTP_201_CREATED, ) def share_meta_audience( identity_id: auth.IdentityId, data: schemas.ShareMetaAudienceInput, handler: Annotated[ShareMetaAudienceHandler, Inject()], ) -> Any: return handler.handle( ShareMetaAudienceRequest( identity_id=identity_id, audience_id=data.audience_id, ad_account_id=data.ad_account_id, justification=data.justification, reason_notes=data.reason_notes, ) ) @router.get( "/audiences/{metaAudienceId}/share-status", operation_id="getMetaAudienceShareStatus", response_model=schemas.MetaAudienceShareStatus, response_model_by_alias=True, responses=response_errors( MetaAudienceNotFoundError, ), ) def get_meta_audience_share_status( identity_id: auth.IdentityId, meta_audience_id: Annotated[str, Path(alias="metaAudienceId")], handler: Annotated[GetMetaAudienceShareStatusHandler, Inject()], ) -> Any: return handler.handle( GetMetaAudienceShareStatusRequest( identity_id=identity_id, meta_audience_id=meta_audience_id ) ) @router.get( "/audience-shares/{shareId}", tags=["internal"], operation_id="getMetaAudienceShare", response_model=schemas.MetaAudienceShare, responses=response_errors( MetaAudienceNotFoundError, MetaUserConnectionNotFoundError, ), ) def get_meta_audience_share( share_id: Annotated[str, Path(alias="shareId")], handler: Annotated[GetMetaAudienceShareHandler, Inject()], ) -> Any: return handler.handle(GetMetaAudienceShareRequest(share_id=share_id)) @router.post( "/ad-reporting/handle-dbt-sync", operation_id="handleMetaAdReportingDbtSync", tags=["internal"], response_class=Response, status_code=status.HTTP_204_NO_CONTENT, ) def handle_ad_reporting_dbt_sync( handler: Annotated[NotifyMetaAdReportingSyncCompletedHandler, Inject()], ) -> None: handler.handle() @router.post( "/sync-fivetran-tables-state", operation_id="syncMetaFivetranTablesState", summary="Sync Meta Fivetran tables state for particular connection or all at once. Internal use only.", tags=["internal"], response_model=Iterable[str], responses=response_errors( AppConnectionNotFoundError, ), ) def handle_sync_meta_fivetran_tables_state( data: SyncFivetranTablesStateInput, handler: Annotated[SyncMetaFivetranTablesStateHandler, Inject()], background_tasks: BackgroundTasks, ) -> Any: background_tasks.add_task( handler.handle, SyncMetaFivetranTablesStateRequest( fivetran_connection_id=data.fivetran_connection_id if data else None ), ) return [] @router.get( "/fivetran-connectors", operation_id="getMetaFivetranConnectorsIds", summary="Get all Fivetran connectors ids for Meta connections. Internal use only.", tags=["internal"], response_model=Iterable[str], ) def handle_get_meta_fivetran_connectors_ids( handler: Annotated[GetMetaFivetranConnectionsHandler, Inject()], ) -> Any: return handler.handle()